用 Runtime 自己动手实现 KVO —— 探究 KVO 的底层实现

321 阅读4分钟

KVO 的实现原理

在 Objective-C 中,用 KVO 可以很方便的观察某个属性的值的变化,一有变化可以立刻响应,虽然滥用 KVO 容易踩坑,但是在很多情形下,KVO 还是很好用的。接下来我们来看一看 KVO 是怎么实现的。


我们来写一个例子来研究,创建一个 Simple App 项目,首先还是写一个 Student 类:

#import <Foundation/Foundation.h>
 @interface Student : NSObject
 @property (nonatomic, copy) NSString *name;
@end

在viewcontroller里定义

- (void)viewDidLoad {  
  [super viewDidLoad];    
    // Do any additional setup after loading the view, typically from a nib.
    Student *b = [[Student alloc] init];    
    [b addObserver:self 
       forKeyPath:@"name" 
          options:NSKeyValueObservingOptionNew context:nil];
    self.b = b;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {    
    NSLog(@"name = %@", self.b.name);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    static NSInteger i = 0;
    i++;    
    self.b.name = [NSString stringWithFormat:@"%@", @(i)];
}

运行程序,每次点击屏幕后,会修改 b 对象的 name 的值,然后触发 KVO 回调,打印出 name 的值。

在 Student *b = [[Student alloc] init]; 一行设一个断点,重新运行程序,程序中断在这一行上。

让程序往下运行一步,然后在下方查看 b 对象的 isa 指针的值:

isa Class Student 0x0000000103ba15d8

这时 isa 的值是 Student 这个类。



让程序再往下走一步,再次查看 b 对象的 isa 指针:

会发现这时 b 对象的 isa 指针变为了 NSKVONotifying_Student 这个类。

因此,可以大概知道,KVO 的实现原理为:

在执行 addObserver:selector:name:object: 时,创建了一个被观察对象的子类,并重写了被观察属性的 setter 方法。

结果表明,

原始类和中间类都有 setter 方法。根据我们前面所探索的消息发送以及转发流程,这里的中间类应该是重写了 setName:classdealloc_isKVOA 方法。(中间类重写的 class 方法结果仍然是返回的是原始类,显然系统这样做的目的就是隐藏中间类的存在,让调用者调用 class 方法结果前后一致。

移除观察者之后,对象的 isa 指针已经指回了原始的类。
中间类仍然存在,也就是说移除观察者并不会导致中间类销毁,显然这样对于多次添加和移除观察者来说性能上更好。



下面来通过自己实现 KVO 来看一下具体的细节。

自己动手实现 KVO

下面我们来针对上面例子中 Student 的 name 来自己实现 KVO。下面的实现中先写死仅对 setName: 方法实现 KVO,来提供一个最简单的实现流程。

1.

创建一个 NSObject 的类别,文件为 NSObject+MyKVOImp.h 和 NSObject+MyKVOImp.m。

2.

在 NSObject+MyKVOImp.h 中,添加两个方法声明:

- (void)my_addObserver:(NSObject *)observer 
               forKeyPath:(NSString *)keyPath 
              options:(NSKeyValueObservingOptions)options 
             context:(void *)context;

- (void)my_observeValueForKeyPath:(NSString *)keyPath 
                        ofObject:(id)object 
                          change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
                         context:(void *)context;

上面两个方法用于替代系统 KVO 提供的那两个不带前缀的方法。

3.

在 NSObject+MyKVOImp.m 中添加 my_addObserver... 方法的实现:

- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
//1. 拿到当前类名,在前面加上 NSKVONotifying_ 前缀创建一个新的类名。  NSString *oldClassName = NSStringFromClass([self class]);    
  NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@", oldClassName];   

//2. 创建一个当前类的子类,并以新类名命名。
  Class NewClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);       

//3. 为这个新类添加一个方法
  class_addMethod(NewClass, @selector(setName:), (IMP)setName, "v@:@");   

//4. 把新创建的子类注册进运行时环境,使其可用。
  objc_registerClassPair(NewClass);     

//5. 把调用 my_addObserver 方法的对象的 isa 指针设置为新创建的子类
  object_setClass(self, NewClass);       

//6. 把观察者关联到 self 上,使得可以在 setName 函数中取出
  objc_setAssociatedObject(self, &kAssociatedKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

4. 现在来看看 setName 函数的实现:

void setName(id self, SEL _cmd, NSString *str) {
     //1. 修改实例变量的值。
     Ivar ivar = class_getInstanceVariable([self class], "_name");
     object_setIvar(self, ivar, str);              

     //2. 获取到关联的 Observer 对象
     NSObject *observer = objc_getAssociatedObject(self, &kAssociatedKey);

     //3. 调用 my_observeValueForKeyPath... 方法
     if ([observer respondsToSelector:@selector(my_observeValueForKeyPath:ofObject:change:context:)]) {
           [observer my_observeValueForKeyPath:@"name" ofObject:self change:@{NSKeyValueChangeNewKey: str} context:nil];    
     }
}

我们知道每个 Objective-C 方法都含有两个隐式参数 self 和 _cmd,他们对应 C 函数中的第一个和第二个参数。因此 setName 的函数的第一个和第二个参数必须是 id self 和 SEL _cmd。第三个参数才是作为生成的 Objective-C 方法 setName: 的第一个参数。




这样一个简单的 KVO 就实现好了,可以在 ViewController 中调用我们自己实现的方法试试效果:

- (void)viewDidLoad {    
    [super viewDidLoad];    
    self.st = [[Student alloc] init];
    [self.st my_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];}
- (void)my_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"name = %@", self.st.name);}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    static NSInteger i = 0;
    i++;
    self.st.name = [NSString stringWithFormat:@"%@", @(i)];}

结语

以上我们介绍了 KVO 的实现原理并自己实现了一个简单的 KVO,实际上 KVO 的实现还是很复杂的,要考虑到很多地方,复杂的实现网上有相关代码,或者看 KVO 源码了解一下。