本系列是學(xué)習(xí)iOS底層原理過程中的記錄筆記第二篇,第一篇在這里:
探索iOS底層原理開篇——對(duì)象本質(zhì)
首先拋出三個(gè)面試題:
- iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO膊夹?(KVO的本質(zhì)是什么坏怪?)
- 如何手動(dòng)觸發(fā)KVO?
- 直接修改成員變量會(huì)觸發(fā)KVO么?
KVO的全稱是Key-Value Observing铛楣,俗稱“鍵值監(jiān)聽”,可以用于監(jiān)聽某個(gè)對(duì)象屬性值的改變
使用方法:
新建Person類艺普,一個(gè)age成員變量簸州,給Person的age添加KVO監(jiān)聽
當(dāng)前控制器類添加name屬性,給self的name添加KVO監(jiān)聽
self.person = [[Person alloc] init];
self.person.age = 1;
// 給person1對(duì)象添加KVO監(jiān)聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self addObserver:self forKeyPath:@"name" options:options context:@"456"];
//修改age值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person.age = 21;
[self.person setAge:22];
self.name = @"05241";
_name = @"890";
}
// 當(dāng)監(jiān)聽對(duì)象的屬性值發(fā)生改變時(shí)歧譬,就會(huì)調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
打印結(jié)果如下:可以看出只要是通過set方法修改成員變量都會(huì)觸發(fā)KVO監(jiān)聽岸浑,直接下劃線訪問成員變量不會(huì)觸發(fā)KVO。
我們的都是不管是self.person.age還是[self.person setAge]本質(zhì)都是調(diào)用Person類的setAge方法瑰步,在上一篇我們一直到調(diào)用對(duì)象方法的本質(zhì)是通過isa找到類對(duì)象的方法列表矢洲,因此我們?cè)囍鴮?duì)比兩個(gè)Person的類對(duì)象有什么不同,一個(gè)有添加KVO監(jiān)聽缩焦,一個(gè)沒有读虏,因?yàn)檎G闆r下,Person的類對(duì)象永遠(yuǎn)都是同一個(gè)袁滥。詳細(xì)可參考我上一篇:探索iOS底層原理——對(duì)象本質(zhì)
我們打印兩個(gè)person的isa發(fā)現(xiàn)person的類對(duì)象確實(shí)不同:
添加KVO的person1的類對(duì)象是NSKVONotifying_Person盖桥,我們繼續(xù)分析,前面說了更改屬性值實(shí)際上是調(diào)用了setAge方法题翻,我們打印一下person在添加KVO監(jiān)聽前后的setAge方法的地址揩徊,看一下他底層到底是調(diào)用了什么方法,因?yàn)榍懊娣治隽颂砑覭VO后,Person類的isa實(shí)際上是指向了NSKVONotifying_Person類靴拱,這是Runtime在運(yùn)行時(shí)動(dòng)態(tài)添加的類垃喊,因?yàn)槲覀兛梢圆孪雽?shí)際上是調(diào)用了NSKVONotifying_Person的setAge方法,在這個(gè)方法里面再調(diào)用了KVO的方法通知KVO觸發(fā)的方法observeValueForKeyPath: ofObject: change: context:
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
NSLog(@"person1添加KVO監(jiān)聽之前 - person1:%p person2:%p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
// 給person1對(duì)象添加KVO監(jiān)聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"person1添加KVO監(jiān)聽之后 - person1: %p person2:%p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
}
打印結(jié)果如下:
結(jié)果符合我們的猜想袜炕,Person在添加KVO之后本谜,類對(duì)象NSKVONotifying_Person調(diào)用setAge方法實(shí)際上是調(diào)用了Foundation框架的_NSSetIntValueAndNotify方法,由于Apple上的Foundation不開源偎窘,我們無法直接查看_NSSetIntValueAndNotify方法的具體實(shí)現(xiàn)乌助,不過可以通過越獄手機(jī)查找系統(tǒng)目錄的Foundation框架可執(zhí)行文件,再通過hopper反編譯工具查看Foundation的匯編實(shí)現(xiàn)陌知,這個(gè)不在本文討論之內(nèi)他托,在之后總結(jié)逆向知識(shí)的時(shí)候再分析。這里直接給出答案(偽代碼實(shí)現(xiàn)):
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
_NSSetIntValueAndNotify方法實(shí)際上會(huì)先調(diào)用willChangeValueForKey方法仆葡,然后再調(diào)回父類Person的setAge方法赏参,再調(diào)用didChangeValueForKey,這個(gè)方法內(nèi)部實(shí)現(xiàn)最終會(huì)調(diào)用observeValueForKeyPath: ofObject: change: context:觸發(fā)監(jiān)聽方法沿盅,最終我們就可以在這個(gè)方法收到KVO更新的通知了:
// 當(dāng)監(jiān)聽對(duì)象的屬性值發(fā)生改變時(shí)把篓,就會(huì)調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
既然添加KVO之后調(diào)用set方法實(shí)際上是調(diào)用** willChangeValueForKey和didChangeValueForKey**方法,我們?cè)囍煌ㄟ^setAge觸發(fā)KVO腰涧,試著調(diào)用這兩個(gè)方法看能不能成功觸發(fā)KVO:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self.person1 setAge:21];
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
}
當(dāng)點(diǎn)擊后韧掩,發(fā)現(xiàn)確實(shí)能成功收到觸發(fā)KVO!
至此窖铡,我們分析完畢疗锐!
總結(jié):
一.iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO?(KVO的本質(zhì)是什么费彼?)
- 利用RuntimeAPI動(dòng)態(tài)生成一個(gè)子類滑臊,并且讓instance對(duì)象的isa指向這個(gè)全新的子類
- 當(dāng)修改instance對(duì)象的屬性時(shí),會(huì)調(diào)用Foundation的_NSSetXXXValueAndNotify函數(shù)
- willChangeValueForKey:
父類原來的setter - didChangeValueForKey:
- 內(nèi)部會(huì)觸發(fā)監(jiān)聽器(Oberser)的監(jiān)聽方法( observeValueForKeyPath:ofObject:change:context:)
- willChangeValueForKey:
二.如何手動(dòng)觸發(fā)KVO敌买?
- 手動(dòng)調(diào)用willChangeValueForKey:和didChangeValueForKey:
三.直接修改成員變量會(huì)觸發(fā)KVO么简珠?
不會(huì)觸發(fā)KVO