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è)問題:
-
為什么添加監(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。
-
為什么添加監(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é)論:
- 如果在創(chuàng)建監(jiān)聽的時(shí)候只使用了NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld這兩個(gè)枚舉值,那么observeValueForKeyPath方法會(huì)在didChangeValueForKey方法調(diào)用后被調(diào)用。
- 如果在創(chuàng)建監(jiān)聽的時(shí)候使用了NSKeyValueObservingOptionPrior枚舉值,那么observeValueForKeyPath方法會(huì)在willChangeValueForKey方法調(diào)用后被調(diào)用第一次,在didChangeValueForKey方法調(diào)用后被調(diào)用第二次。
- 如果在創(chuàng)建監(jiān)聽的時(shí)候使用了NSKeyValueObservingOptionInitial枚舉值,那么在observeValueForKeyPath方法會(huì)在willChangeValueForKey方法調(diào)用之前被調(diào)用一次。
我們還可以利用這兩個(gè)方法手動(dòng)觸發(fā)observeValueForKeyPath方法
- 當(dāng)使用了NSKeyValueObservingOptionInitial枚舉值時(shí),創(chuàng)建監(jiān)聽時(shí)就會(huì)調(diào)用一次observeValueForKeyPath方法,不需要其他條件觸發(fā)。
- 當(dāng)使用NSKeyValueObservingOptionPrior枚舉值時(shí),手動(dòng)調(diào)用willChangeValueForKey時(shí)可以觸發(fā)一次observeValueForKeyPath方法的調(diào)用。
- 如果想在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é)
- 添加監(jiān)聽時(shí),會(huì)動(dòng)態(tài)創(chuàng)建一個(gè)監(jiān)聽對(duì)象類型的子類,并將監(jiān)聽對(duì)象的isa指針指向新的子類。
- 子類中重寫了class和監(jiān)聽屬性的set方法。
- 重寫class方法是為了不將動(dòng)態(tài)創(chuàng)建的類型暴露出來。
- 重寫set方法是將set方法的具體實(shí)現(xiàn)替換成了與屬性類型相關(guān)的__NSSetXXXValueAndNotify函數(shù)。
- 在__NSSetXXXValueAndNotify函數(shù)內(nèi)部在set方法前后分別插入了willChangeValueForKey和didChangeValueForKey這兩個(gè)方法。
- 根據(jù)添加監(jiān)聽時(shí)的枚舉值決定調(diào)用observeValueForKeyPath的具體時(shí)機(jī)。

歡迎關(guān)注公眾號(hào),留言討論
參考文獻(xiàn):
KVO源碼淺析
iOS底層原理探索—KVO的本質(zhì)