我们知道实例实际是存储了成员变量的值和指向类的isa
指针,class
对象和meta-class
对象包含 isa
、superclass
和class_rw_t
这几种结构体,只是数据不一样,isa
需要ISA_MASK
&之后才是真正的值。那么今天我们在看一下Key-Value Observing的本质。
KVO本质
首先需要了解KVO基本使用,KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。下面我们展示一下KVO的基本使用。
1 |
|
从上述代码可以看出,添加监听之后,当值改变时,会触发函数observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
。
触发条件
1 | - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ |
当把age
具体值的改变,变成手动调用willChangeValueForKey
和didChangeValueForKey
的时候,结果如下:
1 | 监听到了age变化: { |
new
和old
的值竟然一样,经测试只有同时先后调用willChangeValueForKey
和didChangeValueForKey
,会触发回调函数observeValueForKeyPath
,由此可知触发条件是willChangeValueForKey
和didChangeValueForKey
配合使用。
探寻KVO底层实现原理
通过上述代码我们发现,一旦age属性的值发生改变时,就会通知到监听者,并且我们知道赋值操作都是调用 set方法,我们可以来到Person类中重写age的set方法,观察是否是KVO在set方法内部做了一些操作来通知监听者。
我们发现即使重写了set方法,p1对象和p2对象调用同样的set方法,但是我们发现p1除了调用set方法之外还会另外执行监听器的observeValueForKeyPath方法。
说明KVO在运行时获取对p1对象做了一些改变。相当于在程序运行过程中,对p1对象做了一些变化,使得p1对象在调用setage方法的时候可能做了一些额外的操作,所以问题出在对象身上,两个对象在内存中肯定不一样,两个对象可能本质上并不一样。接下来来探索KVO内部是怎么实现的。
KVO底层实现分析
首先我们对上述代码中添加监听的地方打断点,看观察一下,addObserver方法对p1对象做了什么处理?也就是说p1对象在经过addObserver方法之后发生了什么改变,我们通过打印isa指针:
1 | @interface ViewController () |
从输出的isa指针看来,经过【person addObserver】
之后,person
的isa
指针指向了NSKVONotifying_FYPerson
,而person2
的isa
是FYPerson
,可以看出系统是对instance
对象的isa
进行了赋值操作。通过p NSKVONotifying_FYPerson_class->superclass==FYPerson
可以看出isa是指向了子类,那么子类NSKVONotifying_FYPerson
到底做了那些事情呢?
看下边代码查看函数isa改变过程:
1 | self.person=[FYPerson new]; |
可以看出来两次的函数地址不一致,添加KVO之前是[FYPerson setAge:]
,添加之后是(Foundation_NSSetIntValueAndNotify)
。我们将age
的类型改成double
,再看一下结果:
1 | (lldb) po [_person methodForSelector:@selector(setAge:)] |
age
是int
的时候添加之后是Foundation _NSSetIntValueAndNotify
,改成double
之后,是Foundation _NSSetDoubleValueAndNotify
。那么我们可以推测Foundation
框架中还有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify
等等函数。
运行nm Foundation | grep ValueAndNotify
结果如下:
1 | nm Foundation | grep ValueAndNotify |
另外一种验证方法
在macOS中可以使用
1 | //开始记录日志 |
如果将NSObjCMessageLoggingEnabled
环境变量设置为YES
,则Objective-C
运行时会将所有已分派的Objective-C
消息记录到名为/tmp/msgSends-<pid>
的文件中。每一次运行会生成一个文件,我们进入到该文件内部:
1 | //初始化 |
经过仔细把重要的函数过滤出来,我们可以了解到person.age = 12
的执行过程是NSKVONotifying_FYPerson setAge:
->NSKeyValueUnnestedProperty object:withObservance:willChangeValueForKeyOrKeys:recurse:forwardingValues
->FYPerson FYPerson setAge:
->NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty object:withObservance:didChangeValueForKeyOrKeys:recurse:forwardingValues:
->NSKVONotifying_FYPerson NSObject valueForKeyPath:
->NSMutableDictionary NSObject self
->- NSKVONotifying_FYPerson FYPerson observeValueForKeyPath:ofObject:change:context:
,我们来用伪代码实现一遍:
1 | //person.age = 12 |
NSKVONotifyin_Person内部结构是怎样的?
首先我们知道,NSKVONotifyin_Person作为Person的子类,其superclass指针指向Person类,并且NSKVONotifyin_Person内部一定对setAge方法做了单独的实现,那么NSKVONotifyin_Person同Person类的差别可能就在于其内存储的对象方法及实现不同。
我们通过runtime分别打印Person类对象和NSKVONotifyin_Person类对象内存储的对象方法
1 | - (void)viewDidLoad { |
通过上述代码我们发现NSKVONotifyin_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA,那么至此我们可以画出NSKVONotifyin_Person的内存结构以及方法调用顺序。
这里NSKVONotifyin_Person重写class方法是为了隐藏NSKVONotifyin_Person。不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的class可以发现他们都返回Person。
如果NSKVONotifyin_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到nsobject,而nsobect的class的实现大致为返回自己isa指向的类,返回p1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person,但是apple不希望将NSKVONotifyin_Person类暴露出来,并且不希望我们知道NSKVONotifyin_Person内部实现,所以在内部重写了class类,直接返回Person类,所以外界在调用p1的class对象方法时,是Person类。这样p1给外界的感觉p1还是Person类,并不知道NSKVONotifyin_Person子类的存在。
那么我们可以猜测NSKVONotifyin_Person内重写的class内部实现大致为
1 | - (Class) class { |
最后自己写代码验证一下:
1 |
|
执行之后结果如下:
1 | -[FYPerson willChangeValueForKey:] 开始 |
总结:
KVO其实是一个通过runtime注册建立子类,通过修改instance的isa指针,指向新的子类,重写instace的class方法来掩盖,子类拥有自己的set方法,调用顺序是willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
KVC的本质
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性。
常用的API有
1 | - (void)setValue:(id)value forKeyPath:(NSString *)keyPath; |
其实当Obj调用(void)setValue:(id)value forKey:(NSString *)key
的时候,obj
会主动寻找方法setKey
和_setKey
两个方法,没有找到这两个方法会再去寻找accessInstanceVariablesDirectly
,返回值为NO
则抛出异常,返回YES
则去按照_key
、_isKey
、key
、isKey
的查找优先级查找成员变量,找到之后直接复制,否则抛出异常。
我们使用这段代码来验证:
1 | @interface FYPerson(){ |
当执行code1
和code2
都有的时候,输出-[FYPerson setAge:] 2
,当code1
注释掉,输出-[FYPerson _setAge:] 2
,可以看出执行顺序是setAge
,没有setAge
的时候再去执行_setAge
。
现在新增FYPerson
4个成员变量,依次注释掉他们来测试寻找成员变量的顺序。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface FYPerson : NSObject
{
@public
NSInteger _age;
NSInteger _isAge;
NSInteger age;
NSInteger isAge;
}
@end
FYPerson *p=[[FYPerson alloc]init];
[p setValue:@(2) forKey:@"age"];
NSLog(@"age:%d _age:%d isAge:%d _isAge:%d",(int)p->age,(int)p->_age,(int)p->isAge,(int)p->_isAge);
- 没注释输出
age:0 _age:2 isAge:0 _isAge:0
- 注释
_age
输出age:0 isAge:0 _isAge:2
- 注释
_isAge
输出age:2 isAge:0
- 注释
age
输出isAge:2
KVC和KVO联系
我们知道KVC本质也是调用setter方法,那么会出发KVO吗?
1 | FYPerson *p=[[FYPerson alloc]init]; |
经过测试,可以看出KVC能触发KVO的。那么valueForKey:key
底层是怎么运行的呢?其实底层是按照顺序查找四个方法_age
->_isAge
->age
->isAge
。我们测试一下:
1 | FYPerson *p=[[FYPerson alloc]init]; |
总结:
KVC其实本质是执行4个set方法和4个get方法,当使用setValue:forKey:key
会触发KVO,找不到4个方法的时候会抛出异常。
资料下载
- 学习资料下载
- demo code
之前看的没有手动去试验一下,然后再写出来,现在总结一下,参考了很多文章,还有macOS中日志记录是无意搜索出来了一个老外的blog,大家可以了解下,以后会有用,后边会讲如何hook objc_msgsend
,感觉这个挺好玩的。
本文章之所以图片比较少,我觉得还是跟着代码敲一遍,印象比较深刻。
最怕一生碌碌无为,还安慰自己平凡可贵。
广告时间