<簡書 — 劉小壯> https://www.jianshu.com/p/badf5cac0130git
咱們在工做中常常會用到KVO
,可是系統原生的KVO
並很差用,很容易致使Crash
。並且編寫代碼時,須要編寫大量KVO
相關的代碼,因爲不支持block
的形式,代碼會寫的很分散。本篇文章對
KVO
的實現原理進行了詳細的分析,而且簡單的實現了一個KVO
,來當作技術交流。因爲系統提供的KVO
存在不少問題,在文章的最下面給出瞭解決方案。github
KVO
全稱KeyValueObserving
,是蘋果提供的一套事件通知機制。容許對象監聽另外一個對象特定屬性的改變,並在改變時接收到事件。因爲KVO
的實現機制,因此對屬性纔會發生做用,通常繼承自NSObject
的對象都默認支持KVO
。安全
KVO
和NSNotificationCenter
都是iOS
中觀察者模式的一種實現。區別在於,相對於被觀察者和觀察者之間的關係,KVO
是一對一的,而一對多的。KVO
對被監聽對象無侵入性,不須要修改其內部代碼便可實現監聽。框架
KVO
能夠監聽單個屬性的變化,也能夠監聽集合對象的變化。經過KVC
的mutableArrayValueForKey:
等方法得到代理對象,當代理對象的內部對象發生改變時,會回調KVO
監聽的方法。集合對象包含NSArray
和NSSet
。函數
使用KVO
分爲三個步驟:源碼分析
addObserver:forKeyPath:options:context:
方法註冊觀察者,觀察者能夠接收keyPath
屬性的變化事件。observeValueForKeyPath:ofObject:change:context:
方法,當keyPath
屬性發生改變後,KVO
會回調這個方法來通知觀察者。removeObserver:forKeyPath:
方法將KVO
移除。須要注意的是,調用removeObserver
須要在觀察者消失以前,不然會致使Crash
。在註冊觀察者時,能夠傳入options
參數,參數是一個枚舉類型。若是傳入NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
表示接收新值和舊值,默認爲只接收新值。若是想在註冊觀察者後,當即接收一次回調,則能夠加入NSKeyValueObservingOptionInitial
枚舉。測試
還能夠經過方法context
傳入任意類型的對象,在接收消息回調的代碼中能夠接收到這個對象,是KVO中的一種傳值方式。atom
在調用addObserver
方法後,KVO
並不會對觀察者進行強引用,因此須要注意觀察者的生命週期,不然會致使觀察者被釋放帶來的Crash
。spa
觀察者須要實現observeValueForKeyPath:ofObject:change:context:
方法,當KVO
事件到來時會調用這個方法,若是沒有實現會致使Crash
。change
字典中存放KVO
屬性相關的值,根據options
時傳入的枚舉來返回。枚舉會對應相應key
來從字典中取出值,例若有NSKeyValueChangeOldKey
字段,存儲改變以前的舊值。代理
change
中還有NSKeyValueChangeKindKey
字段,和NSKeyValueChangeOldKey
是平級的關係,來提供本次更改的信息,對應NSKeyValueChange
枚舉類型的value
。例如被觀察屬性發生改變時,字段爲NSKeyValueChangeSetting
。
若是被觀察對象是集合對象,在NSKeyValueChangeKindKey
字段中會包含NSKeyValueChangeInsertion
、NSKeyValueChangeRemoval
、NSKeyValueChangeReplacement
的信息,表示集合對象的操做方式。
調用KVO
屬性對象時,不只能夠經過點語法和set
語法進行調用,KVO
兼容不少種調用方式。
// 直接調用set方法,或者經過屬性的點語法間接調用 [account setName:@"Savings"]; // 使用KVC的setValue:forKey:方法 [account setValue:@"Savings" forKey:@"name"]; // 使用KVC的setValue:forKeyPath:方法 [document setValue:@"Savings" forKeyPath:@"account.name"]; // 經過mutableArrayValueForKey:方法獲取到代理對象,並使用代理對象進行操做 Transaction *newTransaction = <#Create a new transaction for the account#>; NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"]; [transactions addObject:newTransaction];
KVO
主要用來作鍵值觀察操做,想要一個值發生改變後通知另外一個對象,則用KVO
實現最爲合適。斯坦福大學的iOS
教程中有一個很經典的案例,經過KVO
在Model
和Controller
之間進行通訊。
KVO
的addObserver
和removeObserver
須要是成對的,若是重複remove
則會致使NSRangeException
類型的Crash
,若是忘記remove
則會在觀察者釋放後再次接收到KVO
回調時Crash
。
蘋果官方推薦的方式是,在init
的時候進行addObserver
,在dealloc
時removeObserver
,這樣能夠保證add
和remove
是成對出現的,是一種比較理想的使用方式。
KVO
在屬性發生改變時的調用是自動的,若是想要手動控制這個調用時機,或想本身實現KVO
屬性的調用,則能夠經過KVO
提供的方法進行調用。
- (void)setBalance:(double)theBalance { if (theBalance != _balance) { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; } }
能夠看到調用KVO
主要依靠兩個方法,在屬性發生改變以前調用willChangeValueForKey:
方法,在發生改變以後調用didChangeValueForKey:
方法。
若是想控制當前對象的自動調用過程,也就是由上面兩個方法發起的KVO
調用,則能夠重寫下面方法。方法返回YES
則表示能夠調用,若是返回NO
則表示不能夠調用。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"balance"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic; }
KVO
是經過isa-swizzling
技術實現的(這句話是整個KVO
實現的重點)。在運行時根據原類建立一箇中間類,這個中間類是原類的子類,並動態修改當前對象的isa
指向中間類。而且將class
方法重寫,返回原類的Class
。因此蘋果建議在開發中不該該依賴isa
指針,而是經過class
實例方法來獲取對象類型。
爲了測試KVO
的實現方式,咱們加入下面的測試代碼。首先建立一個KVOObject
類,並在裏面加入兩個屬性,而後重寫description
方法,並在內部打印一些關鍵參數。
@interface KVOObject : NSObject @property (nonatomic, copy ) NSString *name; @property (nonatomic, assign) NSInteger age; @end @implementation KVOObject - (NSString *)description { NSLog(@"object address : %p \n", self); 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 < count; i++) { Method method = methodList[i]; NSString *methodName = NSStringFromSelector(method_getName(method)); NSLog(@"method Name = %@\n", methodName); } return @""; }
在另外一個類中分別建立兩個KVOObject
對象,其中一個對象被觀察者經過KVO
的方式監聽,另外一個對象則始終沒有被監聽。在KVO
先後分別打印兩個對象的關鍵信息,看KVO
先後有什麼變化。
@property (nonatomic, strong) KVOObject *object1; @property (nonatomic, strong) KVOObject *object2; self.object1 = [[KVOObject alloc] init]; self.object2 = [[KVOObject alloc] init]; [self.object1 description]; [self.object2 description]; [self.object1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; [self.object1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; [self.object1 description]; [self.object2 description]; self.object1.name = @"lxz"; self.object1.age = 20;
下面是KVO
先後打印的關鍵信息,咱們在下面作詳細分析。
// 第一次 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 : 0x604000237920 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 address : 0x604000237920 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
咱們發現對象被KVO
後,其真正類型變爲了NSKVONotifying_KVOObject
類,已經不是以前的類了。KVO
會在運行時動態建立一個新類,將對象的isa
指向新建立的類,新類是原類的子類,命名規則是NSKVONotifying_xxx
的格式。KVO
爲了使其更像以前的類,還會將對象的class
實例方法重寫,使其更像原類。
在上面的代碼中還發現了_isKVOA
方法,這個方法能夠當作使用了KVO
的一個標記,系統可能也是這麼用的。若是咱們想判斷當前類是不是KVO
動態生成的類,就能夠從方法列表中搜索這個方法。
KVO
會重寫keyPath
對應屬性的setter
方法,沒有被KVO
的屬性則不會重寫其setter
方法。在重寫的setter
方法中,修改值以前會調用willChangeValueForKey:
方法,修改值以後會調用didChangeValueForKey:
方法,這兩個方法最終都會被調用到observeValueForKeyPath:ofObject:change:context:
方法中。
爲何上面調用runtime
的object_getClass
函數,就能夠獲取到真正的類呢?
調用object_getClass
函數後其返回的是一個Class
類型,Class
是objc_class
定義的一個typedef
別名,經過objc_class
就能夠獲取到對象的isa
指針指向的Class
,也就是對象的類對象。
由此能夠推測,object_getClass
函數內部返回的是對象的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 }
蘋果提供的KVO
自身存在不少問題,首要問題在於,KVO
若是使用不當很容易崩潰。例如重複add
和remove
致使的Crash
,Observer
被釋放致使的崩潰,keyPath
傳錯致使的崩潰等。
在調用KVO
時須要傳入一個keyPath
,因爲keyPath
是字符串的形式,因此其對應的屬性發生改變後,字符串沒有改變容易致使Crash
。咱們能夠利用系統的反射機制將keyPath
反射出來,這樣編譯器能夠在@selector()
中進行合法性檢查。
NSStringFromSelector(@selector(isFinished))
KVO
是一種事件綁定機制的實現,在keyPath
對應的值發生改變後會回調對應的方法。這種數據綁定機制,在對象關係很複雜的狀況下,很容易致使很差排查的bug
。例如keyPath
對應的屬性被調用的關係很複雜,就不太建議對這個屬性進行KVO
,能夠想一下RAC
的信號腦補一下。
除了上面的缺點,KVO
還不支持block
語法,須要單獨重寫父類方法,這樣加上add
和remove
方法就會致使代碼很分散。因此,我經過runtime
簡單的實現了一個KVO
,源碼放在個人Github
上,叫作EasyKVO。
self.object1 = [[KVOObject alloc] init]; [self.object1 lxz_addObserver:self originalSelector:@selector(name) callback:^(id observedObject, NSString *observedKey, id oldValue, id newValue) { // callback }]; self.object1.name = @"lxz"; [self.object1 lxz_removeObserver:self originalSelector:@selector(name)];
調用代碼很簡單,直接經過lxz_addObserver:originalSelector:callback:
方法就能夠添加KVO
的監聽,能夠經過callback
的block
接收屬性發生改變後的回調,並且方法的keyPath
接收的是一個SEL
類型參數,因此能夠經過@selector()
傳入參數時進行方法合法性檢查,若是是未實現的方法直接就會報警告。
經過lxz_removeObserver:originalSelector:
方法傳入觀察者和keyPath
,當觀察者全部keyPath
都移除後則從KVO
中移除觀察者對象。
若是重複addObserver
和removeObserver
也沒事,內部有判斷邏輯。EasyKVO
內部經過weak
對觀察者作引用,並不會影響觀察者的生命週期,而且在觀察者釋放後不會致使Crash
。一次add
方法調用對應一個block
,若是觀察者監聽多個keyPath
屬性,不須要在block
回調中判斷keyPath
。
須要注意的是,EasyKVO
只是作技術交流,不建議在項目中使用。由於KVO
實現須要考慮不少狀況,繼承關係、多個觀察者等不少問題。
想在項目中安全便捷的使用KVO
的話,推薦Facebook
的一個KVO
開源第三方框架-KVOController。KVOController
本質上是對系統KVO
的封裝,具備原生KVO
全部的功能,並且規避了原生KVO
的不少問題,兼容block
和action
兩種回調方式。
從源碼來看仍是比較簡單的,主要分爲NSObject
的Category
和FBKVOController
兩部分。
在Category
中提供了KVOController
和KVOControllerNonRetaining
兩個屬性,顧名思義第一個會對observer
產生強引用,第二個則不會。其內部代碼就是建立FBKVOController
對象的代碼,並將建立出來的對象賦值給Category
的屬性,直接經過這個Category
就能夠懶加載建立FBKVOController
對象。
- (FBKVOController *)KVOControllerNonRetaining { id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey); if (nil == controller) { controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO]; self.KVOControllerNonRetaining = controller; } return controller; }
在FBKVOController
中分爲三部分,_FBKVOInfo
是一個私有類,這個類的功能很簡單,就是以結構化的形式保存FBKVOController
所需的各個對象,相似於模型類的功能。
還有一個私有類_FBKVOSharedController
,這是FBKVOController
框架實現的關鍵。從命名上能夠看出其是一個單例,全部經過FBKVOController
實現的KVO
,觀察者都是它。每次經過FBKVOController
添加一個KVO
時,_FBKVOSharedController
都會將本身設爲觀察者,並在其內部實現observeValueForKeyPath:ofObject:change:context:
方法,將接收到的消息經過block
或action
進行轉發。
其功能很簡單,經過observe:info:
方法添加KVO
監聽,並用一個NSHashTable
保存_FBKVOInfo
信息。經過unobserve:info:
方法移除監聽,並從NSHashTable
中將對應的_FBKVOInfo
移除。這兩個方法內部都會調用系統的KVO
方法。
在外界使用時須要用FBKVOController
類,其內部實現了初始化以及添加和移除監聽的操做。在調用添加監聽方法後,其內部會建立一個_FBKVOInfo
對象,並經過一個NSMapTable
對象進行持有,而後會調用_FBKVOSharedController
來進行註冊監聽。
使用FBKVOController
的話,不須要手動調用removeObserver
方法,在被監聽對象消失的時候,會在dealloc
中調用remove
方法。若是由於業務需求,能夠手動調用remove
方法,重複調用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
的實現很簡單,因此這裏就很簡單的講講,具體實現能夠去Github下載源碼仔細分析一下。