<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          KVO原理分析介紹

          共 15904字,需瀏覽 32分鐘

           ·

          2021-12-12 16:23

          ?

          本文字?jǐn)?shù):5154

          預(yù)計閱讀時間:15分鐘

          KVO原理分析

          介紹

          KVO全稱KeyValueObserving,是蘋果提供的一套事件通知機制。允許對象監(jiān)聽另一個對象特定屬性的改變,并在改變時接收到事件。由于KVO的實現(xiàn)機制,所以對屬性才會發(fā)生作用,一般繼承自NSObject的對象都默認(rèn)支持KVO。

          KVONSNotificationCenter都是iOS中觀察者模式的一種實現(xiàn)。區(qū)別在于,相對于被觀察者和觀察者之間的關(guān)系,KVO是一對一的,而不一對多的。KVO對被監(jiān)聽對象無侵入性,不需要手動修改其內(nèi)部代碼即可實現(xiàn)監(jiān)聽。

          KVO可以監(jiān)聽單個屬性的變化,也可以監(jiān)聽集合對象的變化。通過KVCmutableArrayValueForKey:等方法獲得代理對象,當(dāng)代理對象的內(nèi)部對象發(fā)生改變時,會回調(diào)KVO監(jiān)聽的方法。集合對象包含NSArrayNSSet。

          使用

          使用KVO分為三個步驟

          1. 通過addObserver:forKeyPath:options:context:方法注冊觀察者,觀察者可以接收keyPath屬性的變化事件回調(diào)。
          2. 在觀察者中實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法,當(dāng)keyPath屬性發(fā)生改變后,KVO會回調(diào)這個方法來通知觀察者。
          3. 當(dāng)觀察者不需要監(jiān)聽時,可以調(diào)用removeObserver:forKeyPath:方法將KVO移除。需要注意的是,調(diào)用removeObserver需要在觀察者消失之前,否則會導(dǎo)致Crash

          注冊

          在注冊觀察者時,可以傳入options參數(shù),參數(shù)是一個枚舉類型。如果傳入NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld表示接收新值和舊值,默認(rèn)為只接收新值。如果想在注冊觀察者后,立即接收一次回調(diào),則可以加入NSKeyValueObservingOptionInitial枚舉。

          還可以通過方法context傳入任意類型的對象,在接收消息回調(diào)的代碼中可以接收到這個對象,是KVO中的一種傳值方式。

          在調(diào)用addObserver方法后,KVO并不會對觀察者進(jìn)行強引用。所以需要注意觀察者的生命周期,否則會導(dǎo)致觀察者被釋放帶來的Crash。

          監(jiān)聽

          觀察者需要實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法,當(dāng)KVO事件到來時會調(diào)用這個方法,如果沒有實現(xiàn)會導(dǎo)致Crash。change字典中存放KVO屬性相關(guān)的值,根據(jù)options時傳入的枚舉來返回。枚舉會對應(yīng)相應(yīng)key來從字典中取出值,例如有NSKeyValueChangeOldKey字段,存儲改變之前的舊值。

          change中還有NSKeyValueChangeKindKey字段,和NSKeyValueChangeOldKey是平級的關(guān)系,來提供本次更改的信息,對應(yīng)NSKeyValueChange枚舉類型的value。例如被觀察屬性發(fā)生改變時,字段為NSKeyValueChangeSetting

          如果被觀察對象是集合對象,在NSKeyValueChangeKindKey字段中會包含NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement的信息,表示集合對象的操作方式。

          其他觸發(fā)方法

          調(diào)用KVO屬性對象時,不僅可以通過點語法和set語法進(jìn)行調(diào)用,KVO兼容很多種調(diào)用方式。

          //?直接調(diào)用set方法,或者通過屬性的點語法間接調(diào)用
          [account?setName:@"Savings"];

          //?使用KVC的setValue:forKey:方法
          [account?setValue:@"Savings"?forKey:@"name"];

          //?使用KVC的setValue:forKeyPath:方法
          [document?setValue:@"Savings"?forKeyPath:@"account.name"];

          //?通過mutableArrayValueForKey:方法獲取到代理對象,并使用代理對象進(jìn)行操作
          Transaction?*newTransaction?=?<#Create?a?new?transaction?for?the?account#>;
          NSMutableArray?*transactions?=?[account?mutableArrayValueForKey:@"transactions"];
          [transactions?addObject:newTransaction];

          實際應(yīng)用

          KVO主要用來做鍵值觀察操作,想要一個值發(fā)生改變后通知另一個對象,則用KVO實現(xiàn)最為合適。斯坦福大學(xué)的iOS教程中有一個很經(jīng)典的案例,通過KVOModelController之間進(jìn)行通信。


          觸發(fā)

          主動觸發(fā)

          KVO在屬性發(fā)生改變時的調(diào)用是自動的,如果想要手動控制這個調(diào)用時機,或想自己實現(xiàn)KVO屬性的調(diào)用,則可以通過KVO提供的方法進(jìn)行調(diào)用。

          -?(void)setBalance:(double)theBalance?{
          ????if?(theBalance?!=?_balance)?{
          ????????[self?willChangeValueForKey:@"balance"];
          ????????_balance?=?theBalance;
          ????????[self?didChangeValueForKey:@"balance"];
          ????}
          }

          可以看到調(diào)用KVO主要依靠兩個方法,在屬性發(fā)生改變之前調(diào)用willChangeValueForKey:方法,在發(fā)生改變之后調(diào)用didChangeValueForKey:方法。但是,如果不調(diào)用willChangeValueForKey,直接調(diào)用didChangeValueForKey是不生效的,二者有先后順序并且需要成對出現(xiàn)。

          禁用KVO

          如果想禁止某個屬性的KVO,例如關(guān)鍵信息不想被三方SDK通過KVO的方式獲取,可以通過automaticallyNotifiesObserversForKey方法返回NO來禁止其他地方對這個屬性進(jìn)行KVO。方法返回YES則表示可以調(diào)用,如果返回NO則表示不可以調(diào)用。此方法是一個類方法,可以在方法內(nèi)部判斷keyPath,來選擇這個屬性是否允許被KVO

          +?(BOOL)automaticallyNotifiesObserversForKey:(NSString?*)theKey?{
          ????BOOL?automatic?=?NO;
          ????if?([theKey?isEqualToString:@"balance"])?{
          ????????automatic?=?NO;
          ????}
          ????else?{
          ????????automatic?=?[super?automaticallyNotifiesObserversForKey:theKey];
          ????}
          ????return?automatic;
          }

          KVC觸發(fā)

          KVCKVO有特殊兼容,當(dāng)通過KVC調(diào)用非屬性的實例變量時,KVC內(nèi)部也會觸發(fā)KVO的回調(diào),并通過NSKeyValueDidChangeNSKeyValueWillChange向上回調(diào)。

          下面忽略main函數(shù)向上的系統(tǒng)函數(shù),只保留關(guān)鍵堆棧。這是通過調(diào)用屬性setter方法的方式回調(diào)的KVO堆棧。

          *?thread?#1,?queue?=?'com.apple.main-thread',?stop?reason?=?breakpoint?38.1
          *?frame?#0:?0x0000000101bc3a15?TestKVO`::-[ViewController?observeValueForKeyPath:ofObject:change:context:](self=0x00007f8419705890,?_cmd="observeValueForKeyPath:ofObject:change:context:",?keyPath="object",?object=0x0000604000015b00,?change=0x0000608000265540,?context=0x0000000000000000)?at?ViewController.mm:84
          frame?#1:?0x000000010327e820?Foundation`NSKeyValueNotifyObserver?+?349
          frame?#2:?0x000000010327e0d7?Foundation`NSKeyValueDidChange?+?483
          frame?#3:?0x000000010335f22b?Foundation`-[NSObject(NSKeyValueObservingPrivate)?_changeValueForKeys:count:maybeOldValuesDict:usingBlock:]?+?778
          frame?#4:?0x000000010324b1b4?Foundation`-[NSObject(NSKeyValueObservingPrivate)?_changeValueForKey:key:key:usingBlock:]?+?61
          frame?#5:?0x00000001032a7b79?Foundation`_NSSetObjectValueAndNotify?+?255
          frame?#6:?0x0000000101bc3937?TestKVO`::-[ViewController?viewDidLoad](self=0x00007f8419705890,?_cmd="viewDidLoad")?at?ViewController.mm:70

          這是通過KVC觸發(fā)的向上回調(diào),可以看到正常通過修改屬性的方式觸發(fā)KVO,和通過KVC觸發(fā)的KVO還是有區(qū)別的。通過KVC的方式觸發(fā)KVO,甚至都沒有_NSSetObjectValueAndNotify的調(diào)用。

          *?thread?#1,?queue?=?'com.apple.main-thread',?stop?reason?=?breakpoint?37.1
          *?frame?#0:?0x0000000106be1a85?TestKVO`::-[ViewController?observeValueForKeyPath:ofObject:change:context:](self=0x00007fe68ac07710,?_cmd="observeValueForKeyPath:ofObject:change:context:",?keyPath="object",?object=0x0000600000010c80,?change=0x000060c000262780,?context=0x0000000000000000)?at?ViewController.mm:84
          frame?#1:?0x000000010886d820?Foundation`NSKeyValueNotifyObserver?+?349
          frame?#2:?0x000000010886d0d7?Foundation`NSKeyValueDidChange?+?483
          frame?#3:?0x000000010894d422?Foundation`NSKeyValueDidChangeWithPerThreadPendingNotifications?+?148
          frame?#4:?0x0000000108879b47?Foundation`-[NSObject(NSKeyValueCoding)?setValue:forKey:]?+?292
          frame?#5:?0x0000000106be19aa?TestKVO`::-[ViewController?viewDidLoad](self=0x00007fe68ac07710,?_cmd="viewDidLoad")?at?ViewController.mm:70

          實現(xiàn)原理

          核心邏輯

          KVO是通過isa-swizzling技術(shù)實現(xiàn)的,這是整個KVO實現(xiàn)的重點。在運行時根據(jù)原類創(chuàng)建一個中間類,這個中間類是原類的子類,并動態(tài)修改當(dāng)前對象的isa指向中間類。并且將class方法重寫,返回原類的Class。蘋果重寫class方法,就是為了屏蔽中間類的存在。

          所以,蘋果建議在開發(fā)中不應(yīng)該依賴isa指針,而是通過class實例方法來獲取對象類型,來避免被KVO或者其他runtime方法影響。

          _NSSetObjectValueAndNotify

          隨后會修改中間類對應(yīng)的set方法,并且插入willChangeValueForkey方法以及didChangeValueForKey方法,在兩個方法中間調(diào)用父類的set方法。這個過程,系統(tǒng)將其封裝到_NSSetObjectValueAndNotify函數(shù)中。通過查看這個函數(shù)的匯編代碼,可以看到內(nèi)部封裝的willChangeValueForkey方法和didChangeValueForKey方法的調(diào)用。

          系統(tǒng)并不是只封裝了_NSSetObjectValueAndNotify函數(shù),而是會根據(jù)屬性類型,調(diào)用不同的函數(shù)。如果是Int類型就會調(diào)用_NSSetIntValueAndNotify,這些實現(xiàn)都定義在Foundation框架中。具體的可以通過hopper來查看Foundation框架的實現(xiàn)。

          runtime會將新生成的NSKVONotifying_KVOTestsetObject方法的實現(xiàn),替換成_NSSetObjectValueAndNotify函數(shù),而不是重寫setObject函數(shù)。通過下面的測試代碼,可以查看selector對應(yīng)的IMP,并且將其實現(xiàn)的地址打印出來。

          KVOTest?*test?=?[[KVOTest?alloc]?init];
          [test?setObject:[[NSObject?alloc]?init]];
          NSLog(@"%p",?[test?methodForSelector:@selector(setObject:)]);
          [test?addObserver:self?forKeyPath:@"object"?options:NSKeyValueObservingOptionNew?context:nil];
          [test?setObject:[[NSObject?alloc]?init]];
          NSLog(@"%p",?[test?methodForSelector:@selector(setObject:)]);

          //?打印結(jié)果,第一次的方法地址為0x100c8e270,第二次的方法地址為0x7fff207a3203
          (lldb)?p?(IMP)0x100c8e270
          (IMP)?$0?=?0x0000000100c8e270?(DemoProject`-[KVOTest?setObject:]?at?KVOTest.h:11)
          (lldb)?p?(IMP)0x7fff207a3203
          (IMP)?$1?=?0x00007fff207a3203?(Foundation`_NSSetObjectValueAndNotify)

          _NSKVONotifyingCreateInfoWithOriginalClass

          對于系統(tǒng)實現(xiàn)KVO的原理,可以對object_setClass打斷點,或者對objc_allocateClassPair方法打斷點也可以,這兩個方法都是創(chuàng)建類必走的方法。通過這兩個方法的匯編堆棧,向前回溯。隨后,可以得到翻譯后如下的匯編代碼。

          可以看到有一些類名拼接規(guī)則,隨后根據(jù)類名創(chuàng)建新類。如果newCls為空則已經(jīng)創(chuàng)建過,或者可能為空。如果newCls不為空,則注冊新創(chuàng)建的類,并且設(shè)置SDTestKVOClassIndexedIvars結(jié)構(gòu)體的一些參數(shù)。

          Class?_NSKVONotifyingCreateInfoWithOriginalClass(Class?originalClass)?{
          ????const?char?*clsName?=?class_getName(originalClass);
          ????size_t?len?=?strlen(clsName);
          ????len?+=?0x10;
          ????char?*newClsName?=?malloc(len);
          ????const?char?*prefix?=?"NSKVONotifying_";
          ????__strlcpy_chk(newClsName,?prefix,?len);
          ????__strlcat_chk(newClsName,?clsName,?len,?-1);
          ????Class?newCls?=?objc_allocateClassPair(originalClass,?newClsName,?0x68);
          ????if?(newCls)?{
          ????????objc_registerClassPair(newCls);
          ????????SDTestKVOClassIndexedIvars?*indexedIvars?=?object_getIndexedIvars(newCls);
          ????????indexedIvars->originalClass?=?originalClass;
          ????????indexedIvars->KVOClass?=?newCls;
          ????????CFMutableSetRef?mset?=?CFSetCreateMutable(nil,?0,?kCFCopyStringSetCallBacks);
          ????????indexedIvars->mset?=?mset;
          ????????CFMutableDictionaryRef?mdict?=?CFDictionaryCreateMutable(nil,?0,?nil,?kCFTypeDictionaryValueCallBacks);
          ????????indexedIvars->mdict?=?mdict;
          ????????pthread_mutex_init(indexedIvars->lock);
          ????????static?dispatch_once_t?onceToken;
          ????????dispatch_once(&onceToken,?^{
          ????????????bool?flag?=?true;
          ????????????IMP?willChangeValueForKeyImp?=?class_getMethodImplementation(indexedIvars->originalClass,?@selector(willChangeValueForKey:));
          ????????????IMP?didChangeValueForKeyImp?=?class_getMethodImplementation(indexedIvars->originalClass,?@selector(didChangeValueForKey:));
          ????????????if?(willChangeValueForKeyImp?==?_NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange?&&?didChangeValueForKeyImp?==?_NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange)?{
          ????????????????flag?=?false;
          ????????????}
          ????????????indexedIvars->flag?=?flag;
          ????????????NSKVONotifyingSetMethodImplementation(indexedIvars,?@selector(_isKVOA),?NSKVOIsAutonotifying,?nil);
          ????????????NSKVONotifyingSetMethodImplementation(indexedIvars,?@selector(dealloc),?NSKVODeallocate,?nil);
          ????????????NSKVONotifyingSetMethodImplementation(indexedIvars,?@selector(class),?NSKVOClass,?nil);
          ????????});
          ????}?else?{
          ????????return?nil;
          ????}
          ????return?newCls;
          }

          驗證

          為了驗證KVO的實現(xiàn)方式,我們加入下面的測試代碼。首先創(chuàng)建一個KVOObject類,并在里面加入兩個屬性,然后重寫description方法,并在內(nèi)部打印一些關(guān)鍵參數(shù)。

          需要注意的是,為了驗證KVO在運行時做了什么,我打印了對象的class方法,以及通過runtime獲取對象的類和父類。在添加KVO監(jiān)聽前后,都打印一次,觀察系統(tǒng)做了什么。

          @interface?KVOObject?:?NSObject
          @property?(nonatomic,?copy??)?NSString?*name;
          @property?(nonatomic,?assign)?NSInteger?age;
          @end

          -?(NSString?*)description?{
          ????IMP?nameIMP?=?class_getMethodImplementation(object_getClass(self),?@selector(setName:));
          ????IMP?ageIMP?=?class_getMethodImplementation(object_getClass(self),?@selector(setAge:));
          ????NSLog(@"object?setName:?IMP?%p?object?setAge:?IMP?%p?\n",?nameIMP,?ageIMP);
          ????
          ????Class?objectMethodClass?=?[self?class];
          ????Class?objectRuntimeClass?=?object_getClass(self);
          ????Class?superClass?=?class_getSuperclass(objectRuntimeClass);
          ????NSLog(@"objectMethodClass?:?%@,?ObjectRuntimeClass?:?%@,?superClass?:?%@?\n",?objectMethodClass,?objectRuntimeClass,?superClass);
          ????
          ????NSLog(@"object?method?list?\n");
          ????unsigned?int?count;
          ????Method?*methodList?=?class_copyMethodList(objectRuntimeClass,?&count);
          ????for?(NSInteger?i?=?0;?i?????????Method?method?=?methodList[i];
          ????????NSString?*methodName?=?NSStringFromSelector(method_getName(method));
          ????????NSLog(@"method?Name?=?%@\n",?methodName);
          ????}
          ????
          ????return?@"";
          }

          創(chuàng)建一個KVOObject對象,在KVO前后分別打印對象的關(guān)鍵信息,看KVO前后有什么變化。

          self.object?=?[[KVOObject?alloc]?init];
          [self.object?description];

          [self.object?addObserver:self?forKeyPath:@"name"?options:NSKeyValueObservingOptionNew?|?NSKeyValueObservingOptionOld?context:nil];

          [self.object?description];

          下面是KVO前后打印的關(guān)鍵信息。

          我們發(fā)現(xiàn)對象被KVO后,其真正類型變?yōu)榱?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(244, 138, 0);">NSKVONotifying_KVOObject類,已經(jīng)不是之前的類了。KVO會在運行時動態(tài)創(chuàng)建一個新類,將對象的isa指向新創(chuàng)建的類,并且將superClass指向原來的類KVOObject,新創(chuàng)建的類命名規(guī)則是NSKVONotifying_xxx的格式。KVO為了使其更像之前的類,還會將對象的class實例方法重寫,使其更像原類。

          添加KVO之后,由于修改了setName方法和setAge方法的IMP,所以打印這兩個方法的IMP,也是一個新的地址,新的實現(xiàn)在NSKVONotifying_KVOObject中。

          這種實現(xiàn)方式對業(yè)務(wù)代碼沒有侵入性,可以在不影響KVOObject其他對象的前提下,對單個對象進(jìn)行監(jiān)聽并修改其方法實現(xiàn),在賦值時觸發(fā)KVO回調(diào)。

          在上面的代碼中還發(fā)現(xiàn)了_isKVOA方法,這個方法可以當(dāng)做使用了KVO的一個標(biāo)記,系統(tǒng)可能也是這么用的。如果我們想判斷當(dāng)前類是否是KVO動態(tài)生成的類,就可以從方法列表中搜索這個方法。

          //?第一次
          object?address?:?0x604000239340
          object?setName:?IMP?0x10ddc2770?object?setAge:?IMP?0x10ddc27d0
          objectMethodClass?:?KVOObject,?ObjectRuntimeClass?:?KVOObject,?superClass?:?NSObject
          object?method?list
          method?Name?=?.cxx_destruct
          method?Name?=?description
          method?Name?=?name
          method?Name?=?setName:
          method?Name?=?setAge:
          method?Name?=?age

          //?第二次
          object?address?:?0x604000239340
          object?setName:?IMP?0x10ea8defe?object?setAge:?IMP?0x10ea94106
          objectMethodClass?:?KVOObject,?ObjectRuntimeClass?:?NSKVONotifying_KVOObject,?superClass?:?KVOObject
          object?method?list
          method?Name?=?setAge:
          method?Name?=?setName:
          method?Name?=?class
          method?Name?=?dealloc
          method?Name?=?_isKVOA


          object_getClass

          為什么上面調(diào)用runtimeobject_getClass函數(shù),就可以獲取到真正的類呢?

          調(diào)用object_getClass函數(shù)后其返回的是一個Class類型,Classobjc_class定義的一個typedef別名,通過objc_class就可以獲取到對象的isa指針指向的Class,也就是對象的類對象。

          由此可以知道,object_getClass函數(shù)內(nèi)部返回的是對象的isa指針。

          typedef?struct?objc_class?*Class;

          struct?objc_class?{
          ????Class?_Nonnull?isa??OBJC_ISA_AVAILABILITY;

          #if?!__OBJC2__
          ????Class?_Nullable?super_class??????????????????????????????OBJC2_UNAVAILABLE;
          ????const?char?*?_Nonnull?name???????????????????????????????OBJC2_UNAVAILABLE;
          ????long?version?????????????????????????????????????????????OBJC2_UNAVAILABLE;
          ????long?info????????????????????????????????????????????????OBJC2_UNAVAILABLE;
          ????long?instance_size???????????????????????????????????????OBJC2_UNAVAILABLE;
          ????struct?objc_ivar_list?*?_Nullable?ivars??????????????????OBJC2_UNAVAILABLE;
          ????struct?objc_method_list?*?_Nullable?*?_Nullable?methodLists????????????????????OBJC2_UNAVAILABLE;
          ????struct?objc_cache?*?_Nonnull?cache???????????????????????OBJC2_UNAVAILABLE;
          ????struct?objc_protocol_list?*?_Nullable?protocols??????????OBJC2_UNAVAILABLE;
          #endif
          }

          注意點

          Crash

          KVOaddObserverremoveObserver需要是成對的,如果重復(fù)remove則會導(dǎo)致NSRangeException類型的Crash,如果忘記remove則會在觀察者釋放后再次接收到KVO回調(diào)時Crash。

          蘋果官方推薦的方式是,在init的時候進(jìn)行addObserver,在deallocremoveObserver,這樣可以保證addremove是成對出現(xiàn)的,是一種比較理想的使用方式。

          錯誤檢查

          如果傳入一個錯誤的keyPath并不會有錯誤提示。在調(diào)用KVO時需要傳入一個keyPath,由于keyPath是字符串的形式,如果屬性名發(fā)生改變后,字符串沒有改變?nèi)菀讓?dǎo)致Crash。對于這個問題,我們可以利用系統(tǒng)的反射機制將keyPath反射出來,這樣編譯器可以在@selector()中進(jìn)行合法性檢查。

          NSString?*keyPath?=?NSStringFromSelector(@selector(isFinished));


          不能觸發(fā)回調(diào)

          由于KVO的實現(xiàn)機制,如果調(diào)用成員變量進(jìn)行賦值,是不會觸發(fā)KVO的。

          @interface?TestObject?:?NSObject?{
          ????@public
          ????NSObject?*object;
          }
          @end

          //?錯誤的調(diào)用方式
          self.object?=?[[TestObject?alloc]?init];
          [self.object?addObserver:self?forKeyPath:@"object"?options:NSKeyValueObservingOptionNew?context:nil];
          self.object->object?=?[[NSObject?alloc]?init];

          但是,如果通過KVC的方式調(diào)用賦值操作,則會觸發(fā)KVO的回調(diào)方法。這是因為KVCKVO有單獨的兼容,在KVC的賦值方法內(nèi)部,手動調(diào)用了willChangeValueForKey:didChangeValueForKey:方法。

          //?KVC的方式調(diào)用
          self.object?=?[[TestObject?alloc]?init];
          [self.object?addObserver:self?forKeyPath:@"object"?options:NSKeyValueObservingOptionNew?context:nil];
          [self.object?setValue:[[NSObject?alloc]?init]?forKey:@"object"];


          重復(fù)添加

          KVO進(jìn)行重復(fù)addObserver并不會導(dǎo)致崩潰,但是會出現(xiàn)重復(fù)執(zhí)行KVO回調(diào)方法的問題。

          [self.testLabel?addObserver:self?forKeyPath:@"text"?options:NSKeyValueObservingOptionNew?context:nil];
          self.testLabel.text?=?@"test";
          [self.testLabel?addObserver:self?forKeyPath:@"text"?options:NSKeyValueObservingOptionNew?context:nil];
          [self.testLabel?addObserver:self?forKeyPath:@"text"?options:NSKeyValueObservingOptionNew?context:nil];
          [self.testLabel?addObserver:self?forKeyPath:@"text"?options:NSKeyValueObservingOptionNew?context:nil];
          self.testLabel.text?=?@"test";

          //?輸出
          2018-08-03?11:48:49.502450+0800?KVOTest[5846:412257]?test
          2018-08-03?11:48:52.975102+0800?KVOTest[5846:412257]?test
          2018-08-03?11:48:53.547145+0800?KVOTest[5846:412257]?test
          2018-08-03?11:48:54.087171+0800?KVOTest[5846:412257]?test
          2018-08-03?11:48:54.649244+0800?KVOTest[5846:412257]?test

          通過上面的測試代碼,并且在回調(diào)中打印object所對應(yīng)的Class來看,并不會重復(fù)創(chuàng)建子類,始終都是一個類。雖然重復(fù)addobserver不會立刻崩潰,但是重復(fù)添加后在第一次調(diào)用removeObserver時,就會立刻崩潰。從崩潰堆棧來看,和重復(fù)移除的問題一樣,都是系統(tǒng)主動拋出的異常。

          Terminating?app?due?to?uncaught?exception?'NSRangeException',?reason:?'Cannot?remove?an?observer??for?the?key?path?"text"?from??because?it?is?not?registered?as?an?observer.'

          重復(fù)移除

          KVO是不允許對一個keyPath進(jìn)行重復(fù)移除的,如果重復(fù)移除,則會導(dǎo)致崩潰。例如下面的測試代碼。

          [self.testLabel?addObserver:self?forKeyPath:@"text"?options:NSKeyValueObservingOptionNew?context:nil];
          self.testLabel.text?=?@"test";
          [self.testLabel?removeObserver:self?forKeyPath:@"text"];
          [self.testLabel?removeObserver:self?forKeyPath:@"text"];
          [self.testLabel?removeObserver:self?forKeyPath:@"text"];

          執(zhí)行上面的測試代碼后,會造成下面的崩潰信息。從KVO的崩潰堆??梢钥闯鰜?,系統(tǒng)為了實現(xiàn)KVOaddObserverremoveObserver,為NSObject添加了一個名為NSKeyValueObserverRegistrationCategory,KVOaddObserverremoveObserver的實現(xiàn)都在里面。

          在移除KVO的監(jiān)聽時,系統(tǒng)會判斷當(dāng)前KVOkeyPath是否已經(jīng)被移除,如果已經(jīng)被移除,則主動拋出一個NSException的異常。

          2018-08-03?10:54:27.477379+0800?KVOTest[4939:286991]?***?Terminating?app?due?to?uncaught?exception?'NSRangeException',?reason:?'Cannot?remove?an?observer??for?the?key?path?"text"?from??because?it?is?not?registered?as?an?observer.'
          ***?First?throw?call?stack:
          (
          ?0???CoreFoundation??????????????????????0x000000010db2312b?__exceptionPreprocess?+?171
          ?1???libobjc.A.dylib?????????????????????0x000000010cc6af41?objc_exception_throw?+?48
          ?2???CoreFoundation??????????????????????0x000000010db98245?+[NSException?raise:format:]?+?197
          ?3???Foundation??????????????????????????0x0000000108631f15?-[NSObject(NSKeyValueObserverRegistration)?_removeObserver:forProperty:]?+?497
          ?4???Foundation??????????????????????????0x0000000108631ccb?-[NSObject(NSKeyValueObserverRegistration)?removeObserver:forKeyPath:]?+?84
          ?5???KVOTest?????????????????????????????0x0000000107959a55?-[ViewController?viewDidAppear:]?+?373
          ?//?.....
          ?20??UIKit???????????????????????????????0x000000010996d5d6?UIApplicationMain?+?159
          ?21??KVOTest?????????????????????????????0x00000001079696cf?main?+?111
          ?22??libdyld.dylib???????????????????????0x000000010fb43d81?start?+?1
          )
          libc++abi.dylib:?terminating?with?uncaught?exception?of?type?NSException


          排查鏈路

          KVO是一種事件綁定機制的實現(xiàn),在keyPath對應(yīng)的值發(fā)生改變后會回調(diào)對應(yīng)的方法。這種數(shù)據(jù)綁定機制,在對象關(guān)系很復(fù)雜的情況下,很容易導(dǎo)致不好排查的bug。例如keyPath對應(yīng)的屬性被調(diào)用的關(guān)系很復(fù)雜,就不太建議對這個屬性進(jìn)行KVO。

          自己實現(xiàn)KVO

          除了上面的缺點,KVO還不支持block語法,需要單獨重寫父類方法,這樣加上addremove方法就會導(dǎo)致代碼很分散。所以,我通過runtime簡單的實現(xiàn)了一個KVO,源碼放在我的Github上,叫做EasyKVO。

          self.object?=?[[KVOObject?alloc]?init];
          [self.object?lxz_addObserver:self?originalSelector:@selector(name)?callback:^(id?observedObject,?NSString?*observedKey,?id?oldValue,?id?newValue)?{
          ????//?處理業(yè)務(wù)邏輯
          }];

          self.object.name?=?@"lxz";

          //?移除通知
          [self.object?lxz_removeObserver:self?originalSelector:@selector(name)];

          調(diào)用代碼很簡單,直接通過lxz_addObserver:originalSelector:callback:方法就可以添加KVO的監(jiān)聽,可以通過callbackblock接收屬性發(fā)生改變后的回調(diào)。而且方法的keyPath接收的是一個SEL類型參數(shù),所以可以通過@selector()傳入?yún)?shù)時進(jìn)行方法合法性檢查,如果是未實現(xiàn)的方法直接就會報警告。

          通過lxz_removeObserver:originalSelector:方法傳入觀察者和keyPath,當(dāng)觀察者所有keyPath都移除后則從KVO中移除觀察者對象。

          如果重復(fù)addObserverremoveObserver也沒事,內(nèi)部有判斷邏輯。EasyKVO內(nèi)部通過weak對觀察者做引用,并不會影響觀察者的生命周期,并且在觀察者釋放后不會導(dǎo)致Crash。一次add方法調(diào)用對應(yīng)一個block,如果觀察者監(jiān)聽多個keyPath屬性,不需要在block回調(diào)中判斷keyPath

          KVOController

          想在項目中安全便捷的使用KVO的話,推薦Facebook的一個KVO開源第三方框架KVOController。KVOController本質(zhì)上是對系統(tǒng)KVO的封裝,具有原生KVO所有的功能,而且規(guī)避了原生KVO的很多問題,兼容blockaction兩種回調(diào)方式。

          源碼分析

          從源碼來看還是比較簡單的,主要分為NSObjectCategoryFBKVOController兩部分。

          Category中提供了KVOControllerKVOControllerNonRetaining兩個屬性,顧名思義第一個會對observer產(chǎn)生強引用,第二個則不會。其內(nèi)部代碼就是創(chuàng)建FBKVOController對象的代碼,并將創(chuàng)建出來的對象賦值給Category的屬性,直接通過這個Category就可以懶加載創(chuàng)建FBKVOController對象。

          -?(FBKVOController?*)KVOControllerNonRetaining
          {
          ??id?controller?=?objc_getAssociatedObject(self,?NSObjectKVOControllerNonRetainingKey);
          ??
          ??if?(nil?==?controller)?{
          ????controller?=?[[FBKVOController?alloc]?initWithObserver:self?retainObserved:NO];
          ????self.KVOControllerNonRetaining?=?controller;
          ??}
          ??
          ??return?controller;
          }

          實現(xiàn)原理

          FBKVOController中分為三部分,_FBKVOInfo是一個私有類,這個類的功能很簡單,就是以結(jié)構(gòu)化的形式保存FBKVOController所需的各個對象,類似于模型類的功能。

          還有一個私有類_FBKVOSharedController,這是FBKVOController框架實現(xiàn)的關(guān)鍵。從命名上可以看出其是一個單例,所有通過FBKVOController實現(xiàn)的KVO,觀察者都是它。每次通過FBKVOController添加一個KVO時,_FBKVOSharedController都會將自己設(shè)為觀察者,并在其內(nèi)部實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法,將接收到的消息通過blockaction進(jìn)行轉(zhuǎn)發(fā)。

          其功能很簡單,通過observe:info:方法添加KVO監(jiān)聽,并用一個NSHashTable保存_FBKVOInfo信息。通過unobserve:info:方法移除監(jiān)聽,并從NSHashTable中將對應(yīng)的_FBKVOInfo移除。這兩個方法內(nèi)部都會調(diào)用系統(tǒng)的KVO方法。

          在外界使用時需要用FBKVOController類,其內(nèi)部實現(xiàn)了初始化以及添加和移除監(jiān)聽的操作。在調(diào)用添加監(jiān)聽方法后,其內(nèi)部會創(chuàng)建一個_FBKVOInfo對象,并通過一個NSMapTable對象進(jìn)行持有,然后會調(diào)用_FBKVOSharedController來進(jìn)行注冊監(jiān)聽。

          使用FBKVOController的話,不需要手動調(diào)用removeObserver方法,在被監(jiān)聽對象消失的時候,會在dealloc中調(diào)用remove方法。如果因為業(yè)務(wù)需求,可以手動調(diào)用remove方法,重復(fù)調(diào)用remove方法不會有問題。

          -?(void)_observe:(id)object?info:(_FBKVOInfo?*)info
          {
          ????NSMutableSet?*infos?=?[_objectInfosMap?objectForKey:object];

          ????_FBKVOInfo?*existingInfo?=?[infos?member:info];
          ????if?(nil?!=?existingInfo)?{
          ??????return;
          ????}

          ????if?(nil?==?infos)?{
          ??????infos?=?[NSMutableSet?set];
          ??????[_objectInfosMap?setObject:infos?forKey:object];
          ????}

          ????[infos?addObject:info];

          ????[[_FBKVOSharedController?sharedController]?observe:object?info:info];
          }

          因為FBKVOController的實現(xiàn)很簡單,所以這里就很簡單的講講,具體實現(xiàn)可以去Github下載源碼仔細(xì)分析一下。

          瀏覽 36
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久9999 | 豆花av在线观看 豆花AV在线入口 豆花精品在线视频 | 在线播放a| 超碰 天天干 天天摸 | 一级免费毛片 |