iOS版的志愿實現(xiàn)剖析

志愿原理

對于志愿的原理,很多人都比較清楚了大概是這樣子的:

假定我們自己的類是Object和它的對象obj,當(dāng)obj發(fā)送addObserverForKeypath:keypath消息后,系統(tǒng)會做3件事情:

創(chuàng)建33動態(tài)一個Object的子類,名字可自定義假設(shè)叫做Object_KVONotify。

同時,子類動態(tài)增加方法setKeypath:,動態(tài)添加的方法會綁定到一個?語言的函數(shù)。

調(diào)用object_setClass函數(shù),將OBJ的類設(shè)置為Object_KVONotify。

這樣做會相當(dāng)于建立如下結(jié)構(gòu):

//Object@interfaceObject:NSObject@property(nonatomic,copy)NSString*keypath;@end@implementationObject-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{NSLog(@" --- Object observeValueForKeyPath:%@ ofObject:%@ change:%@ context:%@", keyPath, object, change, context);}-(NSString*) description{return[NSStringstringWithFormat:@"This is %@ instance keypath = %@",self.class,self.keypath];}@end//Object_KVONotify@interfaceObject_KVONotify:Object@endstaticvoiddynamicSetKeyPath(idobj, SEL sel,idv){... ...}@implementationObject_KVONotify-(void) setKeypath:(NSString*)keypath{dynamicSetKeyPath(self,@selector(setKeyPath:), keypath);}@end//objObject *obj = [[Object alloc] init];object_setClass(obj, Object_KVONotify.class);//上面2句其實相當(dāng)于Object_KVONotify *obj = [[Object_KVONotify alloc] init]

這樣一來,當(dāng)我們調(diào)用

obj.keypath ="hello world";

實際上調(diào)用的是

dynamicSetKeyPath(self,@selector(setKeypath:), keypath);

此時dynamicSetKeyPath要做2件事情。

父調(diào)用類的setKeyPath:方法。

調(diào)用observeValueForKeyPath方法,觸發(fā)回調(diào)。

所以dynamicSetKeyPath函數(shù)應(yīng)該是這樣的:

staticvoiddynamicSetKeyPath(idobj, SEL sel,idv){Method superMethod = class_getInstanceMethod(Object.class, sel);((void(*)(id, Method,id))method_invoke)(obj, superMethod, v);NSMutableDictionary* change = [[NSMutableDictionaryalloc] init];change[@"new"] = v;[obj observeValueForKeyPath:@"keypath"ofObject:obj change:change context:nil];}

或者這樣

staticvoiddynamicSetKeyPath(idobj, SEL sel,idv){object_setClass(obj, Object.class);[obj setValue: v forKey:@"keyPath"];object_setClass(obj, Object_Notify.class);[(Object *)obj observeValueForKeyPath:@"keypath"ofObject: objChange:@{@"new":v} context:nil];}

在對象類中添加測試代碼

+(void)test{Object*obj = [[Objectalloc] init];obj.keypath = @"inited";NSLog(@"%@", obj);object_setClass(obj, Object_KVONotify.class);obj.keypath = @"hello world";}

調(diào)用測試代碼,產(chǎn)生輸入如下

This isObjectinstance keypath = initedObjectobserveValueForKeyPath:keypath ofObject:This is Object_KVONotify instance keypath = hello world change:{new="hello world";} context:(null)

上述過程就是志愿具體流程及測試代碼。具體的演示可以代碼在這里找到。

志愿痛點

大家都知道,系統(tǒng)KVO略有點難用,主要因為這幾點:

addObserver后,不會在對象釋放時,自動釋放,只能我們在dealloc中手動removeObserver。在這樣的疏忽下情況忘記removeObserver可能會導(dǎo)致崩潰。另外,這個限制讓我們無法在一個類中為其他類對象增加監(jiān)聽。

如果沒有addObserver的英文不能removeObserver的,會崩潰。

不支持塊。

重新實現(xiàn)KVO

要重新實現(xiàn)志愿,根據(jù)志愿原理,我們需要創(chuàng)建一個增加監(jiān)聽的函數(shù),并在函數(shù)內(nèi)做到:

動態(tài)創(chuàng)建當(dāng)前類的的子類,名字帶固定后綴_NotifyKVO。

同時,子類動態(tài)增加方法setXXXX:,動態(tài)添加的方法會綁定到一個?語言的函數(shù)。

調(diào)用object_setClass函數(shù),將OBJ的類設(shè)置為XXXX_NotifyKVO。

首先我們創(chuàng)建一個NSObject的的分類,添加創(chuàng)建志愿方法。

@implementationNSObject(BlockKVO)-(void) addObserverForKeyPath:(NSString*)keyPath option:(NSKeyValueObservingOptions)option block:((^)(idobj,NSDictionary *change))block{//self.blockKVO是通過associate與NSObject對象綁定的//這樣我們就把所有邏輯轉(zhuǎn)移到了BlockKVO這個類中[self.blockKVO addObserver:selfforKeyPath:keyPath option:option block:block];}//這里覆蓋了系統(tǒng)的KVO監(jiān)聽,里面僅僅調(diào)用了添加監(jiān)聽時的block//這樣做,可以讓系統(tǒng)的KVO監(jiān)聽方法也能收到通過blockKVO添加的事件。-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{BlockKVOItem *item = [self.blockKVO itemWithKeyPath:keyPath];if(item.block) {item.block(self, keyPath, change);}}@end

由于我們有很多參數(shù)和狀態(tài)需要存儲,而OC的類別中保存屬性是很麻煩的。

所以我們將創(chuàng)建一個新的類來處理所有的綁定邏輯,這就需要將所有參數(shù)及對象本身傳遞到這個類對象中。

請仔細閱讀代碼中的注釋。

@implementationBlockKVO//這里的參數(shù)obj就是需要kvo的對象,這個函數(shù)很重要,它做到了2件事//1 為obj的class 創(chuàng)建一個以`_NotifyKVO`為后綴的子類//2. 將obj的class指向XXX_NotifyKVO這個子類//搞這么多幺蛾子的好處是實現(xiàn)了AOP,原有的類沒有任何改變,obj仍然能訪問原類的所有屬性方法,而且obj可以通過擴展XXX_NotifyKVO方法,增加功能,也能修改原來類的行為,而不會影響原來類的結(jié)構(gòu)。-(void) initKVOClassWithObj:(id) obj{if(self.srcClass ==nil){self.srcClass = [objclass];//添加子類NSString*dynamicClassName = [NSStringstringWithFormat:@"%@_NotifyKVO",NSStringFromClass(self.srcClass)];Class dynamicClass =NSClassFromString(dynamicClassName);if(!dynamicClass) {dynamicClass = objc_allocateClassPair(self.srcClass, dynamicClassName.UTF8String,0);objc_registerClassPair(dynamicClass);}self.dynamicClass = dynamicClass;//將obj的類換成新創(chuàng)建的子類,否則不會調(diào)到dynamicSetKeyPathobject_setClass(obj, dynamicClass);}}//這個方法是從原類中接收參數(shù)的,它只做2件事://1. 收到參數(shù)后,保存到observers字典中。//2. 根據(jù)keyPath,添加setter方法。-(void) addObserver: (id) obj forKeyPath:(NSString*)keyPath option:(NSKeyValueObservingOptions)option block:(void(^)(idobj,NSString*keyPath,NSDictionary *change))block{[selfinitKVOClassWithObj:obj];if(self.observers ==nil){self.observers = [[NSMutableDictionaryalloc] init];}if(self.observers[keyPath] !=nil){return;}//添加方法SEL methodSel = getSetSelector(keyPath);class_addMethod(self.dynamicClass, methodSel, (IMP)dynamicSetKeyPath,"v@:@");//保存BlockKVOItem *item = [[BlockKVOItem alloc] init];item.obj = obj;item.keyPath = keyPath;item.options = option;item.block = block;self.observers[keyPath] = item;}@end

會我們注意到class_addMethod方法,最后一個參數(shù)是一個奇怪的字符串。這個字符串是為了表示所添加方法的類型,包括返回值類型和所有參數(shù)類型。

這東西又叫做TypeEncoding,為啥有這個東西呢?

我們知道,OC是動態(tài)語言,它發(fā)送消息是要通過SEL去查找函數(shù)的,一旦找到了函數(shù)我們再去調(diào)用它就不是動態(tài)調(diào)用了,而是靜態(tài)調(diào)用。

靜態(tài)調(diào)用參數(shù)的數(shù)量和類型就很重要了。參數(shù)數(shù)量和類型其中任意一個對不上都會導(dǎo)致程序出錯。

對于class_addMethod函數(shù)來說,TypeEncoding可以為添加的方法標(biāo)記出它的返回值類型,參數(shù)個數(shù)和每個參數(shù)的類型。

上面的“v @:@”表示的是,所添加的函數(shù)指針,返回值為虛,有3個參數(shù),第一個參數(shù)是id,第二個參數(shù)是SEL,第三個參數(shù)是id。很簡單。

OC的類property可以很多種類型,不僅僅是id。所以如果想為不同類型調(diào)用class_addMethod,就要編寫不同的TypeEncoding。

列一下常用的TypeEncoding:( 更多細節(jié)查閱點這里TypeEncoding

“v @:q”=> setKeyPath :(很長)

“v @:c”=> setKeyPath:(char)

“v @:{CGSize = dd}”=> setKeypPath:(CGSize)

通過上述代碼,我們當(dāng)對象的調(diào)用再setKeyPath:方法的時候,調(diào)用實際上的的英文dynamicSetKeyPath函數(shù),我們看一下它的實現(xiàn):

//這個函數(shù)的定義符合我們定義的typeencoding:"v@:@"staticvoiddynamicSetKeyPath(idobj, SEL sel,idvalue){BlockKVO *blockKVO = [obj blockKVO];//這里肯定不會為空,習(xí)慣性防御寫法if(blockKVO !=nil) {//根據(jù)SEL獲取keyPathNSString*keypath = getKeyPath(sel);//獲取到注冊KVO時傳入的參數(shù),包括block啥的。BlockKVOItem *item = [blockKVO itemWithKeyPath:keypath];//這里先將obj的class恢復(fù),否則會陷入循環(huán)object_setClass(obj, blockKVO.srcClass);//獲取舊值idoldValue = [obj valueForKey:keypath];//設(shè)置新值[obj setValue:value forKey: keypath];//設(shè)置成子類object_setClass(obj, blockKVO.dynamicClass);//將oldValue和newValue通過observerValueForKeyPath:ofObject:change:方法通知給調(diào)用方(調(diào)用了block)NSMutableDictionary* change = [[NSMutableDictionaryalloc] init];if(item.options &NSKeyValueObservingOptionNew){change[@"old"] = oldValue;}if(item.options &NSKeyValueObservingOptionOld) {change[@"new"] = value;}[obj observeValueForKeyPath:keypath ofObject:obj change:change context:nil];}}

這樣,每次我們調(diào)用setKeyPath:的時候,前面注冊的KVO監(jiān)聽的塊都會被調(diào)用。

整個KVO流程就完成了。

當(dāng)然,如果實現(xiàn)完整的志愿,上面的代碼是不夠的你還需要解決如下問題:

不同類型的屬性支持

setValue:forKey:處理,弱變量可以通過這個函數(shù)處理。

線程安全(如果你只在主線程使用,則不必要)

動態(tài)創(chuàng)建類的釋放

其他可能出現(xiàn)的問題

作者:hard_man

鏈接:http://m.itdecent.cn/p/2a2a03681814

來源:簡書

簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容