iOS中KVO的底層實現原理

1. KVO的使用

KVO(Key-Value Observing),也就是咱們常說的鍵值監聽,能夠用於監聽某個對象屬性值的改變。KVO使用比較簡單,以下所示定義了一個含有2個屬性的Student類,而後聲明一個實例對象,並添加一個觀察者監聽某個屬性,當被監聽的屬性發生變化時就會調用觀察者的observeValueForKeyPath: ofObject: change: context:方法。當不須要監聽的時候須要移除觀察者。bash

// Student.h文件
@interface Student : NSObject
@property (nonatomic , strong) NSString *name;
@property (nonatomic , strong) NSMutableArray booksArr;
@end
複製代碼
// 使用Student類的文件
- (void)test{
    self.stu1 = [[Student alloc] init];
    // 添加觀察者監聽name的變化
    [self.stu1 addObserver:self
                forKeyPath:@"name"
                   options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                   context:NULL];

    NSLog(@"name改變前");
    self.stu1.name = @"Jack";
    NSLog(@"name改變後");
}

// 當監聽屬性發生變化時的回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"keyPath:%@,change-->%@",keyPath,change);
}

- (void)dealloc{
    // 移除觀察者
    [self.stu1 removeObserver:self forKeyPath:@"name"];
}

// ********************打印結果********************
2020-01-05 09:42:32.371008+0800 GCDDemo[13375:567451] name改變前
2020-01-05 09:42:32.371618+0800 GCDDemo[13375:567451] keyPath:name,change-->{
    kind = 1;
    new = Jack;
    old = "<null>";
}
2020-01-05 09:42:32.371895+0800 GCDDemo[13375:567451] name改變後
複製代碼

2. KVO底層實現原理

KVO的實現過程其實是利用了OC的runtime機制,當一個實例對象(好比上面的self.stu1)添加觀察者時,底層根據該實例對象所屬的類動態添加了一個類(動態添加的類名就是在原來類的類名前加上NSKVONotifying_前綴),這個類是繼承自原來的類的。上面實例的底層實現過程以下:框架

  • self.stu1添加觀察者時,底層就利用runtime動態生成一個叫NSKVONotifying_Student的類,這個類繼承自Student類,並重寫了如下實例方法:
    • 重寫class方法,不重寫的話調用這個方法返回的是NSKVONotifying_Student這個類,重寫後返回的是本來的Student類。蘋果這麼作的目的是爲了隱藏KVO的實現細節。
    • 重寫dealloc方法,在這個方法裏面作一些收尾的工做。
    • 重寫_isKVOA方法,這是一個私有方法,咱們沒必要關心。
    • 重寫被監聽屬性的setter方法,上面案例只監聽了name屬性,因此只需重寫setName:方法。重寫setter是實現KVO的關鍵,在setter方法裏面實際是調用的Foundation框架下的_NSSet***ValueAndNotify方法(***表示不是一個固定的,這個和監聽的屬性的類型有關,好比是屬性是int類型的話這裏就是__NSSetIntValueAndNotify,所包含的類型會在後面列出來)。
  • 而後將self.stu1這個實例對象的isa改成指向NSKVONotifying_Student(本來是指向Student類的)。
  • 當咱們設置被監聽屬性的值時self.stu1.name = @"Jack",是調用的setName:方法,前面說了setName:方法被重寫了,因此實際上調用的是_NSSetObjectValueAndNotify這個方法。這個方法實現蘋果是沒有開源的,沒法得知其具體實現,不過能夠猜出其實現流程大體以下:
    • 首先調用[self willChangeValueForKey:@"name"];這個方法。
    • 而後調用原先的setter方法的實現(好比_name = name;);
    • 再調用[self didChangeValueForKey:@"name"];這個方法。
    • 最後在didChangeValueForKey:這個方法中調用觀察者的observeValueForKeyPath: ofObject: change: context:方法來通知觀察者屬性值發生了變化。

Foundation框架下的_NSSet***ValueAndNotify系列方法列表以下:函數

_NSSetBoolValueAndNotify 
 _NSSetCharValueAndNotify 
 _NSSetDoubleValueAndNotify 
 _NSSetFloatValueAndNotify 
 _NSSetIntValueAndNotify 
 _NSSetLongLongValueAndNotify 
 _NSSetLongValueAndNotify 
 _NSSetObjectValueAndNotify 
 _NSSetPointValueAndNotify 
 _NSSetRangeValueAndNotify 
 _NSSetRectValueAndNotify 
 _NSSetShortValueAndNotify 
 _NSSetSizeValueAndNotify 
 _NSSetUnsignedCharValueAndNotify 
 _NSSetUnsignedIntValueAndNotify 
 _NSSetUnsignedLongLongValueAndNotify 
 _NSSetUnsignedLongValueAndNotify 
 _NSSetUnsignedShortValueAndNotify 
複製代碼

3. KVO底層實現的驗證

3.1 咱們怎麼知道添加觀察者時動態添加了一個類?

這個其實咱們只須要打印一下再添加觀察者以前和以後實例對象所屬的類就知道了。不過前面已經說過了,動態添加的類重寫了class方法,因此咱們不能經過這個方法來獲取一個實例對象的類,而要經過runtimeobject_getClass()這個API來獲取:ui

- (void)test1{
    self.stu1 = [[Student alloc] init];
    
    NSLog(@"觀察前- [self.stu1 class] -->%@",[self.stu1 class]);
    NSLog(@"觀察前- object_getClass(self.stu1) -->%@",object_getClass(self.stu1));

    [self.stu1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

    NSLog(@"觀察後- [self.stu1 class] -->%@",[self.stu1 class]);
    NSLog(@"觀察後- object_getClass(self.stu1) -->%@",object_getClass(self.stu1));
}

// ********************打印結果********************
2020-01-05 10:51:00.584299+0800 GCDDemo[14497:600230] 觀察前- [self.stu1 class] -->Student
2020-01-05 10:51:00.584690+0800 GCDDemo[14497:600230] 觀察前- object_getClass(self.stu1) -->Student
2020-01-05 10:51:00.592797+0800 GCDDemo[14497:600230] 觀察後- [self.stu1 class] -->Student
2020-01-05 10:51:00.593064+0800 GCDDemo[14497:600230] 觀察後- object_getClass(self.stu1) -->NSKVONotifying_Student
複製代碼

3.2 如何知道重寫了哪些方法?

這裏咱們須要用到runtime的一些API來獲取一個類對象裏面存儲的方法列表信息,下面咱們先封裝一個方法來獲取這些信息,而後把監聽前和監聽後的方法列表打印出來。atom

- (void)test2{
    self.stu1 = [[Student alloc] init];

    NSLog(@"觀察前方法列表-->%@",[self methodNamesOfClass:object_getClass(self.stu1)]);

    [self.stu1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

    NSLog(@"觀察後方法列表-->%@",[self methodNamesOfClass:object_getClass(self.stu1)]);
  
}

// 傳入一個類,將類中方法列表的方法名拼接換成字符串返回
- (NSString *)methodNamesOfClass:(Class)cls{
    unsigned int count;
    // 獲取方法列表
    Method *methodList = class_copyMethodList(cls, &count);
    NSString *methodNamesStr = @"";

    // 遍歷方法列表將方法名拼接成字符串
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        methodNamesStr = [methodNamesStr stringByAppendingFormat:@"%@ ,",methodName];
    }
    
    // 釋放
    free(methodList);
    
    return methodNamesStr;
}

// ********************打印結果********************
2020-01-05 10:56:43.077817+0800 GCDDemo[14606:603376] 觀察前方法列表-->.cxx_destruct ,name ,setName: ,age ,setAge: ,
2020-01-05 10:56:43.078483+0800 GCDDemo[14606:603376] 觀察後方法列表-->setName: ,class ,dealloc ,_isKVOA ,
複製代碼

3.3 怎麼知道重寫setter方法是調用的哪一個方法?

這裏咱們一樣須要用到runtime的API,首先經過class_getInstanceMethod()函數來獲取setter方法的Method,而後再調用method_getImplementation()來獲得setter方法的IMPspa

不過咱們首先打印的是IMP的地址,想要看IMP的具體信息咱們須要打一個斷點調出LLDB,而後藉助LLDB來打印具體信息。好比在監聽前的IMP地址是0x10967d4c0,就能夠在LLDB中輸入p (IMP)0x10967d4c0來打印具體信息。從下面能夠看出監聽前setter方法就是正常的,監聽後就變成了_NSSetObjectValueAndNotifycode

- (void)test1{
    self.stu1 = [[Student alloc] init];

    NSLog(@"監聽前的setter方法IMP-->%p",[self IMPWithSelector:@selector(setName:)]);

    [self.stu1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

    NSLog(@"監聽後的setter方法IMP-->%p",[self IMPWithSelector:@selector(setName:)]);
}

// 獲取一個方法的IMP
- (IMP)IMPWithSelector:(SEL)selector{
    Class cls = object_getClass(self.stu1);
    Method methon = class_getInstanceMethod(cls, selector);
    IMP imp = method_getImplementation(methon);
    return imp;
}

// ********************打印結果********************
2020-01-05 11:25:40.485792+0800 GCDDemo[15032:617260] 監聽前的setter方法IMP-->0x10967d4c0
2020-01-05 11:25:40.489656+0800 GCDDemo[15032:617260] 監聽後的setter方法IMP-->0x7fff25701c8a
(lldb) p (IMP)0x10967d4c0
(IMP) $0 = 0x000000010967d4c0 (GCDDemo`-[Student setName:] at Student.h:15)
(lldb) p (IMP)0x7fff25701c8a
(IMP) $1 = 0x00007fff25701c8a (Foundation`_NSSetObjectValueAndNotify)
複製代碼

4. KVO小結

KVO的核心是動態生成一個繼承自原類的類,而後將實例對象的isa指向這個類。而後重寫了監聽屬性的setter方法,在原有setter方法的前面調用willChangeValueForKey方法,在原有setter方法的後面調用didChangeValueForKeyorm

因此咱們要判斷某個操做是否會觸發KVO關鍵在於它是否調用了監聽屬性的setter方法。好比上面的例子,self.stu1.name = @"Jack";這種方式就是調用setter方法,因此它會觸發KVO。可是下面這幾種方式是不會觸發KVO的:server

  • 採用給成員變量賦值的方式,self.stu1->_name = @"Jack";(前提是須要將成員變量_name給暴露出去才能在外面訪問),這種方式是不會觸發KVO的,由於它沒有調用setter方法。
  • 對於集合類型,集合裏面數據的更新是不會觸發KVO的。好比[self.stu1.booksArr addObject:@"book1"]這樣的操做,它一樣沒有調用setBooksArr:方法,因此不會觸發KVO
  • 若是所監聽的屬性是一個自定義的OC對象,好比有個Dog類裏面有個age屬性,Student類裏面有個Dog類型的屬性dog,若是咱們監聽dog這個屬性,當dogage發生變化時並不會觸發KVO,由於它不會調用setDog:方法。

上面這幾種狀況,若是咱們也想觸發KVO的話,咱們能夠手動觸發,也就是在原有方法的前面和後面分別加上willChangeValueForKeydidChangeValueForKey這兩個方法。就好比最後這個例子,咱們能夠這樣寫:對象

[self.stu1 willChangeValueForKey:@"dog"];
self.stu1.dog.age = 3;
[self.stu1 didChangeValueForKey:@"dog"];
複製代碼

最後還有一點要說明,經過KVC方式設置屬性值也是會觸發KVO的。好比[self.stu1 setValue:@"Jack" forKey:@"name"];這樣寫是能夠觸發KVO的,這應該是蘋果在KVC實現中調用了willChangeValueForKeydidChangeValueForKey這兩個方法。

相關文章
相關標籤/搜索