美文网首页
04 iOS底层原理 - KVC本质探究

04 iOS底层原理 - KVC本质探究

作者: 程序小胖 | 来源:发表于2020-01-30 18:14 被阅读0次

    老规矩,还是先来两个面试题:

    一,通过KVC修改属性会触发KVO吗?
    二,KVC的赋值和取值过程是怎样的?原理是什么?

    什么是KVC呢?

    KVC的全称是Key-Value Coding,即“键值编码”,可以通过一个key来访问某个属性

    废话不多说,直接上代码

    一,准备代码

    1. 创建Student类 继承自NSObject
    // 声明Student
    @interface Student : NSObject
    @property (nonatomic, assign) int height;
    @end
    
    // 声明Person
    @interface Person : NSObject
    {
        @public
        int _age;
        int _isAge;
        int age;
        int isAge;
    }
    // 测试 setAge: 和 _setAge: 调用流程时,注调这句代码
    @property (nonatomic, assign) int age;
    @property (nonatomic, strong) Student *student;
    @end
    
    // 实现 
    @implementation Student
    
    @end
    
    @implementation Person
    
    //- (int)getAge {
    //    return 18;
    //}
    //- (int)age {
    //    return 28;
    //}
    //- (int)isAge {
    //    return 38;
    //}
    //- (int)_age {
    //    return 48;
    //}
    
    - (void)setAge:(int)age {
        _age = age;
       NSLog(@"setAge == %d", age);
    }
    
    - (void)_setAge:(int)age {
        NSLog(@"_setAge == %d", age);
    }
    
    /*
    - (void)willChangeValueForKey:(NSString *)key {
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey");
    }
    
    - (void)didChangeValueForKey:(NSString *)key {
        NSLog(@"didChangeValueForKey-begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey-end");
    }
    
    // 默认返回YES,表示如果没有找到setKey:/_setKey:方法的话,
    // 会按照_key,_isKey,key,isKey的顺序搜索成员变量,设置成NO就不这样搜索
    + (BOOL)accessInstanceVariablesDirectly {
        return NO;
    }*/
    @end
    

    二,都有哪些KVC的方法呢?

    赋值:
    setter
    setValue:forKey
    setValue:forKeyPath

    取值:
    getter
    valueForKey:
    valueForKeyPath:

    注意:

    赋值和取值没有对应关系,
    但是用setValue:forKeyPath赋值了.路径的值,
    只能用getter和valueForKeyPath:方法获取
    

    直接上代码

    // KVC简单测试
    - (void)kvcTest1 {
        Person *person = [[Person alloc]init];
        person.age = 10;
        NSLog(@"%d", person.age);
        NSLog(@"%@", [person valueForKey:@"age"]);
        NSLog(@"%@", [person valueForKeyPath:@"age"]);
        
        [person setValue:[NSNumber numberWithInt:11] forKey:@"age"];
        [person setValue:@11 forKey:@"age"];
        NSLog(@"%d", person.age);
        NSLog(@"%@", [person valueForKey:@"age"]);
        NSLog(@"%@", [person valueForKeyPath:@"age"]);
    
        // forKeyPath 不仅可以修改当前类key的值,还可以修改其他类key的值
        // 用setValue:forKeyPath赋值了.路径的值,
        // 只能用getter和valueForKeyPath:方法获取
        person.student = [[Student alloc]init];
        [person setValue:@180 forKeyPath:@"student.height"];
        NSLog(@"%d", person.student.height);
        NSLog(@"%@", [person valueForKeyPath:@"student.height"]);
    }
    

    三,通过修改KVC的值可以触发KVO吗?

    先看测试代码:

    - (void)kvcTest2 {
        Person *person = [[Person alloc]init];
        
        // 添加KVO
        [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
        
        // 通过KVC修改属性
        [person setValue:@18 forKey:@"age"];
      
        // 移除监听
        [person removeObserver:self forKeyPath:@"age"];
    }
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"%@-%@-%@", keyPath, object, change);
    }
    

    分两种情况:

    1. setKey:和_setKey:方法都存在,或者至少存在一种

    在Person类里面把这两个方法打开测试

    - (void)setAge:(int)age {
        _age = age;
        NSLog(@"setAge == %d", age);
    }
    - (void)_setAge:(int)age {
        NSLog(@"_setAge == %d", age);
    }
    

    打印结果:

    age-<Person: 0x600003028220>-{
              kind = 1;
              new = 18;
              old = 10;
    }
    

    通过打印结果,发现确实可以通过KVC修改属新触发KVO

    2. 没有setKey:和_setKey:方法,只有成员变量,还会触发KVO吗?

    在Person类里面把这两句注掉

    //- (void)setAge:(int)age {
    //    _age = age;
    //    NSLog(@"setAge == %d", age);
    //}
    //
    //- (void)_setAge:(int)age {
    //    NSLog(@"_setAge == %d", age);
    //}
    

    会的,首先会触发accessInstanceVariablesDirectly方法,返回yes,
    然后按照_key,_isKey,key,isKey的顺序搜索成员变量,并赋值。
    此时的KVC大概实现流程是这样的:

    1. 调用willChangeValueForKey
    2. 修改_key的值
    3. 调用didChangeValueForKey,didChangeValueForKey 中实现了KVO的监听事件

    三,KVC赋值和取值原理

    先看两张图:

    1. setValue:forKey:赋值原理
    image.png

    按照示意图,我这里解释下KVC取值的原理:
    setValue:forKey:的内部实现,可以分以下几种情况:

    1>. 有属性值key

    不管实现不实现setKey:方法,默认都是实现了该方法的,因此_setKey:不管实现还是不实现,_setKey:方法都不会被调用,同样也都不会去访问_key,_isKey,key,isKey这些成员变量

    2>. 没有属性值key

    a> 如果 setKey:和_setKey: 方法都实现了,那么只会调用setKey:方法,但不会去访问_key,_isKey,key,isKey这些成员变量;
    b> 如果 setKey:和_setKey: 只实现其中一个方法,那么就只调用实现的那个方法,但不会去访问_key,_isKey,key,isKey这些成员变量。

    3>. 没有属性值key,setKey:和_setKey:方法也没实现

    accessInstanceVariablesDirectly默认返回YES,表示如果没有找到setKey:/_setKey:方法的话,会按照_key,_isKey,key,isKey的顺序搜索成员变量。

    4>. 其他情况

    没有_key,_isKey,key,isKey成员变量,或者accessInstanceVariablesDirectly返回NO,那么,就直接抛出异常NSUnknownKeyException。

    总结:通过1和2看出,其实和属性值key存不存在没关系,只要存在setKey:或_setKey:方法,就都不会去访问_key,_isKey,key,isKey这些成员变量

    2. valueForKey:取值原理
    image.png

    从上图可以看出,基本的流程和赋值是一样的,需要注意两点:

    1. 第一个红框框住的方法,是四个方法依次是:
      getKey、key、isKey、_key
    2. 第二个红框,如果没有找到相关的方法,
      并且accessInstanceVariablesDirectly返回NO时,报的错误和赋值时的不一样。
      调用:valueForUndefinedKey:并抛出错误NSUnknowKeyExcention

    四,现在看看文章开头的面试题

    一,通过KVC修改属性会触发KVO吗?
    会触发KVO,因为KVC在本质上是调用了
    willChangeValueForKey:和didChangeValueForKey:的

    二,KVC的赋值和取值过程是怎样的?原理是什么?
    这个过程就不说了,上面两幅图记住比啥都好

    结束语:非常感谢,MJ老师,和Hank老师,收获挺多

    相关文章

      网友评论

          本文标题:04 iOS底层原理 - KVC本质探究

          本文链接:https://www.haomeiwen.com/subject/xsoithtx.html