弄透KVO

目標

  1. KVO的本質是什麼?
  2. 如何手動觸發KVO?
  3. 如何本身動手實現KVO?
  4. 直接修改爲員變量會觸發KVO嗎?

KVO的本質是什麼?

想要回答這個問題,首先須要弄明白當給一個對象的屬性添加KVO後,系統作了哪些事情?git

  • 當給一個對象的屬性添加KVO監聽時,系統會利用Runtime動態建立一個類,這個類是對象父類的子類
  • 將對象的isa指針由父類更改到新建的子類
  • 重寫子類的setter方法,在子類的setter方法中添加觸發KVO監聽者的監聽方法的機制

因此,KVO的本質是修改屬性的setter方法,在屬性的setter方法裏添加調用監聽方法的邏輯,爲了避免破壞原始類,系統又增長了動態建立子類並修改對象的isa指針的機制。github

如何手動觸發KVO?

想要知道如何手動觸發KVO,首先須要弄明白系統是如何修改setter方法以調用監聽方法的。數組

  • 在子類的setter方法會調用 _NSSetXXXValueAndNotify 函數
  • _NSSetXXXValueAndNotify 函數內部會調用 willChangeValueForKey: 和 didChangeValueForKey: 方法
  • didChangeValueForKey: 內部會調用KVO監聽者的監聽方法

因此,想要手動觸發KVO,能夠經過手動調用willChangeValueForKey:didChangeValueForKey: 來實現,須要強調這兩個方法都必須調用纔會起做用bash

如何手動實現KVO?

手動實現KVO的過程就是把系統實現KVO的過程本身用代碼實現一下。大體流程以下:app

  1. 添加一個NSObject分類,在分類中添加addObserver和removeObserver方法
  2. 判斷監聽的對象是否含有對應的key的setter方法
  3. 判斷添加了前綴的子類是否已經存在
  4. 利用runtime的 objc_allocateClassPair 函數動態建立子類
  5. 判斷KVO類有沒有重寫過setter方法
  6. 利用runtime的 class_addMethod 函數重寫setter方法
  7. 重寫的setter方法內部調用原始類的setter方法
  8. 重寫的setter方法內部找到監聽者進行回調
  9. 監聽者的保存方式利用runtime的關聯對象給分類添加一個數組屬性

詳細內容可參考iOS_KVO_Study 框架

直接修改爲員變量會觸發KVO嗎?

不會觸發,直接修改爲員變量並不會觸發setter方法,所以也就不會觸發KVO函數

探究KVO原理

KVO全稱 Key-Value Observing,鍵值監聽。 ui

基本用法

  • 註冊觀察者,實施監聽
[p1 addObserver:self
             forKeyPath:@"age"
             options:NSKeyValueObservingOptionNew
             context:nil];
複製代碼
  • 在回調方法裏處理屬性發生變化後的後續處理
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
  //...實現監聽處理
}
複製代碼
  • 移除觀察者
[self removeObserver:self forKeyPath:@「age"]; 複製代碼

在添加監聽後,age屬性的值在發生改變時就會通知監聽者,執行監聽者的observeValueForKeyPath方法。接下來咱們就一步步探究爲什麼會在age值發生改變後通知監聽者。spa

重寫set方法

賦值操做會調用set方法,咱們經過重寫Person類的setAge:方法,觀察是不是KVO在set方法內部作了一些操做來通知監聽者。指針

// ViewController類
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p1 = [[Person alloc] init];
    Person *p2 = [[Person alloc] init];
    p1.age = 1;
    p1.age = 2;
    p2.age = 2;
    // self 監聽 p1的 age屬性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    p1.age = 10;
    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監聽到%@的%@改變了%@", object, keyPath,change);
}

// Person類
- (void)setAge:(NSInteger)age {
    _age = age;
}
複製代碼

經過觀察發現p1和p2一樣調用了setAge:方法,p1除了調用setAge:方法外還會執行監聽者的observeValueForKeyPath方法。顯然這些並非在setAge:方法中調用的。

對比p1在addObserver先後的變化

既然不是經過修改setAge:方法來實現監聽的,那addObserver方法對p1對象作了什麼特殊處理呢?咱們經過打印isa指針來進行對比。

kvo-po-isa.png

經過上圖咱們發現,p1對象在執行過addObserver後,isa指針發生了改變,由以前的 Person變爲了 NSKVONotifying_Person。因此,系統生成的新類的格式是 NSKVONotifying_原類

思路驗證

  • 打印方法實現的地址來看p1和p2的setAge:方法實現的地址在添加KVO先後有什麼變化
NSLog(@"添加KVO監聽以前 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)], [p2 methodForSelector:@selector(setAge:)]);
    
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
[self printMethods:object_getClass(p2)];
[self printMethods:object_getClass(p1)];
    
NSLog(@"添加KVO監聽以後 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)], [p2 methodForSelector:@selector(setAge:)]);
複製代碼

kvo-p-address.png

經過打印的地址信息,咱們發如今添加KVO監聽以前,p1和p2的setAge:方法實現的地址相同,而通過KVO監聽以後,p1的setAge方法實現的地址發生了變化,p1的setAge:方法的實現轉換爲了C語言的Foundation框架的 _NSSetLongLongValueAndNotify 函數。

  • 查看 NSKVONotifying_Person 的內部結構
    咱們經過runtime分別打印Person類對象和NSKVONotifying_Person類對象內存儲的對象方法
- (void)printMethods:(Class)cls {
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@ - ", cls];
    
    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        
        [methodNames appendString:methodName];
        [methodNames appendString:@" "];
    }
    
    NSLog(@"%@", methodNames);
    free(methods);
}
複製代碼

打印內容以下:

kvo-print-methods.png

經過打印結果咱們發現NSKVONotifying_Person中有4個對象方法,分別爲 setAge:  class  dealloc  _isKVOA ,下圖是NSKVONotifying_Person的內存結構以及方法調用順序。

這裏NSKVONotifying_Person重寫class方法是爲了隱藏NSKVONotifying_Person。咱們在p1添加KVO後,打印p一、p2對象的class能夠發現他們都返回Person。

NSLog(@"%@, %@", [p1 class], [p2 class]);
// 打印結果: Person, Person
複製代碼

猜想NSKVONotifying_Person內重寫的class內部實現大體爲:

- (Class) class {
     // 獲得類對象,在找到類對象父類
     return class_getSuperclass(object_getClass(self));
}
複製代碼
  • 驗證didChangeValueForKey:內部會調用observer的observerValueForKeyPath:ofObject:change:context:方法
    在Person類中重寫willChangeValueForKey:和didChangeValueForKey:方法,模擬KVO的實現。
- (void)willChangeValueForKey:(NSString *)key {
    NSLog(@"willChangeValueForKey: - begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey: - end");
}

- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"didChangeValueForKey: - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey: - end");
}
複製代碼

打印結果:

kvo-changeforkey.png

經過打印的結果不難發現,確實在 didChangeValueForKey 方法內部調用了 observeValueForKeyPath:ofObject:change:context: 方法。
相關文章
相關標籤/搜索