<簡書 — 劉小壯> https://www.jianshu.com/p/badf5cac0130git
咱們在工做中常常會用到
KVO
,可是系統原生的KVO
並很差用,很容易致使Crash
。並且編寫代碼時,須要編寫大量KVO
相關的代碼,因爲不支持block
的形式,代碼會寫的很分散。github本篇文章對
KVO
的實現原理進行了詳細的分析,而且簡單的實現了一個KVO
,來當作技術交流。因爲系統提供的KVO
存在不少問題,在文章的最下面給出瞭解決方案。安全
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
枚舉。ui
還能夠經過方法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下載源碼仔細分析一下。