底层探索--KVO、KVC的本质

KVO

原理

  • 基本使用

      //添加监听
      [self addObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>]
      //参数详解:
      Observer:观察者对象,一般self;
      keyPath:观察的属性,字符串属性,一般采用系统的反射机制 NSStringFromSelector(@selector(keypath))
      options:枚举
          NSKeyValueObservingOptionNew //接受新值,默认
          NSKeyValueObservingOptionOld //接受旧值
          NSKeyValueObservingOptionInitial //在注册时立即接受一次回调,改变是也会
          NSKeyValueObservingOptionPrior //改变之前发送一次,改变之后再发一次
          可采用如下形式:NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
      
      //实现监听方法
      - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
      //参数详解:
      keyPath:监听属性的名称
      Object:被观察对象
      Change:字典,key一般为:new、old
      Context:入口,用于传值,添加监听时原样传来的
    
      //移除监听对象
      [self removeObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#>]
    
  • 对象添加监听之后,运行时新增了类NSKVONotifying_对象的类名,并会重写对应的set方法:

    NSKVONotifying_对象的类名的组成结构:
    isa
    superclass
    set对象方法---内部调用 _NSSet*ValueAndNotify(如:_NSSetObjectValueAndNotify,*代表监听对象对应的类型,如:int double object等)--详情见下图
    class:[类名 class],重写后返回本身类(屏蔽内部实现,隐藏实际类NSKVONotifying_对象的类名的存在),
    dealloc:做一些销毁操作,
    _isKVOA
    
    
    //实际证明方法如下:
    
    //监听方法
    - (void) observeFunction {
        NSLog(@"class = %@, metaclass = %@ \n",object_getClass(self.model),object_getClass(object_getClass(self.model)));
        //class = ZTKVORootM, metaclass = ZTKVORootM
        
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
        [self.model addObserver:self forKeyPath:@"name" options:options context:nil];
        
        //打印 类对象 和 元类对象(注意:不能使用[self.model class],因为内部已经被重写,不是实际isa指向的类,需用运行时直接获取才是真正的类)
        NSLog(@"class = %@, metaclass = %@ \n",object_getClass(self.model),object_getClass(object_getClass(self.model)));
        //打印结果:class = NSKVONotifying_ZTKVORootM, metaclass = NSKVONotifying_ZTKVORootM
        
        //打印方法
        NSLog(@"Method = %p",[self.model methodForSelector:@selector(setName:)]);
        //打印结果:p (IMP)0x223413e04
        //(IMP) $3 = 0x0000000223413e04 (Foundation`_NSSetObjectValueAndNotify)
        
        //证明:添加监听后,运行时新增了类NSKVONotifying_ZTKVORootM,并重新了监听的属性的set方法
        
        //打印监听后的类 和 没监听类的对象方法
        ZTKVORootM *notModel = [[ZTKVORootM alloc]init];
        
        [self getAllMethodListWithClass:object_getClass(notModel)];
        [self getAllMethodListWithClass:object_getClass(self.model)];
        /** 打印如下
         ZTKVORootM下所有的方法列表:
         .cxx_destruct
         name
         setName:
    
         2020-09-23 14:25:06.560550+0800 testApp[862:180083] NSKVONotifying_ZTKVORootM下所有的方法列表:
         setName:
         class
         dealloc
         _isKVOA
         */
    }

    
    //添加依赖:返回(对象中,需监听的属性)
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"person"]) {
            //此时如果person这个对象中的name或age值改变就会触发
            keyPaths = [[NSSet alloc] initWithObjects:@"person.name",@"person.age", nil];
        }
        return keyPaths;
    }
    
    //KVO是否 开启自动监听(值变后自动通知消息)
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        return YES;
    }
    
    //获取类-所有的对象方法
    - (void) getAllMethodListWithClass:(Class)class {
        unsigned int count = 0;
        Method *methodList = class_copyMethodList(class, &count);
        
        NSMutableString *methodStr = [[NSMutableString alloc] init];
        for (NSInteger i = 0; i < count; i++) {
            Method method = methodList[i];
            SEL sel = method_getName(method);
            [methodStr appendFormat:@"%@ \n",NSStringFromSelector(sel)];
        }
        NSLog(@"%@下所有的方法列表:\n %@",NSStringFromClass(class),methodStr);
    }
KVO重新的set方法.png
  • set内部实现如下图
KVO的set内部实现.png

面试题

1、 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)

2、 如何手动触发KVO?

手动调用willChangeValueForKey:和didChangeValueForKey:

3、 直接修改成员变量会触发KVO么?

不会触发KVO,只有调用了setter方法才会触发

KVC

  • 主要方法

      //get方法
      - (id)valueForKeyPath:(NSString *)keyPath;
      - (id)valueForKey:(NSString *)key;
      - (id)valueForUndefinedKey:(NSString *)key;
      
      //set方法
      - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
      - (void)setValue:(id)value forKey:(NSString *)key;
      - (void)setValue:(id)value forUndefinedKey:(NSString *)key;  
    
  • - (void)setValue:(id)value forKey:(NSString *)key;方法解析:

    • 1.按照setKey: or _setKey: 顺序查方法赋值,找到直接调用,否则往下 走2;
    • 2.查找+ (BOOL)accessInstanceVariablesDirectly;//是否允许查找成员变量,默认返回YES返回值:YES-走3,NO-走4
    • 3.按照_key _isKey key isKey顺序查找成员变量,找到了直接取值,否则往下 走4
    • 4.- (void)setValue:(id)value forUndefinedKey:(NSString *)key; 并抛出异常NSUnknownKeyException
KVC的set方法原理.png
  • - (id)valueForKey:(NSString *)key;方法解析:
    • 1.按照getKeykeyisKey_key: 顺序查方法,找到直接调用,否则往下 走2;
    • 2.查找+ (BOOL)accessInstanceVariablesDirectly;//是否允许查找成员变量,默认返回YES返回值:YES-走3,NO-走4
    • 3.按照_key _isKey key isKey顺序查找成员变量,找到了直接取值,否则往下 走4
    • 4.- (id)valueForUndefinedKey:(NSString *)key; 并抛出异常NSUnknownKeyException
KVC的get方法原理.png

面试题

1、 通过KVC修改属性会触发KVO么?

会触发KVO。
原理:不论有没有实现setter方法,都会触发KVO,内部应该实现了:调用willChangeValueForKey:和didChangeValueForKey:

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

推荐阅读更多精彩内容