一:前言
KVO 是我们经常使用的键值观察者模式的一种实现 。大概功能是 比如有两个对象 A 和B B 观察了A的某个属性E ,当E发生变化的时候 B中收到回调 回调中 有新的 或者 旧的值 。 apple 原生给我们提供了这样的方式 。但是 其实系统提供的 KVO 是有很多不方便的地方例如 系统KVO 的问题 和 系统KVO 问题二 补充一点 重复添加 或者 重复移除KVO 都会直接造成 Crash 对开发者 非常不友好。 DEMO下载地址 https://gitee.com/DeLongYang/iOS_KVO。
二: 系统KVO 的实现原理
当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。自然,重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
深入的分析 可以参考 https://www.jianshu.com/p/e59bb8f59302 这篇文章中的 内容如下:
①NSKVONotifying_A 类剖析:在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_A 类,来实现当前类属性值改变的监听;
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A 的中间类,并指向这个中间类了。
(isa指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
—>我猜,这也是 KVO 回调机制,为什么都俗称KVO技术为黑魔法的原因之一吧:内部神秘、外观简洁。
②子类setter方法剖析:KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用 2 个方法:
被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
笔者 为了验证其 实现过程 在 DEMO 中 新建了 如图a所示的
的一组 。 然后在 ViewController 中注册了 A 对象的KVO ,并没有触发 KVO 。 系统提示
[general] KVO failed to allocate class pair for name NSKVONotifying_A, automatic key-value observing will not work for this class
也就是如果我们自己创建了这个 叫做NSKVONotifying_A 的类 那么系统无法实现这个KVO。 说明KVO的原理 确实动态创建了一个名称 为NSKVONotifying_A的类。
三 : 顺带提下 KVC
1.0 通过KVC 获取 和 设置 私有变量 哈哈
- (void)testKVCGetPrivateProperty
{
// 我们测试了 可以获取私有变量
KVCObject *kvcObj = [[KVCObject alloc] init];
NSString *name = [kvcObj valueForKeyPath:@"name"];
NSString *privateObj = [kvcObj valueForKeyPath:@"privatePro"];
int number = [[kvcObj valueForKeyPath:@"number"] intValue];
NSLog(@"name is:%@ --- privateObj is:%@ number is:%d",name,privateObj,number);
[kvcObj setValue:@"Hello" forKeyPath:@"privatePro"];
// 通过KVC 我们也可以 设置私有的变量的属性
NSLog(@"new privateObj is %@",[kvcObj valueForKeyPath:@"privatePro"]);
}
2.0 通过KVC 获取 多层的属性
// 我们测试一下 多层属性的获取
NSString *employ2Name = [employee1 valueForKeyPath:@"manager.employee2.name"];
NSLog(@"employee2 name is %@",employ2Name);
3.0 测试失败 使用KVC 获取集合对象 知道的同学可以 告诉我下
KVC还提供了集合操作的方法,直接获取到集合属性的同时还能对其进行求和,取平均数,求最大最小值等操作,如下为求和操作,具体可以到苹果官方文档详细了解。
// 这里造成了crash
// NSNumber *arrNumber = [manager valueForKeyPath:@"arrProperty.sum"];
// NSLog(@"arrNumber is %@",arrNumber);
四:如何使用 Runtime 来自己实现 KVO
详细的过程请参考 http://tech.glowing.com/cn/implement-kvo/
在 DEMO 中是 NSObject + KVO 这个分类 。
五:自定义实现的KVO 和 系统的对比
自定义的 KVO 的用法
第一步 : 注册KVO
- (void)secondRegisteCustomKVO
{
//
if (!self.message) {
self.message = [[SecondMessage alloc] init];
}
NSString *key = NSStringFromSelector(@selector(text));
[self.message PG_addObserber:self forKey:key withBlock:^(id observingObject, NSString *observedKey, id oldValue, id newValue) {
NSLog(@"%@ . %@ is now:%@",observingObject,observedKey,newValue);
dispatch_async(dispatch_get_main_queue(), ^{
self.textField.text = newValue;
});
}];
[self onCustomKVOButtonClick:nil];
}
第二步 :移除KVO
- (void)viewWillDisappear:(BOOL)animated
{
// 如果不移除掉的话会造成 内存泄漏
NSString *key = NSStringFromSelector(@selector(text));
NSLog(@"key is %@",key);
[self.message PG_removeObserver:self forKey:key];
}
经过 测试 无论 如何 添加还是删除 都不会 crash 移除后 也没有内存泄漏 !!
但是 还是 出现了问题 如果 被观察者的属性 是 基本数据类型 例如 int ,float 等的类型。笔者 发现 原因出在 NSObject+KVO分类中的 自定义的 setter 方法
#pragma mark ---- 很明显这个 setter 方法 和getter 方法 只是 写了id 类型 的没写 基础类型的 比如int float 等 笔者要加上这些类型 Thread 1: EXC_BAD_ACCESS (code=1, address=0x2)
static void kvo_setter(id self,SEL _cmd,id newValue)
如果 有朋友找到 了解决办法 欢迎 联系我 。
参考文章:
https://www.jianshu.com/p/e59bb8f59302
http://tech.glowing.com/cn/implement-kvo/
https://www.mikeash.com/pyblog/friday-qa-2010-11-6-creating-classes-at-runtime-in-objective-c.html