iOS KVO原理

1. KVO是什么

kvo全稱Key-Value Observing,鍵值監(jiān)聽。
是對(duì)觀察者模式的一種實(shí)現(xiàn)。對(duì)一個(gè)對(duì)象添加Observer后,如果這個(gè)對(duì)象發(fā)生了改變,我們就會(huì)收到對(duì)象改變的通知。

2. 使用方法

先創(chuàng)建一個(gè)Objc類,作為要監(jiān)聽的對(duì)象。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Objc : NSObject

@property (nonatomic, strong) NSString *test;

@end

NS_ASSUME_NONNULL_END

監(jiān)聽的實(shí)現(xiàn)

#import "ViewController.h"
#import "Objc.h"

@interface ViewController ()

@property (nonatomic, strong) Objc *objc;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
    [self.objc addObserver:self forKeyPath:@"test" options:options context:@"context"];
    self.objc.test = @"123";
}

- (Objc *)objc {
    if (!_objc) {
        _objc = [Objc new];
    }
    return _objc;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"keyPath:%@",keyPath);
    NSLog(@"object:%@",object);
    NSLog(@"change:%@",change);
    NSLog(@"context:%@",context);
}

- (void)dealloc {
    [self.objc removeObserver:self forKeyPath:@"test"];
}
@end

打印結(jié)果

2020-08-11 23:04:27.199075+0800 KVODemo[2091:33456] keyPath:test
2020-08-11 23:04:27.199151+0800 KVODemo[2091:33456] object:<Objc: 0x600003258440>
2020-08-11 23:04:27.199267+0800 KVODemo[2091:33456] change:{
    kind = 1;
    new = "<null>";
}
2020-08-11 23:04:27.199332+0800 KVODemo[2091:33456] context:context
2020-08-11 23:04:27.199542+0800 KVODemo[2091:33456] keyPath:test
2020-08-11 23:04:27.199640+0800 KVODemo[2091:33456] object:<Objc: 0x600003258440>
2020-08-11 23:04:27.199754+0800 KVODemo[2091:33456] change:{
    kind = 1;
    notificationIsPrior = 1;
    old = "<null>";
}
2020-08-11 23:04:27.199824+0800 KVODemo[2091:33456] context:context
2020-08-11 23:04:27.199883+0800 KVODemo[2091:33456] keyPath:test
2020-08-11 23:04:27.199951+0800 KVODemo[2091:33456] object:<Objc: 0x600003258440>
2020-08-11 23:04:27.200016+0800 KVODemo[2091:33456] change:{
    kind = 1;
    new = 123;
    old = "<null>";
}
2020-08-11 23:04:27.200076+0800 KVODemo[2091:33456] context:context

3. KVO原理

1. NSKVONotifying_

我們?cè)趏bjc對(duì)象添加監(jiān)聽之前分別打印objc的對(duì)象類型

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"監(jiān)聽前objc類型object_getClass:%@",object_getClass(self.objc));
    NSLog(@"監(jiān)聽前objc類型class:%@",self.objc.class);
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
    [self.objc addObserver:self forKeyPath:@"test" options:options context:@"context"];
    NSLog(@"監(jiān)聽后objc類型object_getClass:%@",object_getClass(self.objc));
    NSLog(@"監(jiān)聽后objc類型class:%@",self.objc.class);
}

打印結(jié)果

2020-08-12 22:45:15.591256+0800 KVODemo[2492:43228] 監(jiān)聽前objc類型object_getClass:Objc
2020-08-12 22:45:15.591338+0800 KVODemo[2492:43228] 監(jiān)聽前objc類型class:Objc
2020-08-12 22:45:15.591515+0800 KVODemo[2492:43228] 監(jiān)聽后objc類型object_getClass:NSKVONotifying_Objc
2020-08-12 22:45:15.591588+0800 KVODemo[2492:43228] 監(jiān)聽后objc類型class:Objc

我們看到,objc添加監(jiān)聽后,使用object_getClass方法獲取objc類型時(shí)獲取到的是NSKVONotifying_Objc。
這里就產(chǎn)生了幾個(gè)問題:

  1. 為什么添加監(jiān)聽后使用object_getClass獲取到的對(duì)象類型是NSKVONotifying_Objc?
    我們獲取添加監(jiān)聽后的objc對(duì)象的類對(duì)象的父類

    - (void)viewDidLoad {
        [super viewDidLoad];
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
        [self.objc addObserver:self forKeyPath:@"test" options:options context:@"context"];
        Class class = object_getClass(self.objc);
        Class superClass = class_getSuperclass(class);
        NSLog(@"添加監(jiān)聽后objc的類對(duì)象:%@",class);
        NSLog(@"%@的父類對(duì)象%@",class,superClass);
    }
    

    打印結(jié)果

    2020-08-12 23:41:51.984192+0800 KVODemo[16522:112127] 添加監(jiān)聽后objc的類對(duì)象:NSKVONotifying_Objc
    2020-08-12 23:41:51.984262+0800 KVODemo[16522:112127] NSKVONotifying_Objc的父類對(duì)象Objc
    

    從打印結(jié)果可以看出,NSKVONotifying_Objc是Objc的子類,說明我們添加了監(jiān)聽之后動(dòng)態(tài)創(chuàng)建了一個(gè)Objc的子類NSKVONotifying_Objc,并將對(duì)象objc的類型更改為了NSKVONotifying_Objc。

  2. 為什么添加監(jiān)聽收使用class方法和object_getClass方法獲取到的類型不一樣?
    我們查看class和object_getClass的源碼

    此源碼在runtim源碼的Object.mm中
    -(id)class {
      return (id)isa; 
    }
    
    + (id)class {
      return self;
    }
    
    此源碼在runtim源碼的objc-class.mm中
    Class object_getClass(id obj) {
      if (obj) return obj->getIsa();
      else return Nil;
    }
    

    我們從源碼看出,實(shí)例對(duì)象調(diào)用class方法會(huì)返回isa指針,類對(duì)象調(diào)用class方法會(huì)返回自己,通過object_getClass方法獲取對(duì)象的類型也會(huì)返回isa指針。從源碼上看objc對(duì)象添加監(jiān)聽之后使用class和使用object_getClass方法獲取到的類型應(yīng)該是一樣的,但是這里卻不同,我們猜測(cè)在添加了監(jiān)聽之后在NSKVONotifying_Objc中重寫了class方法。
    我們打印一下添加監(jiān)聽前后class方法的IMP地址來確認(rèn)是否重寫了class方法

    - (void)viewDidLoad {
        [super viewDidLoad];
        Class class1 = object_getClass(self.objc);
        NSLog(@"監(jiān)聽前objc類型object_getClass:%@",object_getClass(class1));
        NSLog(@"監(jiān)聽前objc的class實(shí)現(xiàn)地址:%p",method_getImplementation(class_getInstanceMethod(class1, @selector(class))));
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
        [self.objc addObserver:self forKeyPath:@"test" options:options context:@"context"];
        Class class2 = object_getClass(self.objc);
        NSLog(@"監(jiān)聽前objc類型object_getClass:%@",object_getClass(class2));
        NSLog(@"監(jiān)聽前objc的class實(shí)現(xiàn)地址:%p",method_getImplementation(class_getInstanceMethod(class2, @selector(class))));
    }
    

    打印結(jié)果

    2020-08-13 22:47:08.351079+0800 KVODemo[2177:36963] 監(jiān)聽前objc類型object_getClass:Objc
    2020-08-13 22:47:08.351165+0800 KVODemo[2177:36963] 監(jiān)聽前objc的class實(shí)現(xiàn)地址:0x7fff51410632
    2020-08-13 22:47:08.351366+0800 KVODemo[2177:36963] 監(jiān)聽前objc類型object_getClass:NSKVONotifying_Objc
    2020-08-13 22:47:08.351438+0800 KVODemo[2177:36963] 監(jiān)聽前objc的class實(shí)現(xiàn)地址:0x7fff2572073d
    

    從打印結(jié)果可以看出,添加監(jiān)聽之后class方法的地址改變了,這驗(yàn)證了我們之前的猜想,NSKVONotifying_Objc類中重寫了class方法。

我們監(jiān)聽對(duì)象時(shí)調(diào)用了set方法,我們對(duì)監(jiān)聽前后的set方法單獨(dú)分析。
我們?cè)偬砑颖O(jiān)聽前后分別打印setTest方法的IMP地址

- (void)viewDidLoad {
    [super viewDidLoad];
    Class class1 = object_getClass(self.objc);
    NSLog(@"監(jiān)聽前objc類型object_getClass:%@",object_getClass(class1));
    IMP imp1 = method_getImplementation(class_getInstanceMethod(class1, @selector(setTest:)));
    NSLog(@"監(jiān)聽前objc的class實(shí)現(xiàn)地址:%p",imp1);
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
    [self.objc addObserver:self forKeyPath:@"test" options:options context:@"context"];
    Class class2 = object_getClass(self.objc);
    NSLog(@"監(jiān)聽后objc類型object_getClass:%@",object_getClass(class2));
    IMP imp2 = method_getImplementation(class_getInstanceMethod(class2, @selector(setTest:)));
    NSLog(@"監(jiān)聽后objc的class實(shí)現(xiàn)地址:%p",imp2);
    NSLog(@"");
}

打印結(jié)果

2020-08-13 23:47:21.212568+0800 KVODemo[4074:75514] 監(jiān)聽前objc類型object_getClass:Objc
2020-08-13 23:47:21.212654+0800 KVODemo[4074:75514] 監(jiān)聽前objc的class實(shí)現(xiàn)地址:0x10cd2f920
2020-08-13 23:47:21.212833+0800 KVODemo[4074:75514] 監(jiān)聽前objc類型object_getClass:NSKVONotifying_Objc
2020-08-13 23:47:21.212917+0800 KVODemo[4074:75514] 監(jiān)聽前objc的class實(shí)現(xiàn)地址:0x7fff25721c7a

通過打印結(jié)果可以看出setTest方法也在NSKVONotifying_Objc中被重寫了,我們?cè)偈褂胠ldb來看下setTest具體是什么

(lldb) print (IMP)0x10cd2f920
(IMP) $0 = 0x000000010cd2f920 (KVODemo`-[Objc setTest:] at Objc.h:15)
(lldb) print (IMP)0x7fff25721c7a
(IMP) $1 = 0x00007fff25721c7a (Foundation`_NSSetObjectValueAndNotify)

第一個(gè)地址打印的是添加監(jiān)聽前setTest方法的IMP地址,第二個(gè)打印的是添加監(jiān)聽后setTest方法的IMP地址。
這里看出添加監(jiān)聽前setTest對(duì)應(yīng)的具體方法就是setTest,但是添加監(jiān)聽后,setTest對(duì)應(yīng)的雞頭方法卻變成了_NSSetObjectValueAndNotify函數(shù)。
下面我們就來研究一下_NSSetObjectValueAndNotify函數(shù)

2. _NSSetObjectValueAndNotify

我們來研究一下_NSSetObjectValueAndNotify是什么
我們使用下面的命令來獲取Foundation的私有函數(shù)有哪些。
使用這個(gè)函數(shù)要注意,有的終端有最大顯示行數(shù)的限制,這個(gè)命令的打印結(jié)果有三萬多行,所以要關(guān)閉最大行數(shù)限制,否則我們需要找的函數(shù)會(huì)顯示不出來。

nm -a /System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation

打印結(jié)果,這里只選擇了與KVO相關(guān)的函數(shù)

__NSSetBoolValueAndNotify
__NSSetCharValueAndNotify
__NSSetDoubleValueAndNotify
__NSSetFloatValueAndNotify
__NSSetIntValueAndNotify
__NSSetLongLongValueAndNotify
__NSSetLongValueAndNotify
__NSSetObjectValueAndNotify
__NSSetPointValueAndNotify
__NSSetRangeValueAndNotify
__NSSetRectValueAndNotify
__NSSetShortValueAndNotify
__NSSetSizeValueAndNotify
__NSSetUnsignedCharValueAndNotify
__NSSetUnsignedIntValueAndNotify
__NSSetUnsignedLongLongValueAndNotify
__NSSetUnsignedLongValueAndNotify
__NSSetUnsignedShortValueAndNotify

從上面與KVO相關(guān)的方法中我們可以看出,每一種數(shù)據(jù)類型都對(duì)應(yīng)了一個(gè)setXXXValueAndNotify函數(shù)。
不過這些函數(shù)的具體實(shí)現(xiàn)沒有公布,所以內(nèi)部構(gòu)造這里還是不清楚。
但是我們知道,在調(diào)用setXXXValueAndNotify函數(shù)的過程中會(huì)調(diào)用另外兩個(gè)方法。

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

測(cè)試后得出了以下幾個(gè)結(jié)論:

  1. 如果在創(chuàng)建監(jiān)聽的時(shí)候只使用了NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld這兩個(gè)枚舉值,那么observeValueForKeyPath方法會(huì)在didChangeValueForKey方法調(diào)用后被調(diào)用。
  2. 如果在創(chuàng)建監(jiān)聽的時(shí)候使用了NSKeyValueObservingOptionPrior枚舉值,那么observeValueForKeyPath方法會(huì)在willChangeValueForKey方法調(diào)用后被調(diào)用第一次,在didChangeValueForKey方法調(diào)用后被調(diào)用第二次。
  3. 如果在創(chuàng)建監(jiān)聽的時(shí)候使用了NSKeyValueObservingOptionInitial枚舉值,那么在observeValueForKeyPath方法會(huì)在willChangeValueForKey方法調(diào)用之前被調(diào)用一次。

我們還可以利用這兩個(gè)方法手動(dòng)觸發(fā)observeValueForKeyPath方法

  1. 當(dāng)使用了NSKeyValueObservingOptionInitial枚舉值時(shí),創(chuàng)建監(jiān)聽時(shí)就會(huì)調(diào)用一次observeValueForKeyPath方法,不需要其他條件觸發(fā)。
  2. 當(dāng)使用NSKeyValueObservingOptionPrior枚舉值時(shí),手動(dòng)調(diào)用willChangeValueForKey時(shí)可以觸發(fā)一次observeValueForKeyPath方法的調(diào)用。
  3. 如果想在didChangeValueForKey方法調(diào)用后再調(diào)用一次observeValueForKeyPath方法,需要同時(shí)實(shí)現(xiàn)willChangeValueForKey和didChangeValueForKey兩個(gè)方法才行。

所以我們判斷在_NSSetObjectValueAndNotify函數(shù)內(nèi)部,在調(diào)用原來的set方法之前插入了willChangeValueForKey方法,在調(diào)用原來的set方法之后插入了didChangeValueForKey方法,并根據(jù)初始化時(shí)的枚舉值決定調(diào)用observeValueForKeyPath的時(shí)機(jī)。

4. 總結(jié)

  1. 添加監(jiān)聽時(shí),會(huì)動(dòng)態(tài)創(chuàng)建一個(gè)監(jiān)聽對(duì)象類型的子類,并將監(jiān)聽對(duì)象的isa指針指向新的子類。
  2. 子類中重寫了class和監(jiān)聽屬性的set方法。
  3. 重寫class方法是為了不將動(dòng)態(tài)創(chuàng)建的類型暴露出來。
  4. 重寫set方法是將set方法的具體實(shí)現(xiàn)替換成了與屬性類型相關(guān)的__NSSetXXXValueAndNotify函數(shù)。
  5. 在__NSSetXXXValueAndNotify函數(shù)內(nèi)部在set方法前后分別插入了willChangeValueForKey和didChangeValueForKey這兩個(gè)方法。
  6. 根據(jù)添加監(jiān)聽時(shí)的枚舉值決定調(diào)用observeValueForKeyPath的具體時(shí)機(jī)。
公眾號(hào)

歡迎關(guān)注公眾號(hào),留言討論

參考文獻(xiàn):
KVO源碼淺析
iOS底層原理探索—KVO的本質(zhì)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容