小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言
从上一篇iOS底层原理-KVO(上)根据苹果官方文档的定义和示例代码,初步了解了KVO
在实际开发中如何去使用,但KVO
是如何通过对象的属性进行监听的?当对象属性改变时又是如何通知外部对象的;KVO
观察的是对象的setter
方法,那实现KVO
的过程setter
方法做了什么呢?通过这篇文章我们来探索一下KVO
的底层原理。
原理分析
代码调试
HomeVC.m
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"首页";
self.person = [[ATPerson alloc] init];
[self.person addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew
context:NULL];
}
在HomeVC
中添加ATPerson
的name
属性的监听,在添加监听之前打个断点(29行),输出当前的对象的类名。
可以看到输出
ATPerson
,然后在添加监听之后再输出对象的类名(33行处打个断点),运行。
可以看到输出了
NSKVONotifying_ATPerson
,在添加监听之后类由原来的ATPerson
变成了NSKVONotifying_ATPerson
,而NSKVONotifying_ATPerson
是由系统动态生成的,这2个类到底有什么关系呢?我们通过打印ATPerson
类及其所有子类看一下。
NSKVONotifying_ATPerson和ATPerson的关系
首先定义一个打印本类和所有子类的方法。
// 遍历类以及子类
- (void)printClasses:(Class)cls {
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i < count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
然后在添加KVO监听的前后分别打印,看前后的输出类的信息。
这里先做个说明,
ATStudent
是ATPerson
的子类,通过打印发现,在添加KVO之前打印出ATPerson
以及子类ATStudent
,而添加KVO之后打印了ATPerson
以及子类NSKVONotifying_ATPerson
和ATStudent
,而通过打印NSKVONotifying_ATPerson
发现它并没有子类。通过上面的分析,得出一个结论:
NSKVONotifying_ATPerson
是ATPerson
的子类NSKVONotifying_ATPerson
没有子类 接下来我们研究一下NSKVONotifying_ATPerson
类里包含了哪些方法?
NSKVONotifying_ATPerson的方法
还是通过定义一个方法来打印输出类的所有方法的函数。
// 遍历方法
- (void)printClassAllMethod:(Class)cls {
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel), imp);
}
free(methodList);
}
然后在viewDidLoad
的KVO
监听后加上这个函数的调用,看看NSKVONotifying_ATPerson
都包含了哪些方法。
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"首页";
self.person = [[ATPerson alloc] init];
[self.person addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew
context:NULL];
[self printClassAllMethod:objc_getClass("NSKVONotifying_ATPerson")];
}
然后点击运行
从输出可以看到打印了4个方法
- setName
- class
- dealloc
- _isKVOA
从上面的输出可以看到NSKVONotifying_ATPerson
重写了ATPerson
的方法(setName
,class
和dealloc
),_isKVOA
是KVO的标识。我们在移除KVO监听时NSKVONotifying_ATPerson
会有什么变化吗?接下来通过代码验证一下。
NSKVONotifying_ATPerson销毁
验证过程:HomeVC
push到SecondVC
,在SecondVC
做KVO监听,然后在dealloc
里移除监听,在SecondVC
的dealloc
打断点,来输出查看NSKVONotifying_ATPerson
是否已被销毁。
当点击返回
pop
到上一视图时,这时走到SecondVC
的dealloc
方法,在42
行代码处此时还是输出NSKVONotifying_ATPerson
,当跳到43
行代码处已经移除了KVO监听,这时再打印类的信息发现输出ATPerson
。因此我们得到了下面的结论:
- 当添加KVO监听时,
isa
指针会指向动态生成的类NSKVONotifying_ATPerson
。- 当移除监听时,
isa
指针又指回了ATPerson
。 通过上面的分析,isa
指针发生了改变,那NSKVONotifying_ATPerson
是否从注册的类里删除了呢?
NSKVONotifying_ATPerson类注册信息
可以在返回到HomeVC
时,在HomeVC
的touchBegin
方法里打印ATPerson
子类的情况。
返回到首页点击屏幕打印子类信息可以看到
NSKVONotifying_ATPerson
类并没有从类注册里删除,所以当KVO监听后注册了相关的NSKVONotifying_xxx
的类,就会保留,下次添加监听时无需再次注册。
KVO的setter方法
根据上面的输出知道KVO只监听setter
方法,也就是只监听属性变化,不监听成员变量的改变,下面通过代码来验证一下。
// 在ATPerson.h中定义一个成员变量hobby和一个属性name
@interface ATPerson : NSObject {
@public
NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
@end
然后在SecondVC
中添加属性name
和成员变量hobby
的监听,在touchesBegin
方法中分别对它们赋值,运行看KVO监听回调的日志打印。
从输出日志可以看出KVO只对
setter
方法的监听,也就是属性
的监听。 上面的setter
方法是NSKVONotifying_ATPerson
的setter
方法,那我们在dealloc
移除监听时打印一下self.person
的属性值是否有变化。当在
touchesBegin
点击给name
赋值,在dealloc
移除监听后,打印self.person.name
依然是存在刚刚KVO监听的值的,因此从底层是通过KVO的setter方法修改了属性,实际也修改了外部ATPerson
的属性。
断点调试
接下来通过断点调试来验证一下KVO的整个流程。还是在self.person
添加name的监听后打个断点,然后对name
属性进行观察。
对
name
属性观察成功后,跳过当前断点,点击屏幕触发改变name
属性值,然后输入bt
,查看执行的堆栈信息
当点击屏幕触发属性值改变时,从堆栈信息可以看到以下的调用流程:
- KVODemo`-[SecondVC touchesBegan:withEvent:]
- Foundation`_NSSetObjectValueAndNotify + 269
- Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
- Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 646
- KVODemo`-[ATPerson setName:]
从上面的调用流程可以看到,当KVO
属性变化时,不是直接对外部类通过setter
方法改变属性,而是有动态生成的中间类NSKVONotifying_xxx
重写了相关的setter
方法,设置完成后再通过block
的方式通知外部,进而改变外部类的属性值。而这些流程都是通过动态去完成,对于外部使用者来说是无感知的。
总结
本篇通过代码示例去验证了关于KVO
底层的实现过程,虽然有关KVC
和KVO
苹果没有提供源代码,但结合苹果的官方文档,以及自己在实际案例中去验证苹果的实现流程,结合上面的分析做个简单的总结:
- 当通过
KVO
的方式对属性进行监听,会自动生成NSKVONotifying_xxx
类 NSKVONotifying_xxx
是xxx
的子类NSKVONotifying_xxx
重写了setter
方法NSKVONotifying_xxx
在改变属性值后会通知外部- 触发
observeValueForKeyPath
方法调用