探究KVO的实现原理

一. KVO简介

熟悉iOS开发者模式的都知道 Key-Value-Observe(观察者模式)
该模式的实现基于三个方法:

  1. 给某个对象实例添加监听

    1
    2
    //参数的意思分别是 要被监听的对象 要被监听的属性 监听的类型 上下文
    addObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>
  2. 监听事件的回调

    1
    2
    //参数的意思分别是 要被监听的属性 要被监听的对象 被监听的属性发生的改变 上下文
    observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
  3. 移除监听事件

    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
2
3
4
5
6
7
self.obj1 = [[ZBYObject alloc]init];
self.obj1.profession = @"Singer";
self.obj2 = [[ZBYObject alloc]init];
self.obj2.profession = @"Student";

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.obj1 addObserver:self forKeyPath:@"profession" options:options context:nil];
1
2
3
4
5
6
7
8
9
10
11
12
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.obj1.profession = @"Doctor";
self.obj2.profession = @"Teacher";
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了:%@",self.obj1,keyPath,change);
}

-(void)dealloc{
[self.obj1 removeObserver:self forKeyPath:@"profession"];
}

从以上代码来看,很容易能得出一个结论:当我们点击屏幕的时候,会Log出obj1profession属性的变化。而obj2因为没有被监听,所以不会有任何相关的信息被log出。我们看到obj1的新值旧值都被打印了出来。这与我们添加监听的时候,传入的监听类型有关NSKeyValueObservingOptions。如果我们只想获得新值得话,去掉NSKeyValueObservingOptionOld就好了。对于NSKeyValueObservingOption来说,一般常用的两个值就是NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld

那么对于obj1obj2来说两者一定在哪方面是有些不同,所以当同时改变其profession属性值得时候,只有obj1可以监听到回调。

我们打印一下obj1obj2isa指针看看。

可以看到obj1被监听之后的isa指针指向了NSKVONotifying_ZBYObject这个类。这个类肯定不是我们自己创建的,所以这是系统通过Runtime动态添加的一个类。也就是说当我们给一个实例对象添加监听的时候,系统会自动通过运行时创建一个类。比如现在创建了一个类AA的实例对象a被监听了,此时系统会动态创建一个叫NSKVONotifying_A的类,这个类是A的子类,原先实例aisa指针是指向A的,现在指向NSKVONotifying_A。那我们看看具体NSKVONotifying_AA有哪些不同,为什么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
2
3
4
5
6
7
8
9
10
11
12
13
14
-(void)printMethodNamesOfClass:(Class )cls{
unsigned int count;
NSMutableArray *methodsNameMA = [NSMutableArray array];
//获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
//遍历所 有方法
for (int i = 0; i<count; i++) {
Method method = methodList[i];
[methodsNameMA addObject:NSStringFromSelector(method_getName(method))];
}
//释放
free(methodList);
NSLog(@"%@ - %@",cls,methodsNameMA);
}
1
2
[self printMethodNamesOfClass:object_getClass(self.obj1)];
[self printMethodNamesOfClass:object_getClass(self.obj2)];

打印信息如下:

我做了一张对比图

可以很清楚的看到两者的区别:

  1. NSKVONotifying_ZBYObject重载了professionsetProfession方法
  2. NSKVONotifying_ZBYObject还实现了父类没有的classdealloc_isKVOA的方法

那么很明显,isKVO决定了NSKVONotifying_ZBYObject类能实现监听。不过因为拿不到苹果的源码,所以具体isKVO的实现我们并不清楚。不过我们可以来探讨一些其他的问题。

当我们想要去改变一个实例对象的某个属性值时,一定是通过该属性的set方法去改变的。而且刚刚我们也证实了NSKVONotifying_ZBYObject确实是重载了set方法。我们再分别打印一下NSKVONotifying_ZBYObjectZBYObjectsetProfession方法看一看。

这里要用到一个函数:

1
- (IMP)methodForSelector:(SEL)aSelector;

该函数用于返回一个方法的实现,我们可以打印出方法的地址。
IMP:一个函数指针,保存了方法的地址。在LLDB环境下,通过(IMP)+方法的地址可以打印出来该方法的实现细节。

1
2
NSLog(@"obj1添加KVO监听之前 - %p %p",[self.obj1 methodForSelector:@selector(setProfession:)],[self.obj2 methodForSelector:@selector(setProfession:)]);
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就拥有了被监听的能力。

为什么会实现classdealloc方法呢?

其实你有兴趣的话可以通过[self.obj1 class]来打印一下结果,你会惊奇的发现,打印出来的居然是ZBYObject类,不是说好的继承自 NSKVONotifying_ZBYObject吗?

这就是为什么NSKVONotifying_ZBYObject类会重写class方法的原因。如果 NSKVONotifying_ZBYObject没有重载class方法,那么对 NSKVONotifying_ZBYObject实例对象调用class方法会去元类里面找相应的实现,这样会一直找到NSObject里的class实现。而NSObjectclass实现是这样的:那么最终当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呢?这是个很有意思的事情。首先KVCKVO在名称上就很相似。

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.

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html

我们来验证一下,在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
9
2018-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
6
self.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
5
2018-07-26 11:57:38.808634+0800 KVO[20846:42857378] {
kind = 1;
new = Student;
old = Student;
}

这样,即使我们没有手动改变profession的值,但是我们通过手动调用willChangeValueForKey:didChangeValueForKey:触发了KVO

进一步我们可以推断,NSSetXXXValueAndNotify()里也应该是先后调用了willChangeValueForKey:被监听属性的set方法didChangeValueForKey:

四. 总结

  1. 经过以上分析,我们知道KVO的实现是基于动态修改属性的set方法来实现的。首先系统会动态创建一个子类,并将当前实例对象继承自该子类,在子类中重载了被监听属性的set方法并且实现了父类没有的两个方法:class_isKVO
  2. 该子类在重载的set方法中调用了willChangeValueForKey:didChangeValueForKey:并且在didChangeValueForKey:里调用了observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
  3. 我们可以通过任何可以访问属性存取方法的方式来触发KVO(间接或者直接),我们也可以通过willChangeValueForKey:didChangeValueForKey:两个方法来手动触发KVO
0%