iOS KVO 实现原理 和 自己实现KVO

一:前言

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所示的 

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335