使用方法
注冊Observer
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
options參數:
NSKeyValueObservingOptionNew:接收方法的change參數的中包含更改后的值
NSKeyValueObservingOptionOld:接收方法的change參數的中包含舊的值
NSKeyValueObservingOptionInitial:注冊的時候發(fā)一次通知,改變后也發(fā)送一次通知
NSKeyValueObservingOptionPrior:屬性改變之前發(fā)一次,改變之后再發(fā)一次
觸發(fā)方法
- 直接調用setter方法,或者通過屬性的點語法間接調用;
- 使用KVC的setValue:forKey: 或者 setValue:forKeyPath:方法;
- 通過mutableArrayValueForKey:K方法獲取到數組代理對象,并使用代理對象進行操作(注意:直接增刪是不會觸發(fā)的,當數組屬性添加了一個觀察者之后,通過mutableArrayValueForKey獲取的代理數組實際類型為NSKeyValueNotifyingMutableArray)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
change:在注冊時用options參數進行的配置,會包含不同的內容,其中kind關鍵字的值含義如下
kind = 1, 賦值 kind = 2, 插入kind= 3, 移除 kind = 4, 替換
延伸
I. kvo自定義結構體怎么取值?
答:當監(jiān)聽屬性是結構體時,可定義一個結構體類型并把它的地址傳進去getValue方法里
II. id是什么,和nsobject的區(qū)別?
答:id是動態(tài)數據類型,而NSObject *是靜態(tài)數據類型,默認情況下所有的數據類型都是靜態(tài)。id類型的實例在編譯階段不會做類型檢查,會在運行時確定,而類NSObject的實例在編譯期要做編譯檢查,保證指針指向是其NSObject類或其子類,當然,實例的具體類型也要在運行時才能確定,這也就是iOS三大特性之一的多態(tài)。
靜態(tài)類型在編譯時就知道變量的類型,編譯時就知道變量的類型,在編譯的時候就可以訪問對象的屬性和方法,如果訪問了不屬于靜態(tài)類型的屬性和方法,那么編譯器就會報錯,而動態(tài)數據類型在編譯的時候并不知道變量的真實類型,只有在運行時的時候才知道它的真實類型,因此編譯時候如果訪問了不屬于動態(tài)類型的屬性和方法,編譯器不會報錯,導致運行時的錯誤,這也是動態(tài)數據類型的弊端。實際上注冊后,KVO默認會自動通知觀察者,但其實也可以手動觸發(fā),首先在被觀察的類中重寫下面的方法:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ( [key isEqualToString:@"value"] ) {
return NO;
}
return YES;
}
在滿足某些條件我們在觸發(fā)調用接收方法,將條件代碼包裝下面任意一對方法中間。
- (will/did)ChangeValueForKey:
- (will/did)ChangeValueForKey:withSetMutation:usingObjects:
- (will/did)Change:valuesAtIndexes:forKey:
使用注意點
- 添加觀察者幾次,也要保證移除觀察者幾次
[m1 addObserver:self forKeyPath:@"value"
options:NSKeyValueObservingOptionNew context:@""];
[m1 addObserver:self forKeyPath:@"value"
options:NSKeyValueObservingOptionNew context:@""];
如果重復給一個對象多次添加相同的觀察者,那么當屬性發(fā)生改變時也會多次調用接收方法,同時在被觀察者被銷毀前也要移除同樣的次數,否則在低版本系統(tǒng)下會崩潰。
原因在于iOS9之前NotificationCenter.default對self是unsafe_unretained引用,當self釋放后,NotificationCenter.default持有的self并不會自動置為nil,而變成了一個野指針,這樣再給self發(fā)送通知的話就會造成崩潰(給野指針發(fā)送消息, iOS9之后對于普通的添加觀察者的方法不需要手動移除觀察者self,因為iOS9之后NotificationCenter.default對self是weak引用,當self釋放后,NotificationCenter.default持有的self會自動置為nil,而給一個nil發(fā)送推送的時候是不會發(fā)生崩潰的。
- 添加觀察者和刪除觀察者以及各自的KeyPath要一一對應
[m1 removeObserver:self.m2 forKeyPath:@"value" context:nil];
[m1 addObserver:m3 forKeyPath:@"value" options:options context:@""];
[m1 removeObserver:m2 forKeyPath:@"value" context:nil];
[m1 addObserver:m2 forKeyPath:@"v" options:options context:@""];
[m1 removeObserver:m2 forKeyPath:@"value" context:nil];
以上幾種情況程序都會崩潰。
實現(xiàn)原理
- 創(chuàng)建MYObject,然后添加一個value屬性;
- 創(chuàng)建兩個MYObject類的實例對象,然后其中為m1對象添加觀察者,然后觀察兩個對象的實際所屬類
self.m1 = [MYObject new];
self.m1.value = 1;
self.m2 = [MYObject new];
self.m2.value = 2;
NSLog(@"m1的類型:%@,m2的類型:%@",object_getClass(_m1),object_getClass(_m2));
[_m1 addObserver:self forKeyPath:@"value"
options:NSKeyValueObservingOptionNewcontext:@""];
NSLog(@"m1的類型:%@,m1的父類:%@, m2的類型:%@",object_getClass(_m1),
class_getSuperclass(object_getClass(_m1)),object_getClass(_m2));

從打印結果可以看到,m1對象未添加觀察者前,m1和m2都是MYObject類的對象,添加之后,m1變成了NSKVONotifying_MYObject類的對象,并且它是MYObject的子類。
下面觀察m1和m2的setValue:方法分別做了什么,通過methodForSelector獲取方法的地址,再用IMP把地址轉化成實際的方法名:
[_m1 methodForSelector:@selector(setValue:)] 0x10b826cb3
[_m2 methodForSelector:@selector(setValue:)] 0x10a6b9d30

可以看到NSKVONotifying_MYObject類的setter方法是Foundation框架中的_NSSetIntValueAndNotify函數,它的內部實現(xiàn)過程大致如下:
- (void)setValue:(int)value
{
_NSSetIntValueAndNotify();
}
void _NSSetIntValueAndNotify(){
[self willChangeValueForKey:@"value"];
[super setValue:value];
[self didChangeValueForKey:@"value"];
}
- (void)didChangeValueForKey:(NSString *)key{
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
3.NSKVONotifying_MYObject類中的方法
利用runtime打印一下類中的方法
unsigned int outCount = 0;
Method *methods = class_copyMethodList(object_getClass(self.m1), &outCount);
for (int i = 0; i < outCount; ++i) {
Method method = methods[i];
NSString *name = NSStringFromSelector(method_getName(method));
NSLog(@"%@", name);
}
free(methods);
打印結果:
這四個方法的含義分別是:
setValue: 實現(xiàn)值改變時的通知效果
class:隱藏NSKVONotifying_MYObject類,直接返回他的父類。
- (Class)class
{
return class_getSuperclass(object_getClass(self));
}
dealloc:runtime在實例對象添加了KVO之后動態(tài)創(chuàng)建了類和一些對象,所以可能會在dealloc中回收這些資源。
_isKVOA: 是否使用了KVO。
-
注意點 - KVO對生成的中間類的格式是有要求的,默認都是以NSKVONotifying_<class>來命名,那如果我們不小心自己創(chuàng)建了一個一樣名字的中間類,KVO就會無法使用。image.png
