關(guān)于KVO的探究
KVO的基本使用
創(chuàng)建Person類,添加屬性age:
@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@end
在ViewController中添加屬性@property (nonatomic, strong) Person * person1;
實例化并添加KVO觀察age屬性:
self.person1 = [[Person alloc] init];
self.person1.age = 1;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
添加觀察監(jiān)聽回調(diào)并打优涡:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"被監(jiān)聽的 %@ 的值 %@ 改變?yōu)?%@", object, keyPath, change);
}
此時準(zhǔn)備工作完成涯呻,當(dāng)點擊view時就會修改age的值兄渺,并且回調(diào)打印出監(jiān)聽的結(jié)果鞋屈,這里在ViewController的touchedBegan中修改值:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.age = 11;
}
記得在最后移除鍵值觀察
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
以上為KVO的基本使用戴卜。
關(guān)于KVO的疑問和分析
再次添加屬性 @property (nonatomic, strong) Person * person2;
實例化person2捣域,在touchedBegan方法中修改值但是不添加KVO:
self.person2 = [[Person alloc] init];
self.person2.age = 2;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.age = 11;
self.person1.age = 22;
}
點擊view可以看到打印臺的日志為:
2018-07-17 14:09:26.944619+0800 KVO-KVC[36344:935709] 被監(jiān)聽的 <Person: 0x6040000106d0> 的值 age 改變?yōu)?{
kind = 1;
new = 11;
old = 1;
}
這時就可以思考都是修改age屬性值啼染,為什么person1會有回調(diào)而person2沒有,修改的本質(zhì)都是調(diào)用age的set方法焕梅。猜想person1和person2的set方法實現(xiàn)可能不一樣迹鹅,但是實例方法都是存放在class中的,set方法應(yīng)該是一樣的才對贞言,在touchesBegan處打斷點
斜棚,然后直接查看person1和person2的isa指針,看看person1和person2的class是否一樣:
(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_Person
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = Person
Fix-it applied, fixed expression was:
self.person2->isa
可以看到person1的class為 NSKVONotifying_Person
person2的class為 Person
,isa指針指向的就是instance的class弟蚀,但是為什么person1和person2會不一樣呢蚤霞?我們在添加鍵值觀察之前和之后分別打印person的類型:
NSLog(@"添加前 person1 : %@ person2 : %@", object_getClass(self.person1), object_getClass(self.person2));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"添加后 person1 : %@ person2 : %@", object_getClass(self.person1), object_getClass(self.person2));
打印的結(jié)果為
2018-07-17 14:40:59.918227+0800 KVO-KVC[37038:970983] 添加前 person1 : Person person2 : Person
2018-07-17 14:40:59.918636+0800 KVO-KVC[37038:970983] 添加后 person1 : NSKVONotifying_Person person2 : Person
可以看到添加鍵值觀察之后person1的isa指針指向確實被修改了,指向了 NSKVONotifying_Person
類义钉,結(jié)合上面的猜想昧绣,會不會是 NSKVONotifying_Person
這個類重新實現(xiàn)了person1的 setAge:
,否則怎么會和person2不一樣呢捶闸?
我們來驗證一下夜畴,通過 methodForSelector:
來獲取 setAge:
的實現(xiàn):
NSLog(@"添加前 person1 : %p person2 : %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
NSLog(@"添加后 person1 : %p person2 : %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
打印的結(jié)果為
2018-07-17 14:46:56.489956+0800 KVO-KVC[37183:978368] 添加前 person1 : 0x102493570 person2 : 0x102493570
2018-07-17 14:46:56.490699+0800 KVO-KVC[37183:978368] 添加后 person1 : 0x1027d9bf4 person2 : 0x102493570
我們知道instance的方法、屬性删壮、協(xié)議等信息都存在與class中贪绘,所以當(dāng)person1和person2調(diào)用 setAge:
時得到的地址應(yīng)該是一樣的,但是在添加鍵值觀察之后person1的調(diào)用方法地址改變了醉锅,為什么會改變呢?讓我們來看看這兩個地址的IMP发绢,在添加鍵值觀察之后斷點硬耍,直接查看兩個地址的IMP:
(lldb) p (IMP)0x100a43570
(IMP) $0 = 0x0000000100a43570 (KVO-KVC -[Person setAge:] at Person.m:13)
(lldb) p (IMP)0x100d89bf4
(IMP) $1 = 0x0000000100d89bf4 (Foundation _NSSetLongLongValueAndNotify)
可以看到添加鍵值觀察之后調(diào)用 setAge:
方法其實就是調(diào)用了 Foundation _NSSetLongLongValueAndNotify
由此可以猜測在添加鍵值觀察之后person1的isa指向了新生成的類 NSKVONotifying_Person
,NSKVONotifying_Person
可能繼承自 Person
類边酒,并且重寫了 setAge:
方法经柴,偽代碼如下:
- (void)setAge:(NSInteger)age {
_NSSetLongLongValueAndNotify();
}
void _NSSetLongLongValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
[observer observeValueForKeyPath:key ofObject:self change:opetions context:nil];
}
綜上我們的猜想KVO的實現(xiàn):instance添加鍵值觀察之后isa指針會被修改為指向 NSKVONotifying_Person
,NSKVONotifying_Person
繼承自 Person
并且重寫了 setAge:
方法墩朦,方法實現(xiàn)如上坯认。
在這里就有了那道最經(jīng)典的面試題:如何手動實現(xiàn)KVO,我們只需要在修改值的時候替換 _NSSetLongLongValueAndNotify
方法里面的 [super setAge:age];
就好了氓涣。
KVO內(nèi)部實現(xiàn)窺探
由上我們猜測出了KVO的實現(xiàn)原理牛哺,下面我們來繼續(xù)探索一下KVO內(nèi)部的實現(xiàn)。
我們分別在添加KVO前后打印person1和person2的class劳吠,這次我們用兩種方式:
NSLog(@"添加前 person1 : %@ -- %@ person2 : %@ -- %@", [self.person1 class], object_getClass(self.person1), [self.person2 class], object_getClass(self.person2));
NSLog(@"添加后 person1 : %@ -- %@ person2 : %@ -- %@", [self.person1 class], object_getClass(self.person1), [self.person2 class], object_getClass(self.person2));
打印出的結(jié)果為:
2018-07-19 11:05:50.553735+0800 KVO-KVC[40616:2560144] 添加前 person1 : Person -- Person person2 : Person -- Person
2018-07-19 11:05:52.772905+0800 KVO-KVC[40616:2560144] 添加后 person1 : Person -- NSKVONotifying_Person person2 : Person -- Person
可以看到我們通常用來獲取class的方法在添加前后結(jié)果都是 Person
引润,通過runtime API獲取到的class不相同,怎么回事呢痒玩?我們先來看一下蘋果官方runtime的源碼 這里淳附,當(dāng)然官方的編譯是失敗,要想調(diào)試runtime的請看 這里蠢古。
我們來分析一下源碼:
class方法:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
runtime object_getClass方法:
Class object_getClass(id obj) {
if (obj) return obj->getIsa();
else return Nil;
}
class
的類方法或者實例方法最終返回的都是class的self奴曙,而 object_getClass
方法返回的是obj的isa指針,所以通過 object_getClass
獲取的才是當(dāng)前obj的真正class草讶,所以在添加KVO之后person1的isa指針確確實實是被修改了洽糟。
我們再來看一下捕捉到的 NSKVONotifying_Person
到底是個什么鬼?
先來看一下 NSKVONotifying_Person
的meta-class:
NSLog(@"元類對象 person : %@ person1 : %@",
object_getClass(object_getClass(self.person1)),
object_getClass(object_getClass(self.person2)));
打印結(jié)果:
2018-07-19 11:39:30.210378+0800 KVO-KVC[41164:2599225] 元類對象 person : NSKVONotifying_Person person1 : Person
NSKVONotifying_Person
的meta-class為 NSKVONotifying_Person
。
在添加KVO之后打住斷點脊框,借用 DLIntrospection 再來查看一下此時class里面方法都有什么:
(lldb) po [[self.person1 class] instanceMethods]
<__NSArrayI 0x60400023daa0>(
- (void)setAge:(q)arg0 ,
- (q)age
)
(lldb) po [object_getClass(self.person1) instanceMethods]
<__NSArrayI 0x60400025fb30>(
- (void)setAge:(q)arg0 ,
- (class)class,
- (void)dealloc,
- (BOOL)_isKVOA
)
結(jié)果可以看到 NSKVONotifying_Person
重寫了 setAge:
方法颁督,并且還有其他的三個方法,可證上面的猜想確實沒錯浇雹,NSKVONotifying_Person
重寫了 setAge:
方法沉御,但是還有一個上面的猜想沒有驗證,那就是 NSKVONotifying_Person
的superClass到底是誰昭灵?
類似isa指針的方式吠裆,我們斷點直接打印:
(lldb) po self.person1.superclass
NSObject
(lldb) po self.person2.superclass
NSObject
咦~~~ 等等烂完,這跟我們猜測的不一樣啊试疙,怎么superclass都是NSObject呢?那我們的猜測是不是都錯了抠蚣?
為了看看superClass里面到底是什么下面我們請出 clang
大神:
clang -rewrite-objc Person.m
可以看出編譯完成后Person類被編譯成了這樣:
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSInteger _age;
};
結(jié)合runtime源碼分析祝旷,Class為 typedef struct objc_class *Class;
類型的結(jié)構(gòu)體,再看下結(jié)構(gòu)體里面的結(jié)構(gòu):
struct objc_object {
private:
isa_t isa;
···
}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
···
}
里面確實有superclass嘶窄,仿照runtime的結(jié)構(gòu)我們自己來創(chuàng)建一個類似的結(jié)構(gòu)體:
struct XFPerson_IMPL {
Class isa;
Class super_Class;
NSInteger _age;
};
用我們自己創(chuàng)建的結(jié)構(gòu)體來接收 NSKVONotifying_Person
怀跛,看看他的superclass到底是什么類型:
struct XFPerson_IMPL * xfPerson1 = (__bridge struct XFPerson_IMPL *)(object_getClass(self.person1));
struct XFPerson_IMPL * xfPerson2 = (__bridge struct XFPerson_IMPL *)(object_getClass(self.person2));
NSLog(@"person1--- %@", xfPerson1->super_Class);
NSLog(@"person2--- %@", xfPerson2->super_Class);
打印結(jié)果:
2018-07-19 14:05:38.855549+0800 KVO-KVC[43578:2717734] person1--- Person
2018-07-19 14:05:38.855658+0800 KVO-KVC[43578:2717734] person2--- NSObject
結(jié)果可見是符合我們的猜想的,NSKVONotifying_Person
確實是Person的子類柄冲,但是為什么上面直接打印instance的superclass卻都是NSObject呢吻谋?
回過頭來看一下上面我們找到的 NSKVONotifying_Person
除了 setAge:
還有三個方法,其中就有class方法现横,我們已經(jīng)知道runtime的class的實現(xiàn)漓拾,class返回的就是self,而通過 [self.person1 class]
得到的是 Person
戒祠,這就證明了 NSKVONotifying_Person
重寫了class方法骇两,并且返回的是 Person
類,通過源碼查看runtime的superclass方法的實現(xiàn):
+ (Class)superclass {
return self->superclass;
}
- (Class)superclass {
return [self class]->superclass;
}
就是先通過class方法找到class姜盈,然后在根據(jù)class找到superclass脯颜,所以前面直接通過 self.person1.superclass
找到的是 Person
,因為此時的class方法返回已經(jīng)被修改了贩据。
蘋果大大可能是因為整個事件中 NSKVONotifying_Person
是個人畜無害的東西栋操,對于開發(fā)者使用KVO是可以不用知道的,所以用這種方式來騙騙開發(fā)者饱亮,真不容易矾芙,還好最近看 白夜追兇 看的整個人都比較有耐心了就是要找到真相,哈(不)哈(要)哈(臉)??近上。
再看看看其他的兩個方法剔宪,dealloc
方法可能就是做一些銷毀現(xiàn)場的事情,畢竟中間動態(tài)創(chuàng)建了 NSKVONotifying_Person
,不用了一定要銷毀葱绒,而 _isKVOA
返回的一定是 YES 感帅,表示當(dāng)前確實是在用KVO,到此關(guān)于KVO的黑科技已經(jīng)探究明白了地淀,好了失球,打完收工,接著去看兩集 白夜追兇帮毁, 哈哈哈实苞。