KVO的相關知識(筆記)

使用方法

注冊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;
  1. 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)數據類型的弊端。

  2. 實際上注冊后,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:

使用注意點

  1. 添加觀察者幾次,也要保證移除觀察者幾次
[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ā)生崩潰的。

  1. 添加觀察者和刪除觀察者以及各自的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)原理

  1. 創(chuàng)建MYObject,然后添加一個value屬性;
  2. 創(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);

打印結果:
image.png

這四個方法的含義分別是:
setValue: 實現(xiàn)值改變時的通知效果
class:隱藏NSKVONotifying_MYObject類,直接返回他的父類。

- (Class)class
{
    return class_getSuperclass(object_getClass(self));
}

dealloc:runtime在實例對象添加了KVO之后動態(tài)創(chuàng)建了類和一些對象,所以可能會在dealloc中回收這些資源。
_isKVOA: 是否使用了KVO。

  1. 注意點 - KVO對生成的中間類的格式是有要求的,默認都是以NSKVONotifying_<class>來命名,那如果我們不小心自己創(chuàng)建了一個一樣名字的中間類,KVO就會無法使用。
    image.png
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容