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: 这个方法内部会调用监听器的监听方法