iOS中的KVC那点儿事

IT永振 2019-09-09 09:48:06 1196

本文首发于个人博客

前言

  • KVC是Key Value Coding的简称。它是一种可以通过字符串的名字(key)来访问类属性的机制。而不是通过调用Setter、Getter方法访问。KVC的方法定义在Foundation/NSKeyValueCoding中。
  • KVC和KVO都属于键值编程而且底层实现机制都是isa-swizzing
  • 常见的API有

  • -(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
  • -(void)setValue:(id)value forKey:(NSString *)key;
  • -(id)valueForKeyPath:(NSString *)keyPath;
  • -(id)valueForKey:(NSString *)key;
  • KVC基本使用

  • 定义一个YZPerson类,有个 name 属性
  • @interface YZPerson : NSObject
    @property (nonatomic,strong) NSString *name;
    @end
  • ViewController 控制器中,如下使用
  • #import "ViewController.h"
    #import "YZPerson.h"
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        YZPerson *person = [[YZPerson alloc]init];
         // 赋值
        [person setValue:@"jack" forKey:@"name"];
        // 取值
        NSLog(@"%@",[person valueForKey:@"name"]);
    }
    
    @end
  • 结果为
  • KVCDemo[25838:347883] jack

    赋值 setValue:forKey:的原理

    1. 按照 setKey:_setKey: 的顺序查找方法
    2. 如果找到了方法,就传递参数,调用方法
    3. 如果没有找到,查看 accessInstanceVariablesDirectly 方法的返回值
    4. 如果accessInstanceVariablesDirectly 返回值为 NO 调用 setValue:forUndefinedKey: 并抛出异常 NSUnknownKeyException
    5. 如果accessInstanceVariablesDirectly 返回值为 YES 按照_key_isKeykeyisKey的顺序查找成员变量
    6. 如果找到了成员变量,就直接赋值。
    7. 如果 _key_isKeykeyisKey的顺序没有查找到成员变量就调用setValue:forUndefinedKey: 并抛出异常 NSUnknownKeyException

    证明赋值

    先证明 按照 setKey:_setKey: 的顺序查找方法

    YZPerson.hYZPerson.m 如下

    
    // 只有name属性,没有age
    @interface YZPerson : NSObject
    @property (nonatomic,strong) NSString *name;
    @end
    
    @implementation YZPerson
    - (void)setAge:(int)age
    {
        NSLog(@"setAge: - %d", age);
    }
    
    - (void)_setAge:(int)age
    {
        NSLog(@"_setAge: - %d", age);
    }
    

    调用地方

     YZPerson *person = [[YZPerson alloc]init];
        // 赋值
     [person setValue:@20 forKey:@"age"];
    

    打印结果是

    KVCDemo[26389:357519] setAge: - 20

    说明调用来的是setAge: 那如果 去掉 setAge:

    
    // 只有name属性,没有age
    @interface YZPerson : NSObject
    @property (nonatomic,strong) NSString *name;
    @end
    
    @implementation YZPerson
    - (void)setAge:(int)age
    {
        NSLog(@"setAge: - %d", age);
    }
    
    - (void)_setAge:(int)age
    {
        NSLog(@"_setAge: - %d", age);
    }
    

    结果为:

    KVCDemo[26594:360894] _setAge: - 20

    证明了 按照 setKey:_setKey: 的顺序查找方法

    证明 accessInstanceVariablesDirectly

    1. 如果accessInstanceVariablesDirectly 返回值为 NO 调用setValue:forUndefinedKey: 并抛出异常 NSUnknownKeyException
    2. 如果accessInstanceVariablesDirectly 返回值为 YES 就去查找成员变量,就直接赋值。
  • 我们在 YZPerson.h 中定义四个成员变量, YZPerson.m中 只有accessInstanceVariablesDirectly 并返回NO
  • 
    @interface YZPerson : NSObject
    {
    @public
            int age;
            int isAge;
            int _isAge;
            int _age;
    }
    @property (nonatomic,strong) NSString *name;
    @end
    
    #import "YZPerson.h"
    
    @implementation YZPerson
    
    // 默认的返回值就是YES
    + (BOOL)accessInstanceVariablesDirectly
    {
        return NO;
    }
    @end
    

    运行报错:找不到 key值 age

     KVCDemo[27163:369895] *** Terminating app due to uncaught exception 
     'NSUnknownKeyException', reason: '[<YZPerson 0x600003a5e700> 
     setValue:forUndefinedKey:]: this class is not key value 
     coding-compliant for the key age.'
  • 我们把YZPerson.m中 只有accessInstanceVariablesDirectly 返回YES
  • 运行结果:

    KVCDemo[27385:373752] 20

    证明是按照_key_isKeykeyisKey的顺序查找成员变量

    代码还是上面的代码,打断点,然后LLDB调试

    (lldb) po person->_age
    20
    
    (lldb) po person->_isAge
    <nil>
    
    (lldb) po person->age
    <nil>
    
    (lldb) po person->isAge
    <nil>
    

    如果去掉成员变量_age

    结果为

    
    (lldb) po person->_isAge
    20
    
    (lldb) po person->age
    <nil>
    
    (lldb) po person->isAge
    <nil>
    

    同理其他的几种情况,读者可自行尝试 demo

    KVC与KVO

    通过关于KVO看这篇就够了 我们知道

    KVO的本质

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

    我们在 YZPerson.h中书写如下代码

    @interface YZPerson : NSObject
    {
    @public
            int age;
            int isAge;
            int _isAge;
            int _age;
    }
    @property (nonatomic,strong) NSString *name;
    @end

    我们知道,成员变量是不会生成set 和 get方法的 然后 YZPerson.m中书写如下代码

    #import "YZPerson.h"
    
    @implementation YZPerson
    
    // 默认的返回值就是YES
    + (BOOL)accessInstanceVariablesDirectly
    {
        return YES;
    }
    @end
    

    在VC中设置KVO监听

    
    #import "ViewController.h"
    #import "YZPerson.h"
    @interface ViewController ()
    @property (nonatomic,strong) YZPerson *person;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.person = [[YZPerson alloc]init];
    
        // 添加KVO监听
        [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
    
        // 通过KVC修改age属性
        [self.person setValue:@10 forKey:@"age"];
    
        // 取值
        NSLog(@"取值为:%@",[self.person valueForKey:@"age"]);
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        NSLog(@"observeValueForKeyPath - %@", change);
    }
    
    -(void)dealloc{
        // 移除KVO监听
        [self.person removeObserver:self forKeyPath:@"age"];
    }
    

    输出结果为:

    KVCDemo[28271:388786] observeValueForKeyPath - {
        kind = 1;
        new = 10;
        old = 0;
    }
    KVCDemo[28271:388786] 取值为:10

    可知,其实在系统内部,是调用了

    - willChangeValueForKey:
    
    - didChangeValueForKey:

    进一步验证

    然后 YZPerson.m中书写如下代码

    #import "YZPerson.h"
    
    @implementation YZPerson
    
    - (void)willChangeValueForKey:(NSString *)key
    {
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey - %@", key);
    }
    
    - (void)didChangeValueForKey:(NSString *)key
    {
        NSLog(@"didChangeValueForKey - begin - %@", key);
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey - end - %@", key);
    }
    // 默认的返回值就是YES
    + (BOOL)accessInstanceVariablesDirectly
    {
        return YES;
    }
    @end
    

    输出结果为:

    
    KVCDemo[28392:390730] willChangeValueForKey - age
    KVCDemo[28392:390730] didChangeValueForKey - begin - age
    KVCDemo[28392:390730] observeValueForKeyPath - {
        kind = 1;
        new = 10;
        old = 0;
    }
    KVCDemo[28392:390730] didChangeValueForKey - end - age
    KVCDemo[28392:390730] 取值为:10

    所以,足以说明,KVC内部调用了 willChangeValueForKeydidChangeValueForKey

    GNU验证

    文章发出去之后,YiHuaXie 评论说

    我觉得你的那个KVC能触发KVO的理由说的比较牵强,willChangeValueForKey 和 didChangeValueForKey的调用只能证明触发KVO的时候会调用这两个方法,并不能证明KVC一定也能调用了,只是说从结果上看上去是那么回事

    因为苹果官方对 setValue: forKey:的源码是不开源的,验证的时候,是从结果来反推回去的,因为调用了KVC之后,确实会打印willChangeValueForKeydidChangeValueForKey,算是间接验证吧。

    之后我又查了GNUstep的源码

    简单说下GNUstep 这是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍,虽然不是官方源码,但是还是有一定的参考价值的

    GNUstep,GNU计划的项目之一。它将Cocoa(前身为NeXT的OpenStep)Objective-C软件库,部件工具箱(widget toolkits)以及其上的应用软件,以自由软件方式重新实现。它能够运行在类Unix操作系统上,也能运作在Microsoft Windows上。 ---- 维基百科

    GNUstep的官方网址为GNUstep

    下载gnustep-base-1.26.0工程,在 NSKeyValueObserving.m中有如下代码

    - (void) setValue: (id)anObject forKey: (NSString*)aKey
    {
      Class     c = [self class];
      void      (*imp)(id,SEL,id,id);
    
      imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];
    
      if ([[self class] automaticallyNotifiesObserversForKey: aKey])
        {
          [self willChangeValueForKey: aKey];
          imp(self,_cmd,anObject,aKey);
          [self didChangeValueForKey: aKey];
        }
      else
        {
          imp(self,_cmd,anObject,aKey);
        }
    }

    可以作为setValue: forKey会调用willChangeValueForKeydidChangeValueForKey的佐证吧。

    取值 valueForKey:的原理

    1. 按照getKeykeyisKey_key的顺序查找方法
    2. 如果找到了,就直接调用
    3. 如果没找到,就查看accessInstanceVariablesDirectly 方法的返回值
    4. 如果accessInstanceVariablesDirectly 返回值为 NO 调用valueForUndefinedKey:并抛出异常NSUnknownKeyException
    5. 如果accessInstanceVariablesDirectly 返回值为 YES 按照_key_isKeykeyisKey的顺序查找成员变量
    6. 如果找到了成员变量,就直接取值。
    7. 如果 _key_isKeykeyisKey的顺序没有查找到成员变量就调用valueForUndefinedKey:并抛出异常NSUnknownKeyException

    验证取值

  • 取值和赋值的大体逻辑基本一致
  • 然后 YZPerson.m中书写如下代码

    #import "YZPerson.h"
    
    @implementation YZPerson
    
    - (int)getAge
    {
        return 11;
    }
    
    - (int)age
    {
        return 12;
    }
    
    - (int)isAge
    {
        return 13;
    }
    
    - (int)_age
    {
        return 14;
    }
    // 默认的返回值就是YES
    + (BOOL)accessInstanceVariablesDirectly
    {
        return YES;
    }
    @end
    

    VC中如下代码

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.person = [[YZPerson alloc]init];
    
        // 取值
        NSLog(@"取值为:%@",[self.person valueForKey:@"age"]);
    }
    

    结果为

    KVCDemo[29145:403008] 取值为:11

    如果去掉

    - (int)getAge
    {
        return 11;
    }

    则,输出结果为12。

    上面验证了 按照getKeykeyisKey_key的顺序查找方法

    其他的验证逻辑,和赋值验证过程一致,就不赘述了。

    本文相关代码github地址 github

    本文参考资料:

    Runtime源码

    iOS底层原理

    ×