iOS进阶 - KVO实现原理

▶ KVO 实现原理

我们在 Person 中声明 age属性;在 ViewController 中创建两个 Person 的实例对象,并将其中一个添加观察者,监听 age属性

// - Person.h

#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic,assign)int age;
@end

// - Person.m

#import "Person.h"
@implementation Person
@synthesize age = _age;
// --------------  没有添加观察者的对象,点语法依旧调用原来的 setter方法----------------
- (void)setAge:(int)age{
    _age = age;
}
- (int)age{
    return _age;
}
//-------------- 添加了观察者的对象,调用 setter方法 的实现如下---------------------
// 这些都是在 runtime 中动态生成!下面是伪代码,模拟 KVO 实现流程

// 在 setter方法 中会执行 __NSSetInValueAndNotify函数,它是 C语言 私有函数
//- (void)setAge:(int)age{
//     __NSSetInValueAndNotify();
//}
//

// __NSSetInValueAndNotify 函数
//void __NSSetInValueAndNotify(){
//
//   // 首先调用 willChangeValueForKey
//   [self willChangeValueForKey:@"age"];
//
//   // 其次是初始化实例变量
//   [super setAge:age];
//
//   // 最后调用 didChangeValueForKey
//   [self didChangeValueForKey:@"age"];
//
//}
//

// didChangeValueForKey 会拿到这个 observer 进而触发监听方法
//- (void)didChangeValueForKey:(NSString *)key{
//    // 触发监听
//    [observer observeValueForKeyPath:@"age" ofObject:self change:@{} context:nil]
//}

//-------------- 重写方法:去验证监听方法是在 didChangeValueForKey 中触发的 ------------
- (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");
}
@end

// - ViewController.m

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

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

▶ KVO 流程分析图

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

▶ 手动启用 KVO

如何手动启动 KVO?只需要在已添加监听的对象中手动调用 willChangeValueForKey、didChangeValueForKey 两方法即可,二者缺一不可

// - Person.h

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

// - ViewController.m

#import "ViewController.h"
#import "Person.h"
#import <objc/runtime.h>
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor cyanColor];

    Person *p1 = [Person new];
    p1 ->_name = @"123";
    [p1 addObserver:self forKeyPath:@"_name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"传参"];

    // 手动启用 KVO
    [p1 willChangeValueForKey:@"_name"];
    p1 ->_name = @"456";
    [p1 didChangeValueForKey:@"_name"];

    [p1 removeObserver:self forKeyPath:@"_name"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"keyPath = %@    object = %@  change = %@",keyPath,object,change);
}

@end

日志信息:手动启用的话,直接赋值同样可以实现监听的目的

▶ 结语

KVO 工作原理

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

B. 子类拥有自己的 setter方法 实现

    先会调用 willChangeValueForKey:

    其次是原来的 setter方法

    最后是 didChangeValueForKey: 这个方法内部会调用监听器的监听方法

 

posted on 2022-05-27 01:18  低头捡石頭  阅读(30)  评论(0编辑  收藏  举报

导航