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

志愿原理

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

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

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

同時(shí),子類動(dòng)態(tài)增加方法setKeypath:初婆,動(dòng)態(tài)添加的方法會(huì)綁定到一個(gè)?語(yǔ)言的函數(shù)。

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

這樣做會(huì)相當(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句其實(shí)相當(dāng)于Object_KVONotify *obj = [[Object_KVONotify alloc] init]

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

obj.keypath ="hello world";

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

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

此時(shí)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];}

在對(duì)象類中添加測(cè)試代碼

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

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

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

上述過(guò)程就是志愿具體流程及測(cè)試代碼紫皇。具體的演示可以代碼在這里找到。

志愿痛點(diǎn)

大家都知道臣缀,系統(tǒng)KVO略有點(diǎn)難用坝橡,主要因?yàn)檫@幾點(diǎn):

addObserver后,不會(huì)在對(duì)象釋放時(shí)精置,自動(dòng)釋放计寇,只能我們?cè)赿ealloc中手動(dòng)removeObserver。在這樣的疏忽下情況忘記removeObserver可能會(huì)導(dǎo)致崩潰脂倦。另外番宁,這個(gè)限制讓我們無(wú)法在一個(gè)類中為其他類對(duì)象增加監(jiān)聽(tīng)。

如果沒(méi)有addObserver的英文不能removeObserver的赖阻,會(huì)崩潰蝶押。

不支持塊。

重新實(shí)現(xiàn)KVO

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

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

同時(shí),子類動(dòng)態(tài)增加方法setXXXX:榆浓,動(dòng)態(tài)添加的方法會(huì)綁定到一個(gè)?語(yǔ)言的函數(shù)于未。

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

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

@implementationNSObject(BlockKVO)-(void) addObserverForKeyPath:(NSString*)keyPath option:(NSKeyValueObservingOptions)option block:((^)(idobj,NSDictionary *change))block{//self.blockKVO是通過(guò)associate與NSObject對(duì)象綁定的//這樣我們就把所有邏輯轉(zhuǎn)移到了BlockKVO這個(gè)類中[self.blockKVO addObserver:selfforKeyPath:keyPath option:option block:block];}//這里覆蓋了系統(tǒng)的KVO監(jiān)聽(tīng)萍鲸,里面僅僅調(diào)用了添加監(jiān)聽(tīng)時(shí)的block//這樣做闷叉,可以讓系統(tǒng)的KVO監(jiān)聽(tīng)方法也能收到通過(guò)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)需要存儲(chǔ)猿推,而OC的類別中保存屬性是很麻煩的片习。

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

請(qǐng)仔細(xì)閱讀代碼中的注釋。

@implementationBlockKVO//這里的參數(shù)obj就是需要kvo的對(duì)象状知,這個(gè)函數(shù)很重要秽五,它做到了2件事//1 為obj的class 創(chuàng)建一個(gè)以`_NotifyKVO`為后綴的子類//2. 將obj的class指向XXX_NotifyKVO這個(gè)子類//搞這么多幺蛾子的好處是實(shí)現(xiàn)了AOP,原有的類沒(méi)有任何改變饥悴,obj仍然能訪問(wèn)原類的所有屬性方法坦喘,而且obj可以通過(guò)擴(kuò)展XXX_NotifyKVO方法盲再,增加功能,也能修改原來(lái)類的行為瓣铣,而不會(huì)影響原來(lái)類的結(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)建的子類,否則不會(huì)調(diào)到dynamicSetKeyPathobject_setClass(obj, dynamicClass);}}//這個(gè)方法是從原類中接收參數(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

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

這東西又叫做TypeEncoding从橘,為啥有這個(gè)東西呢念赶?

我們知道,OC是動(dòng)態(tài)語(yǔ)言恰力,它發(fā)送消息是要通過(guò)SEL去查找函數(shù)的叉谜,一旦找到了函數(shù)我們?cè)偃フ{(diào)用它就不是動(dòng)態(tài)調(diào)用了,而是靜態(tài)調(diào)用牺勾。

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

對(duì)于class_addMethod函數(shù)來(lái)說(shuō)驻民,TypeEncoding可以為添加的方法標(biāo)記出它的返回值類型翻具,參數(shù)個(gè)數(shù)和每個(gè)參數(shù)的類型。

上面的“v @:@”表示的是回还,所添加的函數(shù)指針裆泳,返回值為虛,有3個(gè)參數(shù)柠硕,第一個(gè)參數(shù)是id工禾,第二個(gè)參數(shù)是SEL,第三個(gè)參數(shù)是id蝗柔。很簡(jiǎn)單闻葵。

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

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

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

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

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

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

//這個(gè)函數(shù)的定義符合我們定義的typeencoding:"v@:@"staticvoiddynamicSetKeyPath(idobj, SEL sel,idvalue){BlockKVO *blockKVO = [obj blockKVO];//這里肯定不會(huì)為空,習(xí)慣性防御寫法if(blockKVO !=nil) {//根據(jù)SEL獲取keyPathNSString*keypath = getKeyPath(sel);//獲取到注冊(cè)KVO時(shí)傳入的參數(shù)早直,包括block啥的寥假。BlockKVOItem *item = [blockKVO itemWithKeyPath:keypath];//這里先將obj的class恢復(fù),否則會(huì)陷入循環(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通過(guò)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:的時(shí)候糕韧,前面注冊(cè)的KVO監(jiān)聽(tīng)的塊都會(huì)被調(diào)用。

整個(gè)KVO流程就完成了祥得。

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

不同類型的屬性支持

setValue:forKey:處理级及,弱變量可以通過(guò)這個(gè)函數(shù)處理乒疏。

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

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

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

作者:hard_man

鏈接:http://www.reibang.com/p/2a2a03681814

來(lái)源:簡(jiǎn)書

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市县踢,隨后出現(xiàn)的幾起案子转绷,更是在濱河造成了極大的恐慌,老刑警劉巖硼啤,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件议经,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡谴返,警方通過(guò)查閱死者的電腦和手機(jī)煞肾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)嗓袱,“玉大人籍救,你說(shuō)我怎么就攤上這事∏ǎ” “怎么了蝙昙?”我有些...
    開(kāi)封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)梧却。 經(jīng)常有香客問(wèn)我奇颠,道長(zhǎng),這世上最難降的妖魔是什么放航? 我笑而不...
    開(kāi)封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任大刊,我火速辦了婚禮,結(jié)果婚禮上三椿,老公的妹妹穿的比我還像新娘缺菌。我一直安慰自己,他們只是感情好搜锰,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布伴郁。 她就那樣靜靜地躺著,像睡著了一般蛋叼。 火紅的嫁衣襯著肌膚如雪焊傅。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天狈涮,我揣著相機(jī)與錄音狐胎,去河邊找鬼。 笑死歌馍,一個(gè)胖子當(dāng)著我的面吹牛握巢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播松却,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼暴浦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了晓锻?” 一聲冷哼從身側(cè)響起歌焦,我...
    開(kāi)封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎砚哆,沒(méi)想到半個(gè)月后独撇,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡躁锁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年纷铣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片灿里。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡关炼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出匣吊,到底是詐尸還是另有隱情儒拂,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布色鸳,位于F島的核電站社痛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏命雀。R本人自食惡果不足惜蒜哀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吏砂。 院中可真熱鬧撵儿,春花似錦乘客、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至浪默,卻和暖如春牡直,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纳决。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工碰逸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人阔加。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓饵史,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親掸哑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子约急,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容