一. KVO简介
熟悉iOS开发者模式的都知道 Key-Value-Observe(观察者模式)
该模式的实现基于三个方法:
给某个对象实例添加监听
1
2//参数的意思分别是 要被监听的对象 要被监听的属性 监听的类型 上下文
addObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>监听事件的回调
1
2//参数的意思分别是 要被监听的属性 要被监听的对象 被监听的属性发生的改变 上下文
observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context移除监听事件
1
2//参数的意思分别是 需要移除监听的对象 需要移除的监听属性
removeObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#>
通过比较这两个方法可以发现,很多在注册监听的时候传入的参数在监听回调的方法中都有返回。比如监听的对象,属性以及我们传入的上下文。
二. KVO实现原理
我们通过一个简单的列子来探寻KVO的内部实现细节:
创建一个名为ZBYObject的类。生成两个实例对象,给其中一个实例对象增加监听:1
2
3
4@interface ViewController ()
@property (nonatomic,strong) ZBYObject *obj1;
@property (nonatomic,strong) ZBYObject *obj2;
@end
1 | self.obj1 = [[ZBYObject alloc]init]; |
1 | -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ |
从以上代码来看,很容易能得出一个结论:当我们点击屏幕的时候,会Log出obj1的profession属性的变化。而obj2因为没有被监听,所以不会有任何相关的信息被log出。我们看到obj1的新值旧值都被打印了出来。这与我们添加监听的时候,传入的监听类型有关NSKeyValueObservingOptions。如果我们只想获得新值得话,去掉NSKeyValueObservingOptionOld就好了。对于NSKeyValueObservingOption来说,一般常用的两个值就是NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld。
那么对于obj1和obj2来说两者一定在哪方面是有些不同,所以当同时改变其profession属性值得时候,只有obj1可以监听到回调。
我们打印一下obj1和obj2的isa指针看看。
可以看到obj1被监听之后的isa指针指向了NSKVONotifying_ZBYObject这个类。这个类肯定不是我们自己创建的,所以这是系统通过Runtime动态添加的一个类。也就是说当我们给一个实例对象添加监听的时候,系统会自动通过运行时创建一个类。比如现在创建了一个类A,A的实例对象a被监听了,此时系统会动态创建一个叫NSKVONotifying_A的类,这个类是A的子类,原先实例a的isa指针是指向A的,现在指向NSKVONotifying_A。那我们看看具体NSKVONotifying_A和A有哪些不同,为什么NSKVONotifying_A就可以实现监听呢?
NSKVONotifying_XXX是什么
拥有一定编程基础的肯定都能意识到NSKVONotifying_ZBYObject这个类一定重载了某个方法或者实现了一些父类没有的方法才能监听,不然为什么系统要创建一个ZBYObject的子类来完成监听这件事呢?所以我们先获取NSKVONotifying_ZBYObject的实例方法,看看和ZBYObject的实例方法有什么不同。
这时候需要这样一个函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* Describes the instance methods implemented by a class.
*
* @param cls The class you want to inspect.
* @param outCount On return, contains the length of the returned array.
* If outCount is NULL, the length is not returned.
*
* @return An array of pointers of type Method describing the instance methods
* implemented by the class—any instance methods implemented by superclasses are not included.
* The array contains *outCount pointers followed by a NULL terminator. You must free the array with free().
*
* If cls implements no instance methods, or cls is Nil, returns NULL and *outCount is 0.
*
* @note To get the class methods of a class, use \c class_copyMethodList(object_getClass(cls), &count).
* @note To get the implementations of methods that may be implemented by superclasses,
* use \c class_getInstanceMethod or \c class_getClassMethod.
*/
OBJC_EXPORT Method _Nonnull * _Nullable
class_copyMethodList(Class _Nullable cls, unsigned int * _Nullable outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
这个函数的意思是传入一个class返回给你一个包含了class里面所有实例方法的数组指针,如果该class没有实例方法则返回NULL。这样的话我们可以通过这个函数拿到NSKVONotifying_ZBYObject的方法数组然后一一遍历打印。代码如下
1 | -(void)printMethodNamesOfClass:(Class )cls{ |
1 | [self printMethodNamesOfClass:object_getClass(self.obj1)]; |
打印信息如下:
我做了一张对比图
可以很清楚的看到两者的区别:
NSKVONotifying_ZBYObject重载了profession的setProfession方法NSKVONotifying_ZBYObject还实现了父类没有的class,dealloc和_isKVOA的方法
那么很明显,isKVO决定了NSKVONotifying_ZBYObject类能实现监听。不过因为拿不到苹果的源码,所以具体isKVO的实现我们并不清楚。不过我们可以来探讨一些其他的问题。
当我们想要去改变一个实例对象的某个属性值时,一定是通过该属性的set方法去改变的。而且刚刚我们也证实了NSKVONotifying_ZBYObject确实是重载了set方法。我们再分别打印一下NSKVONotifying_ZBYObject和ZBYObject的setProfession方法看一看。
这里要用到一个函数:1
- (IMP)methodForSelector:(SEL)aSelector;
该函数用于返回一个方法的实现,我们可以打印出方法的地址。
IMP:一个函数指针,保存了方法的地址。在LLDB环境下,通过(IMP)+方法的地址可以打印出来该方法的实现细节。
1 | NSLog(@"obj1添加KVO监听之前 - %p %p",[self.obj1 methodForSelector:@selector(setProfession:)],[self.obj2 methodForSelector:@selector(setProfession:)]); |
我们发现两者的setProfession方法实现是不同的。添加了监听后,setProfession方法的实现打印出了foundation里面的一个NSSetIntValueAndNotify()方法。
通过字面意思可以知道NSSetIntValueAndNotify()是int类型的赋值与通知方法。也就是说,当obj1被监听之后,系统会通过动态创建一个监听类,并重载了该类对应属性的set方法,在set方法中增加了监听实现的相关方法NSSetIntValueAndNotify(),然后让obj1继承这个类。这样obj1就拥有了被监听的能力。
为什么会实现class和dealloc方法呢?
其实你有兴趣的话可以通过[self.obj1 class]来打印一下结果,你会惊奇的发现,打印出来的居然是ZBYObject类,不是说好的继承自 NSKVONotifying_ZBYObject吗?
这就是为什么NSKVONotifying_ZBYObject类会重写class方法的原因。如果 NSKVONotifying_ZBYObject没有重载class方法,那么对 NSKVONotifying_ZBYObject实例对象调用class方法会去元类里面找相应的实现,这样会一直找到NSObject里的class实现。而NSObject的class实现是这样的:那么最终当NSKVONotifying_ZBYObject实例对象调用class方法的返回结果就是NSKVONotifying_ZBYObject。但是苹果并不想让你知道这个类,因为这个类是系统动态添加的一个类,只是用于监听的实现,所以苹果通过重载类的class方法将这个类隐藏起来。
那么为什么会重载dealloc方法呢?
我自己的想法是:既然NSKVONotifying_ZBYObject类重载了被监听属性的set方法。在set方法中实现了监听相关的方法,所以需要在dealloc方法中移除监听,避免内存泄漏。
三. KVO的触发方式
那么问题来了,既然KVO的本质是动态创建一个类重载了被监听属性的set方法。那么如果直接去访问成员变量,能否触发KVO呢?
我们在ZBYObject的头文件中生成一个成员变量:1
2
3
4{
@public
NSString *_profession;
}
然后我们直接访问它1
2
3-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.obj->_profession = @"Singer";
}
会发现监听方法并没有打印。所以直接访问成员变量并不会触发KVO。原因很简单,因为直接访问成员变量并没有触发其set方法,而重载set方法是KVO实现的本质。
我们再想一想,通过KVC来赋值的话能否触发KVO呢?这是个很有意思的事情。首先KVC和KVO在名称上就很相似。
KVC: KeyValueCoding一个非正式的Protocol,允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。
看完KVC的定义,相信读者心里已经有思路了。KVC并没有直接调用属性的存取方法,而是通过key名直接访问了对象的属性。既然是访问了属性,肯定是会访问其存取方法,只不过是间接的,所以KVC是可以触发KVO的。我们来验证一下:1
2
3-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.obj setValue:@"Singer" forKey:@"profession"];
}
会发现监听方法有打印。
所以想要触发KVO,就一定要以某种方式触发监听属性的set方法。那么是否可以手动触发KVO呢?苹果官方文档中有这么一句话:
To implement manual observer notification, you invoke
[willChangeValueForKey:]before changing the value, and[didChangeValueForKey:]after changing the value.
我们来验证一下,在ZBYObject的实现文件里重写set方法,willChangeValueForKey:和didChangeValueForKey:方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15-(void)setProfession:(NSString *)profession{
_profession = profession;
}
- (void)willChangeValueForKey:(NSString *)key
{
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
然后我们调用KVO,打印信息如下:1
2
3
4
5
6
7
8
92018-07-26 11:44:03.969498+0800 KVO[20563:42844607] willChangeValueForKey: - begin
2018-07-26 11:44:03.969754+0800 KVO[20563:42844607] willChangeValueForKey: - end
2018-07-26 11:44:03.969868+0800 KVO[20563:42844607] didChangeValueForKey: - begin
2018-07-26 11:44:03.970206+0800 KVO[20563:42844607] {
kind = 1;
new = Singer;
old = Student;
}
2018-07-26 11:44:03.970342+0800 KVO[20563:42844607] didChangeValueForKey: - end
我们发现,willChangeValueForKey:和didChangeValueForKey:确实被调用了。而且在didChangeValueForKey:里调用了KVO的回调方法observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context。
现在让我们手动触发KVO:1
2
3
4
5
6self.obj.profession = @"Student";
NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew;
[self.obj addObserver:self forKeyPath:@"profession" options:option context:nil];
[self.obj willChangeValueForKey:@"profession"];
[self.obj didChangeValueForKey:@"profession"];
打印信息如下:1
2
3
4
52018-07-26 11:57:38.808634+0800 KVO[20846:42857378] {
kind = 1;
new = Student;
old = Student;
}
这样,即使我们没有手动改变profession的值,但是我们通过手动调用willChangeValueForKey:和didChangeValueForKey:触发了KVO。
进一步我们可以推断,NSSetXXXValueAndNotify()里也应该是先后调用了willChangeValueForKey:,被监听属性的set方法和didChangeValueForKey:。
四. 总结
- 经过以上分析,我们知道
KVO的实现是基于动态修改属性的set方法来实现的。首先系统会动态创建一个子类,并将当前实例对象继承自该子类,在子类中重载了被监听属性的set方法并且实现了父类没有的两个方法:class和_isKVO。 - 该子类在重载的
set方法中调用了willChangeValueForKey:和didChangeValueForKey:并且在didChangeValueForKey:里调用了observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context。 - 我们可以通过任何可以访问属性存取方法的方式来触发
KVO(间接或者直接),我们也可以通过willChangeValueForKey:和didChangeValueForKey:两个方法来手动触发KVO。