iOS底層系列:KVO

前言

最近打算從新梳理一遍iOS底層的知識,儘可能把全部的底層知識點都搞懂搞透徹,礙於iOS不開源,有不少東西並不能很直觀的去學習,因此可能有瑕疵,但願你們能夠理解,並一塊兒交流,筆者也儘量作到盡善盡美吧。html

KVO概述

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

對於這個問題,我想你們均可以簡單的聊上這麼幾句。框架

對某個實例的某一個屬性添加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有能力對類作一些動態的改變)。this

因此咱們能夠獲取一下這兩個實例的類型atom

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

到這裏咱們就能夠肯定確實是生成了一箇中間類。而且讓person1的isa指針指向了這個類(object_getClassName方法就是返回isa的指向)。spa

注:此處爲何要用runtime的api,由於runtime的api調用後的結果更加接近本質debug

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

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

image

關於NSKVONotifying_Person類實現的方法,咱們是怎麼樣獲得的呢,這裏咱們能夠藉助runtime的api窺探一下。code

[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的實現原理,有不縝密的地方還請指點。
感謝閱讀。

參考

mikeash.com: just this guy, you know?
本身實現 KVO

相關文章
相關標籤/搜索