iOS-KVO(一) 基本操作
iOS-KVO(二) 使用注意點
iOS-KVO(三) 窺探底層實現(xiàn)
iOS-KVO(四) 自定義KVO+Block
我們將在這篇文章中了解到KVO底層的實現(xiàn)原理
底層實現(xiàn)分析
- 創(chuàng)建Person,然后添加一個name屬性;
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
- 創(chuàng)建兩個Person類的實例對象,然后其中為p1對象添加觀察者,然后觀察兩個對象的isa指向;
Person *p1 = [Person new];
Person *p2 = [Person new];
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
p1.name = @"p1";
p2.name = @"p2";
isa指向:

我們可以看到,在未添加觀察者之前,p1和p2都是指向Person類,但是p1添加觀察者之后,p1的isa指為NSKVONotifying_Person類,p2還是不變。
可見,p1對象添加觀察者之后,在運行時期間,p1的isa指針指向發(fā)生了改變。
并且我們通過獲取NSKVONotifying_Person的父類,得到的是Person類。
NSLog(@"%s", object_getClassName(object_getClass(p1))); //NSKVONotifying_Person
NSLog(@"%s", object_getClassName(class_getSuperclass(object_getClass(p1)))); //Person
- Person實例對象調(diào)用setter方法的流程
-
無添加觀察者的p2對象,調(diào)用setter方法流程
p2對象-setter.png
p2在調(diào)用setName:的時候,首先會通過isa指針找到Person類對象,然后在類對象的方法列表中找到setName:方法(緩存列表就不說了),然后在找到方法的實現(xiàn)。
添加觀察者的p1對象
上面已經(jīng)驗證了p1對象的isa指針指向的是NSKVONotifying_Person類對象,并且NSKVONotifying_Person是Person的子類。
所以p1在調(diào)用setName:的時候,調(diào)用的是NSKVONotifying_Person類對象中的setName:方法。
那究竟NSKVONotifying_Person的setName:方法實現(xiàn)了什么呢?
通過methodForSelector找到方法實現(xiàn)的地址
NSLog(@"添加觀察者前,p1=%p, p2=%p", [p1 methodForSelector:@selector(setName:)], [p2 methodForSelector:@selector(setName:)]);
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
NSLog(@"添加觀察者后,p1=%p, p2=%p", [p1 methodForSelector:@selector(setName:)], [p2 methodForSelector:@selector(setName:)]);

通過上面的輸出,我們了解到NSKVONotifying_Person類的setter方法轉(zhuǎn)換為Foundation框架的_NSSetObjectValueAndNotify函數(shù)。
那么這個框架內(nèi)部又是怎么實現(xiàn)的呢,我們可以下斷點,查看下函數(shù)調(diào)用棧:
首先通過設(shè)置一個觀察點,觀察屬性的變化:

繼續(xù)執(zhí)行,可以看到函數(shù)調(diào)用棧如下:

在結(jié)果發(fā)生改變的地方繼續(xù)下斷點調(diào)試:

由以上函數(shù)調(diào)用棧,我們大致可以猜測出,_NSSetObjectValueAndNotify函數(shù)內(nèi)部實現(xiàn)過程如下:
1. -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
2. -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
3. -[Person setName:]
4. NSKeyValueDidChange ()
5. NSKeyValueNotifyObserver ()
6. -[ViewController observeValueForKeyPath:ofObject:change:context:]
在_NSSetObjectValueAndNotify方法中,會調(diào)用willChangeValueForKey,然后調(diào)用父類的setName:方法,再調(diào)用didChangeValueForKey方法。在didChangeValueForKey中通知屬性改變,從而使得observeValueForKeyPath得到消息。
偽代碼:
- (void)setName:(NSString *)name
{
_NSSetObjectValueAndNotify();
}
void _NSSetObjectValueAndNotify(){
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
- (void)didChangeValueForKey:(NSString *)key{
// 通知監(jiān)聽器 key發(fā)生了改變
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
- Person和NSKVONotifying_Person內(nèi)部方法
利用runtime打印一下各自的方法
- 獲取Person類的方法
unsigned int outCount = 0;
Method *methods = class_copyMethodList([Person class], &outCount);
for (int i = 0; i < outCount; ++i) {
Method method = methods[i];
NSString *name = NSStringFromSelector(method_getName(method));
NSLog(@"%@", name);
}
free(methods);
打印結(jié)果:
2019-07-04 21:37:24.616010+0800 KVODemo[8362:239834] .cxx_destruct
2019-07-04 21:37:24.616119+0800 KVODemo[8362:239834] name
2019-07-04 21:37:24.616222+0800 KVODemo[8362:239834] setName:
- 獲取NSKVONotifying_Person類的方法
unsigned int outCount = 0;
Method *methods = class_copyMethodList(object_getClass(self.p1), &outCount);
for (int i = 0; i < outCount; ++i) {
Method method = methods[i];
NSString *name = NSStringFromSelector(method_getName(method));
NSLog(@"%@", name);
}
free(methods);
打印結(jié)果:
2019-07-04 21:37:24.616425+0800 KVODemo[8362:239834] setName:
2019-07-04 21:37:24.616518+0800 KVODemo[8362:239834] class
2019-07-04 21:37:24.616605+0800 KVODemo[8362:239834] dealloc
2019-07-04 21:37:24.616692+0800 KVODemo[8362:239834] _isKVOA
分析一下NSKVONotifying_Person類重寫方法的意義:
setName:
實現(xiàn)上面的所說的操作;class
蘋果不希望將NSKVONotifying_Person類暴露出來,所以在內(nèi)部重寫了class類,直接返回Person類;
偽代碼大致如下:
- (Class)class
{
/*
1.找到類對象;
2.再通過類對象找到父類;
*/
return class_getSuperclass(object_getClass(self));
}
dealloc
runtime在實例對象添加了KVO之后動態(tài)創(chuàng)建了類和一些對象,所以可能會在dealloc中回收這些資源;_isKVOA
是否使用了KVO;
自己創(chuàng)建了一個跟系統(tǒng)生成的中間類一樣的類會怎么樣?
因為KVO對生成的中間類的格式是有要求的,默認(rèn)都是以NSKVONotifying_<class>來命名,那如果我們不小心自己創(chuàng)建了一個一樣名字的中間類,會怎么樣呢?
控制臺會輸出
2019-07-04 21:52:12.259489+0800 KVODemo[8597:247439] [general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class
無法創(chuàng)建NSKVONotifying_Person類,導(dǎo)致KVO無法使用。
總結(jié)
- 創(chuàng)建一個中間類,默認(rèn)中間類的格式為NSKVONotifying_<class>,中間類是目標(biāo)對象的類的子類,目標(biāo)對象的isa指針指向中間類;
- 重寫setter方法;
- 重寫class方法,返回父類;
over!
