iOS进阶之KVO底层原理

前言

KVO作为iOS一个设计模式,监听对象属性变化。通过属性变化来做出一些处理。那么KVO底层原理是什么?相信大家前期都不怎么关注过,知其然知其所以然,所以我也研究讲一下KVO底层实现原理。

思考

  1. KVO 底层实现是什么?
  2. 如何手动触发 KVO?
  3. 修改成员变量的值会触发 KVO 吗?
  4. KVC 赋值会触发 KVO 吗?

把这几个问题都整明白,KVO掌握的也就差不多了。

KVO的底层实现

首先创建一个对象Person,在Person.h声明一个属性

  1. 创建两个对象,一个对象KVO监听,一个对象不监听,查看一下他们的监听前后地址

     Person *p1 = [[Person alloc] init];
     Person *p2 = [[Person alloc] init];
     NSLog(@"KVO监听前:p1:%@ p2:%@", p1, p2);
    
     [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|   NSKeyValueObservingOptionNew context:nil];
     
     NSLog(@"KVO监听后:p1:%@ p2:%@",p1, p2);
    

打印的结果:

     KVO监听前:p1:<Person: 0x600003810380> p2:<Person: 0x6000038103a0>
     KVO监听后:p1:<Person: 0x600003810380> p2:<Person: 0x6000038103a0>

打印的结果说明监听前后地址没啥变化。

  1. 那就查看一下监听前后对象的类对象有没有变化,使用object_getClass来获取类对象,只有引入#import <objc/runtime.h>才能使用object_getClass方法

     Person *p1 = [[Person alloc] init];
     Person *p2 = [[Person alloc] init];
     
     id class1 = object_getClass(p1);
     id class2 = object_getClass(p2);
     NSLog(@"KVO监听前:p1:%@ p2:%@",class1, class2);
     
     [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
     
     class1 = object_getClass(p1);
     class2 = object_getClass(p2);
    
     NSLog(@"KVO监听后:p1:%@ p2:%@",class1, class2);
    

    打印结果是:

     KVO监听前:p1:Person p2:Person
     KVO监听后:p1:NSKVONotifying_Person p2:Person
    

    发现监听后的p1是的类对象变成NSKVONotifying_Person,怎么监听后Person变成NSKVONotifying_Person。NSKVONotifying_Person是什么东东?

  2. 进一步窥探 KVO 添加前后的变化,打印 setName 方法实现IMP指针有没有发生改变,我们知道同一个方法的实现,IMP地址是不变的.

     Person *p1 = [[Person alloc] init];
     Person *p2 = [[Person alloc] init];
      //setName的IMP
     IMP imp1 = [p1 methodForSelector:@selector(setName:)];
     IMP imp2 = [p2 methodForSelector:@selector(setName:)];
     NSLog(@"KVO监听前:imp1:%p imp2:%p",imp1, imp2);
     
     [p1 addObserver:self forKeyPath:@"name" 
     options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    
     //setName的IMP
     imp1 = [p1 methodForSelector:@selector(setName:)];
     imp2 = [p2 methodForSelector:@selector(setName:)];
     NSLog(@"KVO监听后:imp1:%p imp2:%p",imp1, imp2);
    

    打印结果:

     KVO监听前:imp1:0x10fa01640 imp2:0x10fa01640
     KVO监听后:imp1:0x10fd5a63a imp2:0x10fa01640
    

    从打印结果来看连 setName方法都不一样了, 为了一探究竟 ,对上边的 NSKVONotifying_Person 和 添加 KVO 之后的 imp 指针进行进一步研究.首先 在 lldb 上输入 imp1和 imp2

     (lldb) po imp1
    
     (Foundation`_NSSetObjectValueAndNotify)
     
     (lldb) po imp2
     (KVO底层原理`-[Person setName:] at Person.h:13)
    

    发生了 imp1 方法实现在 Foundation 框架里的 _NSSetObjectValueAndNotify 函数中 ,而 imp2 则调用了 Person setName 方法.
    添加了 KVO 之后 p1 修改 name 值之后 不再调用 Person 的 setName方法 ,而 p2没有添加 kvo 监听 依然正常调用 setName:方法 ,由此可以得出 p1 添加完 KVO 监听后 系统修改了默认方法实现,那么既然没有调用 setName: 方法 为什么 p1.name 的值也发生了改变?

  3. NSKVONotifying_Person和 Person 之间的关系,打印一下各自的父类

      Person *p1 = [[Person alloc] init];
      Person *p2 = [[Person alloc] init];
    
     //获取p1 p2类型对象
     id class1 = object_getClass(p1);
     id class2 = object_getClass(p2);
     id superClass1 = class_getSuperclass(class1);
     id superClass2 = class_getSuperclass(class2);
    
     NSLog(@"KVO监听前:superclass1:%@ superclass2:%@",superClass1, superClass2);
    
     [p1 addObserver:self forKeyPath:@"name" 
     options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    
     class1 = object_getClass(p1);
     class2 = object_getClass(p2);
     superClass1 = class_getSuperclass(class1);
     superClass2 = class_getSuperclass(class2);
    
     NSLog(@"KVO监听后:superclass1:%@ superclass2:%@",superClass1, superClass2);
    

    打印结果:

     KVO监听前:superclass1:NSObject superclass2:NSObject
     KVO监听后:superclass1:Person superclass2:NSObject
    

    通过打印 NSKVONotifying_Person 的 superclass 和 Person 的 superclass 可以得出, NSKVONotifying_Person是一个 Person 子类,那么为什么苹果会动态创建这么一个 子类呢? NSKVONotifying_Person 这个子类 跟 Person 内部有哪些不同呢 ?

  4. 输出下 Person 和 NSKVONotifying_Person 内部的方法列表 和 属性列表 ,看看NSKVONotifying_Person 子类都添加了哪些方法和属性.

     Person *p1 = [[Person alloc] init];
     Person *p2 = [[Person alloc] init];
    
     //获取p1 p2类型对象
     id class1 = object_getClass(p1);
     id class2 = object_getClass(p2);
    
     [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    
     class1 = object_getClass(p1);
     class2 = object_getClass(p2);
     NSString *methodList1 = [self printPersonMethods:class1];
     NSString *methodList2 = [self printPersonMethods:class2];
    
     NSLog(@"KVO监听后:methodlist1:%@ \n methodlist2:%@",methodList1, methodList2);
    
     - (NSString *) printPersonMethods:(id)obj {
    
         unsigned int count = 0;
         Method *methods = class_copyMethodList([obj class],&count);
         NSMutableString *methodList = [NSMutableString string];
         [methodList appendString:@"[\n"];
         for (int i = 0; i<count; i++) {
             Method method = methods[I];
             SEL sel = method_getName(method);
             [methodList appendFormat:@"%@",NSStringFromSelector(sel)];
             [methodList appendString:@"\n"];
     }
    
         [methodList appendFormat:@"]"];
    
         free(methods);
    
         return methodList;
     }
    

打印结果:

KVO监听后:methodlist1:[
    setName:
    class
    dealloc
    _isKVOA
    ] 
    methodlist2:[
    .cxx_destruct
    name
    setName:
    ]
  • 发现监听后的methodlist1方法内部也有setName:
  • 重写了class 和 dealloc 方法,多了个_isKVOA方法。
  • KVO 在实现中通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类。
  • 重写了 setName 方法 ,那么 setName 内部一定是做了注册通知的事情,当值改变才会触发 observeValueForKeyPath 监听方法.

继续探究 NSKVONotifying_Person 子类 重写 setName 都做了什么?

  • 前面打印了监听后p1的setName的IMP方法变成(Foundation`_NSSetObjectValueAndNotify)
  • setName方法内部应该是调用了 Foundation 的_NSSetObjectValueAndNotify 函数
  • 在 _NSSetObjectValueAndNotify实现触发监听
    • 首先会调用 willChangeValueForKey
    • 其次给 name 属性赋值
    • 然后调用 didChangeValueForKey方法,该内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
    • 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .

由于源码无法查看,大概设想一下NSKVONotifying_Person的实现代码:

- (void)setName:(NSString *)name {

    _NSSetObjectValueAndNotify()
}

- (void)willChangeValueForKey:(NSString *)key {
    
    [super willChangeValueForKey:key];
}

- (void)didChangeValueForKey:(NSString *)key {

    [super didChangeValueForKey:key];
    //observer观察者 
    [observer observeValueForKeyPath:name];
}

void _NSSetObjectValueAndNotify() {
    
    //触发监听
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];]
}

所以我们依然可以通过重写Person 的 willChangeValueForKey 和 didChangeValueForKey 验证我们的猜想.打断点调试一下

    //每次属性改变,第二步
    - (void)setName:(NSString *)name {
        
        _name = [name copy];
    }
    
    //每次属性改变,先走这一步
    - (void)willChangeValueForKey:(NSString *)key {

        [super willChangeValueForKey:key];
    }

    //每次属性改变,第三步
    - (void)didChangeValueForKey:(NSString *)key {

        [super didChangeValueForKey:key];
    }
    
    //这个方法不是在Person,是在哪个类监听的p1
    //每次属性值改变,第四步会走observeValueForKeyPath
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {

NSLog(@"new -- %@",change[NSKeyValueChangeNewKey]);
    }
  • 监听时候Runtime动态的创建Person的子类NSKVONotifying_Person
  • 重写了setName方法,在该方法里面调用_NSSetObjectValueAndNotify()方法
  • 重写class方法返回的还是Person类
  • 重写了dealloc 和 class 方法 是为了做一些KVO 释放内存和隐藏外界对于NSKVONotifying_Person 子类的存在
  • 这就是我们打印[p1 class] [p2 class]都显示Person,让我们误以为 Person 没有发生变化 。
KVO实现原理图.png

问题解答

  1. 如何手动的触发KVO?

    • 关闭该属性的自动触发

    • 实现willChangeValueForKey didChangeValueForKey成对出现才能手动触发KVO

    • 一个被观察属性发生改变之前,willChangeValueForKey: 一定会被调用,这就会记录旧的值

    • 当执行didChangeValueForKey这个方法就会触发监听,这个会记录新的值

        - (void)setName:(NSString *)name            {
      
            if ([_name isEqualToString:name]) return;
            //手动触发 关闭自动监听之后,如果不写下面三行代码是不会触发监听的,
            //要是打开自动监听的话,willChangeValueForKey didChangeValueForKey可以不用不写的,系统会自动加上
            [self willChangeValueForKey:@"name"];
            _name = name;
            [self didChangeValueForKey:@"name"];
        }
      
        + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
      
            //对name属性关闭自动监听
            if ([key isEqualToString:@"name"]) {
      
                return NO;
            } else {
                //对其他属性没影响
                return [super automaticallyNotifiesObserversForKey:key];
            }
        }
      
  2. 修改成员变量的值会触发 KVO 吗?

    • 修改成员变量不会触发,因为不会触发setter方法,因此不会触发KVO相关的代码
  3. KVC 赋值会出发 KVO 吗?

    • 会触发KVO,KVC 对属性赋值时候 是会在这个类里边去查找setName、_name、_isName、name、isName等方法的
    • 最终会调用属性的setter方法,所以KVO还是会被触发的.
    • KVO在调用存取方法之前总是调用 willChangeValueForKey ,之后总是调用 didChangeValueForkey: 怎么做到的呢?答案是通过 isa 混写(isa-swizzling)

使用KVO注意问题

  • 如果同一个属性在不同的类(比如父类)均设置了KVO,那么可通过定义不同的context(类名)来区分,并且相应移除。
  • 对同一个KVO属性多次移除,会导致程序crash。
  • 使用类别(category)向一个已存在的类添加一个属性,可以用KVO观察其变化。
  • 我们可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context为@"ThisIsMyKVOContextNotSuper";然后在dealloc中remove observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的kvo,而不是父类中的kvo,避免二次remove造成crash。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 问题 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 如何手动触发KVO ? 首先需要了解KVO...
    hjltony阅读 574评论 0 2
  • iOS底层原理总结 - 探寻KVO本质 对小码哥底层班视频学习的总结与记录。 面试题:iOS用什么方式实现对一个对...
    爱吃兔兔的胡萝卜吖阅读 292评论 0 1
  • 面试问题: · iOS用什么方式实现对一个对象的KVO? · 如何手动触发KVO? 我们通过以下几个点来寻找这两个...
    高思阳阅读 239评论 0 1
  • 对小码哥底层班视频学习的总结与记录。面试题部分,通过对面试题的分析探索问题的本质内容。 问题iOS用什么方式实现对...
    xx_cc阅读 10,753评论 26 65
  • 无题 细雨蒙蒙湖里洒,飘零红叶向天涯。 伊人离去烟水冷,何故薄情飞泪花。 江南一景(一) 两岸小楼时隐现,红灯高挂...
    6617618811a8阅读 407评论 2 7