KVO实现原理

1 – 代码示例

① 在 Person 中声明 age属性;在 ViewController.m中创建两个 Person实例对象,将其中一个添加观察者

// – Person.h

1 #import <Foundation/Foundation.h>
2 @interface Person : NSObject
3 
4 @property(nonatomic,assign)int age;
5 
6 @end

// – Person.m

 1 #import "Person.h"
 2 @implementation Person
 3 @synthesize age = _age;
 4 
 5 // 没有添加观察者的对象,点语法依旧调用原来的 setter方法
 6 - (void)setAge:(int)age{
 7     _age = age;
 8 }
 9 
10 - (int)age{
11     
12     return _age;
13 }
14 
15 // 添加了观察者的对象,调用 setter方法的实现如下(这些都是在 runtime中动态生成)
16 // ----- 下面是伪代码,模拟 KVO实现流程 -----
17 /*
18 
19 // 在 setter方法中会执行 __NSSetInValueAndNotify函数,它是 C语言私有函数,不会有提示
20 - (void)setAge:(int)age{
21  
22      __NSSetInValueAndNotify();
23 }
24      
25 // __NSSetInValueAndNotify函数的作用
26 void __NSSetInValueAndNotify(){
27    
28    // 首先调用 willChangeValueForKey方法
29    [self willChangeValueForKey:@"age"];
30  
31    // 其实是开始初始化实例变量:直接调用父类的初始化方法
32    [super setAge:age];
33  
34    // 最后调用 didChangeValueForKey方法
35    [self didChangeValueForKey:@"age"];
36     
37 }
38 
39 // 重点来了:didChangeValueForKey会拿到这个 observer,触发监听方法
40 - (void)didChangeValueForKey:(NSString *)key{
41  
42     // 触发监听
43     [observer observeValueForKeyPath:@"age" ofObject:self change:@{} context:nil]
44 }
45  
46 **/
47 
48 // 重写这两个方法,验证监听方法是在 didChangeValueForKey中触发的
49 - (void)willChangeValueForKey:(NSString *)key{
50     
51     NSLog(@"willChangeValueForKey-begin");
52     [super willChangeValueForKey:key];
53     NSLog(@"willChangeValueForKey-end");
54 }
55 
56 -(void)didChangeValueForKey:(NSString *)key{
57     
58     NSLog(@"didChangeValueForKey-begin");
59     [super didChangeValueForKey:key];
60     NSLog(@"didChangeValueForKey-end");
61 }
62 
63 @end

// – ViewController.m

  1 #import "ViewController.h"
  2 #import <objc/runtime.h>
  3 #import "Person.h"
  4 @interface ViewController()
  5 
  6 @end
  7 
  8 @implementation ViewController
  9 
 10 // 添加了观察者的对象,其内部会调用 setAge:   class   dealloc   _isKVOA
 11 // 我们在此把它们打印出来进行验证
 12 -(void)printMethods:(Class)cls{
 13     
 14     // 记录函数个数
 15     unsigned int count;
 16     // 拿到函数列表
 17     Method *methods = class_copyMethodList(cls, &count);
 18     
 19     // 字符串:拼接将要遍历出的函数名
 20     NSMutableString *methodsNames = [NSMutableString string];
 21     [methodsNames appendFormat:@"添加了观察的对象 %@所调用的方法有:\n",cls];
 22     
 23     // 遍历函数
 24     for (int i = 0; i < count; i++) {
 25         
 26         Method  method = methods[i];
 27         NSString *methodName = NSStringFromSelector(method_getName(method));
 28         [methodsNames appendString:methodName];
 29         [methodsNames appendString:@"  "];// 间隔
 30     }
 31     
 32     NSLog(@"%@",methodsNames);
 33     
 34     // C语言函数,需要释放
 35     free(methods);
 36 }
 37 
 38 
 39 // 其中,我们使用 lldb命令更好的展示
 40 - (void)viewDidLoad {
 41     [super viewDidLoad];
 42     self.view.backgroundColor = [UIColor cyanColor];
 43     
 44     
 45     Person *p1 = [Person new];
 46     // p p1->isa    Person
 47     p1.age = 100;
 48     
 49     Person *p2 = [Person new];
 50     // p p2->isa    Person
 51     p2.age = 10000;
 52     
 53     
 54     // 未添加观察者之前,点语法走的都是同一个 setter
 55     NSLog(@"%p",[p1 methodForSelector:@selector(setAge:)]); // 0x10f760390
 56     NSLog(@"%p",[p2 methodForSelector:@selector(setAge:)]); // 0x10f760390
 57     // p (IMP)0x10f760390   [Person setAge:]
 58     
 59 
 60     // p1添加观察者后,其 isa指针发生了变化,指向了一个新的类对象
 61     [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"传参"];
 62     // p p1->isa  NSKVONotifying_Person
 63     // 思考:新的类对象 NSKVONotifying_Person从哪里来,又是什么东东
 64     // 其实 NSKVONotifying_Person是 Person的子类,它是运行时动态生成
 65     
 66     // 遍历方法
 67     [self printMethods:object_getClass(p1)];
 68     // 输出结果 setAge:   class   dealloc   _isKVOA
 69     
 70     // 注意 class
 71     NSLog(@"%@",[p1 class]);// 输出 Person
 72     // 我们知道添 p1加了观察者后,其类对象是 NSKVONotifying_Person,那为什么这里为什么打印的还是 Person
 73     // 因为 NSKVONotifying_Person中重写了 class。要知道新产生的类对象是运行时动态生成的,苹果是不愿意将其暴露出来的
 74     // 如何重写我们无法得知,但一定的是它最终会返回类对象 Person,而不是自己的 NSKVONotifying_Person
 75     
 76     
 77     // 重新赋值
 78     p1.age = 200;
 79     p1.age = 300;
 80     p2.age = 20000;
 81 
 82     
 83     // p2调用的方法依旧是原来的 setter
 84     NSLog(@"%p",[p2 methodForSelector:@selector(setAge:)]); // 0x10f760390
 85     
 86     // p1 添加了观察者,setter方法发生改变
 87     NSLog(@"%p",[p1 methodForSelector:@selector(setAge:)]); // 0x7fff258f10eb
 88     // p (IMP)0x7fff258f10eb    (Foundation`_NSSetIntValueAndNotify)
 89     // 因为 Person属性 age是 int型,所以这里调用的是 _NSSetIntValueAndNotify函数
 90   
 91     // 移除监听
 92     [p1 removeObserver:self forKeyPath:@"age"];
 93 }
 94 
 95 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
 96     
 97     NSLog(@"keyPath = %@    object = %@  change = %@",keyPath,object,change);
 98     
 99 }
100 
101 @end 

日志信息:验证监听方法是在 didChangeValueForKey中触发的

② KVO流程分析图

③ 可以使用命令行 nm Foundation | grep ValueAndNotify 查询相对应的函数

如何手动启用 KVO

1 – 只需要添加监听的对象手动调用 willChangeValueForKey、didChangeValueForKey两方法,二者缺一不可

// – Person.h

#import <Foundation/Foundation.h>
@interface Person : NSObject{
    @public
    NSString *_name;
}
@end

// – ViewController.m

 1 // 其中,我们使用 lldb命令更好的展示
 2 - (void)viewDidLoad {
 3     [super viewDidLoad];
 4     self.view.backgroundColor = [UIColor cyanColor];
 5 
 6     Person *p1 = [Person new];
 7     p1 ->_name = @"123";
 8     [p1 addObserver:self forKeyPath:@"_name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"传参"];
 9 
10     // 手动启用 KVO
11     [p1 willChangeValueForKey:@"_name"];
12     p1 ->_name = @"456";
13     [p1 didChangeValueForKey:@"_name"];
14     
15     [p1 removeObserver:self forKeyPath:@"_name"];
16 }
17 
18 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
19     
20     NSLog(@"keyPath = %@    object = %@  change = %@",keyPath,object,change);
21     
22 }

日志信息:直接赋值,同样可以实现监听

结语

1 – KVO工作原理

① 当一个对象使用了 KVO监听,iOS系统会修改这个对象的 isa指针,重指向一个全新的通过 Runtime动态创建的子类

② 子类拥有自己的 set方法实现,内部会调用

     willChangeValueForKey:

     原来的 setter

     didChangeValueForKey: 这个方法内部又会调用监听器的监听方法

 

版权声明:本文为CodingLife ✎ 原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/self-epoch/p/16316037.html