一、基礎 KVO 的日常使用
一般情況,分如下三個步驟:
// 1. 進行監聽
[self.person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
// 2. 添加監聽回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"是監聽到了%@實例對象的%@值,改變了%@,context:%@", object, keyPath, change, context);
}
// 3. 銷毀監聽
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"height"];
}
二、兩道經典面試題
1. iOS 用什么方式實現對一個對象的 KVO?(KVO 的本質是什么?)
- 利用 RuntimeAPI 動態生成一個子類,并且讓 instance 對象的 isa 指針指向這個全新子類
- 這個全新子類會重寫 KVO 屬性的
set
方法,偽代碼如下: - willChangeValueForKey:
- 父類 setter 方法
- didChangeValueForKey:
2. 如何手動觸發 KVO?
- willChangeValueForKey:
- didChangeValueForKey:
三、探求 KVO 的本質,并從代碼層面上求證
1. 繪圖說明 KVO 的本質
KVO 原理圖
- KVO 對象生成了一個 繼承自
SPPerson
類對象的NSKVONotifying_SPPerson
類對象 - 并將實例對象的 isa 指向
NSKVONotifying_SPPerson
類對象 - 并且有四個方法,
setHeight:
、class
、dealloc
、_isKVOA
- 用圖中的偽代碼,重寫了
setHeight:
方法
2. 代碼觀察被 KOV 實例對象的 isa 指針
- 編寫如下代碼,并打上斷點
- (void)viewDidLoad {
[super viewDidLoad];
SPPerson *person1 = [[SPPerson alloc] init];
person1.height = 1;
SPPerson *person2 = [[SPPerson alloc] init];
person2.height = 2;
NSLog(@"請在此處打斷點1");
[person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
NSLog(@"請在此處打斷點2");
}
- 我們可以獲得如下調試信息
(lldb) p person1->isa
(Class) $0 = SPPerson
(lldb) p person2->isa
(Class) $1 = SPPerson
2020-08-25 15:42:17.983376+0800 Demo-KVO[9298:415101] 請在此處打斷點1
(lldb) p person1->isa
(Class) $2 = NSKVONotifying_SPPerson
(lldb) p person2->isa
(Class) $3 = SPPerson
- 我們發現在 KOV 之后,person1 的 isa 指針,指向了一個
NSKVONotifying_SPPerson
的類對象
3. 代碼觀察NSKVONotifying_SPPerson
類對象方法列表
- (void)viewDidLoad {
[super viewDidLoad];
SPPerson *person1 = [[SPPerson alloc] init];
person1.height = 1;
NSLog(@"KVO 之前 %@ 的方法列表:", object_getClass(person1));
[self printClassMethods:object_getClass(person1)];
[person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
NSLog(@"KVO 之后 %@ 的方法列表:", object_getClass(person1));
[self printClassMethods:object_getClass(person1)];
}
- (void)printClassMethods:(Class) cls {
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [[NSMutableString alloc] init];
for (int i = 0 ; i < count; i++) {
NSString *name = NSStringFromSelector(method_getName(methodList[i]));
[methodNames appendString:name];
[methodNames appendString:@" , "];
}
NSLog(@"%@", methodNames);
}
- 代碼輸出信息如下:
Demo-KVO[10337:501248] KVO 之前 SPPerson 的方法列表:
Demo-KVO[10337:501248] height , setHeight: , weight , setWeight: ,
Demo-KVO[10337:501248] KVO 之后 NSKVONotifying_SPPerson 的方法列表:
Demo-KVO[10337:501248] setHeight: , class , dealloc , _isKVOA ,
- 由此可以看出
NSKVONotifying_SPPerson
有setHeight: , class , dealloc , _isKVOA
這四個方法 - 思考為什么沒有
height
方法?因為它的父類有,所以它不需要多此一舉
4. 代碼觀察被 KOV 實例對象的 set 方法
- (void)viewDidLoad {
[super viewDidLoad];
SPPerson *person1 = [[SPPerson alloc] init];
person1.height = 1;
NSLog(@"%p",[person1 methodForSelector:@selector(setHeight:)]);
[person1 addObserver:self forKeyPath:@"height" options:3 context:@"123"];
NSLog(@"%p",[person1 methodForSelector:@selector(setHeight:)]);
}
- 得到如下信息
Demo-KVO[9473:425751] 0x1010eaf60
Demo-KVO[9473:425751] 0x7fff258f10eb
(lldb) p (IMP)0x1010eaf60
(IMP) $1 = 0x00000001010eaf60 (Demo-KVO`-[SPPerson setHeight:] at SPPerson.h:15)
(lldb) p (IMP)0x7fff258f10eb
(IMP) $2 = 0x00007fff258f10eb (Foundation`_NSSetIntValueAndNotify)
- 說明 KVO 之后
setHeight:
方法被替換了成了_NSSetIntValueAndNotify
方法
5. _NSSetIntValueAndNotify
是一個 C 語言方法,因為 Foundation
框架并不開源,所以我們只能從側面猜測它的內部實現。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person1 willChangeValueForKey:@"height"];
[self.person1 didChangeValueForKey:@"height"];
}
- 打印如下
2020-08-25 16:48:15.816554+0800 Demo-KVO[10176:485066] 是監聽到了<SPPerson: 0x600003eb0230>實例對象的height值,改變了{
kind = 1;
new = 1;
old = 1;
},context:123
- 我們實現上述代碼,我們發現可以不直接改變 person1 的height 的數值,也可以觸發
- (void)observeValueForKeyPath:ofObject:change:context:
- 所以我們可以猜測,KVO 實現
setHeight:
方法的偽代碼如下:
- (void)setHeight:(int)height {
[self willChangeValueForKey:@"height"];
[super setHeight:value];
[self didChangeValueForKey:@"height"];
}
KVO補充
1、如果在B頁面使用蘋果原生的KVO寫法監聽了一個全局對象的值,此時退出B頁面(B頁面銷毀,且未移除KVO監聽),回到A頁面觸發全局對象值修改,程序會怎么樣?
- 會崩潰
- 報錯:
_NSSetIntValueAndNotify
image.png
2、如何解決上述問題?就是監聽者被銷毀了,但是KVO還沒移除監聽?
- 使用YYKit的擴展KVO監聽
- 關鍵原理:用關聯技術,創建一個關聯屬性,將關聯屬性當做監聽者;從而實現實例銷毀,監聽者也自動銷毀,然后KVO監聽也自動銷毀。