重新梳理iOS底層知識:KVO

KVO概述

KVO的底層是如何實現的呢?

對於這個問題,我想大家都可以簡單的聊上這麼幾句。

對某個實例的某一個屬性添加KVO監聽後,系統會利用runtime的運行時特性,生成一個臨時的類NSKVONotifying_xxx,然後把該實例的isa指針指向NSKVONotifying_xxx,監聽哪個屬性,就重寫NSKVONotifying_xxx中此屬性的set方法,然後在重寫的set方法中實現監聽和通知。

簡單的來說就是這樣,但是這太籠統了,下面我們通過例子,一步一步的來分析。

探究

1. 爲什麼會想到可能是類發生了變化?
Person * person1 = [[Person alloc] init];
Person * person2 = [[Person alloc] init];

[person1 addObserver:self
          forKeyPath:@"age"
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
             context:nil];

person1.age = 10;
person2.age = 20;

這是一個最基本的KVO使用,在回調中只有person1的值改變被監聽了,但是我們在賦值的時候都是調用了age的set方法,如果我們在Person類中實現setAge:的方法並debug,這兩次賦值都會走setAge方法,問題不是出在setAge這裏,所以我們推測可能是類發聲了某些變化。(此處應該有runtime的知識基礎,runtime有能力對類做一些動態的改變)。

所以我們可以獲取一下這兩個實例的類型

// 輸出 person1:NSKVONotifying_Person
NSLog(@"person1:%@",object_getClass(person1));

// 輸出 person2:Person
NSLog(@"person2:%@",object_getClass(person2));

到這裏我們就可以確定確實是生成了一箇中間類。並且讓person1的isa指針指向了這個類(object_getClassName方法就是返回isa的指向)。

注:此處爲什麼要用runtime的api,因爲runtime的api調用後的結果更加接近本質

2. NSKVONotifying_Person類中做了什麼處理?

首先我們先看一下這面這個圖,其實這就是添加了KVO之後類的類型結構

關於NSKVONotifying_Person類實現的方法,我們是怎麼樣得到的呢,這裏我們可以藉助runtime的api窺探一下。

[self printMethodList:object_getClass(person1)];
// 下面是方法實現
- (void)printMethodList:(Class)cls {
    unsigned int count;
    Method * methodList = class_copyMethodList(cls, &count);
    for (unsigned int i = 0; i < count; i++) {
        Method method = methodList[i];
        NSLog(@"method(%d) : %@", i, NSStringFromSelector(method_getName(method)));
    }
    free(methodList);
}

輸出結果

method(0) : setAge:
method(1) : class
method(2) : dealloc
method(3) : _isKVOA

到這一步,我們可以先做一下小總結:

person2的isa指針指向Person類,所以在setAge的時候,就直接調用了Person中實現的setAge:方法,正常的賦值操作,沒有觸發KVO。但是person1的isa動態改變,指向了NSKVONotifying_Person,同時NSKVONotifying_Person中又重新實現了setAge:方法,所以在給person1的age賦值時,首先調用的是NSKVONotifying_Person中的setAge:方法,但是我們在之前的debug中發現,Person中的setAge:方法也會調用,其實這很容易理解,這應該是在NSKVONotifying_Person的setAge:實現中又調用了Person的setAge,畢竟NSKVONotifying_Person的isa指向Person(請自行驗證)。

3. NSKVONotifying_Person中的setAge:的實現

個人感覺,挖掘setAge:的實現是比較難的。

我們通過下面的方法打印一下方法IMP的地址

NSLog(@"person1添加KVO之前的兩個setAge地址: \n -person1:%p -- person2:%p",
      [person1 methodForSelector:@selector(setAge:)],
      [person2 methodForSelector:@selector(setAge:)]);

[person1 addObserver:self
          forKeyPath:@"age"
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
             context:nil];

NSLog(@"person1添加KVO之後的兩個setAge地址: \n -person1:%p -- person2:%p",
    [person1 methodForSelector:@selector(setAge:)],
    [person2 methodForSelector:@selector(setAge:)]);

輸出:

person1添加KVO之前的兩個setAge地址: 
-person1:0x1005ee850 -- person2:0x1005ee850

person1添加KVO之後的兩個setAge地址: 
-person1:0x7fff25623f0e -- person2:0x1005ee850

我們可以看到,在添加KVO監聽前後,person2的setAge實現的地址沒有發生變化,但是person1的變了,我們在打印臺用lldb命令打印一下0x7fff25623f0e

(lldb) p (IMP)0x7fff25623f0e
(IMP) $1 = 0x00007fff25623f0e (Foundation`_NSSetIntValueAndNotify)

可以看到setAge的實現其實就是調用了Foundation框架的_NSSetIntValueAndNotify方法。那具體的_NSSetIntValueAndNotify內部實現是怎麼樣的呢?因爲Foundation不開源,我們只能猜測,並對我們的猜測做出相應的驗證。

下面是我看過一些大神的分析之後猜測的_NSSetIntValueAndNotify實現的僞代碼(特此鳴謝我們的MJ老師)

void _NSSetIntValueAndNotify() {
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)keyPath {
    // 通知監聽者,已經修改完畢
    [observer observeValueForKeyPath:keyPath ofObject:self change:nil context:nil];
} 

如何驗證一下我們的猜測呢?

我們知道NSKVONotifying_Person類中沒有實現willChangeValueForKey和didChangeValueForKey這兩個方法,所以我們可以在NSKVONotifying_Person的父類,也就是Person類型重寫這兩個方法,改造完之後的Person類裏面應該是下面這樣子:

@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
- (void)setAge:(int)age {
    _age = age;
    NSLog(@"age:%d",age);
}

- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}

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

person1在添加了KVO監聽,並設置值person1.age = 10;之後,輸出如下:

2020-09-07 23:47:13.787066+0800 KVO[8122:119707] willChangeValueForKey
2020-09-07 23:47:13.787229+0800 KVO[8122:119707] age:10
2020-09-07 23:47:13.787334+0800 KVO[8122:119707] didChangeValueForKey - begin
2020-09-07 23:47:13.787592+0800 KVO[8122:119707] <Person: 0x60000288c190> -- age -- {
    kind = 1;
    new = 10;
    old = 0;
}
2020-09-07 23:47:13.787732+0800 KVO[8122:119707] didChangeValueForKey - end

輸出結果與我們的猜測一致。

其他小知識點

NSKVONotifying_Person也重寫了class方法,使用[person1 class]的時候返回的是Person,其實也很容易理解,只是爲了隱藏NSKVONotifying_Person這個類,儘量隱藏KVO的內部實現。

大家也可以看一下我下面附上的參考文章,寫的很不錯。

結束語

經過上面的層層分析,我們探究了KVO的實現原理,有不縝密的地方還請指點。
感謝閱讀。

推薦文集

收錄原文地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章