iOS 之KVC

458 阅读5分钟

说起KVC相比大家都很熟悉吧,在这也就不多解释了。我们直接看官方文档

image.png

其大致意思就是:键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议来间接访问其属性。既可以通过一个字符串key来访问某个属性。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。

API 方法

 // 通过setValue中key来设值
 - (void)setValue:(nullable id)value forKey:(NSString *)key;
 // 通过valueForKey中key来取值
 - (nullable id)valueForKey:(NSString *)key;
 
 // 通过valueForKeyPath中keyPath来设置值
 - (nullable id)valueForKeyPath:(NSString *)keyPath;
 // keyPath来取值
 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

方法我们一直在使用,但是里面的逻辑是什么我们未必都知道,接下来咱们来研究下底层逻辑,继续看文档

Search Pattern for the Basic Setter

The default implementation of `setValue:forKey:`, given `keyand `value` parameters as input, attempts to set a property named `keyto `value` (or, for non-object properties, the unwrapped version of `value`, as described in [Representing Non-Object Values](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/DataTypes.html#//apple_ref/doc/uid/20002171-BAJEAIEE)) inside the object receiving the call, using the following procedure:

1.  Look for the first accessor named `set<Key>:` or `_set<Key>`, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.
2.  If no simple accessor is found, and if the class method `accessInstanceVariablesDirectly` returns `YES`, look for an instance variable with a name like `_<key>`, `_is<Key>`, `<key>`, or `is<Key>`, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.
3.  Upon finding no accessor or instance variable, invoke `setValue:forUndefinedKey:`. This raises an exception by default, but a subclass of `NSObject` may provide key-specific behavior.

setValue:forKey 设置值步骤

1.查找第一个名为set<Key>:或_set<Key>,setIs<Key>的访问器,按此顺序。如果找到,则使用输入值(或根据需要取消包装的值)调用它并完成。
2.如果没有找到简单的访问器,并且如果类方法accessinstancevariablesdirect返回YES,请查找一个实例变量,其名称依次为_<key>, _is< key>, <key>,或is< key>。如果找到,直接用输入值(或取消包装的值)设置变量并完成。
3.在没有找到访问器或实例变量时,调用setValue:forUndefinedKey:。默认情况下,这会引发一个异常,但是NSObject的子类可能提供特定于键的行为。

接下来我们上代码来验证下是否正确 首先我们创建一个LGPerson类,并创建一个name 成员变量,这里强调一点不能用属性,因素属性底层会自动生成set方法,我们测试不出流程。 我们创建一个person对象,然后调用setValue:forKey: 方法设置值给name

    LGPerson *person = [[LGPerson alloc] init];
    // 1: KVC - 设置值的过程 setValue 分析调用过程
    [person setValue:@"LG_Cooci" forKey:@"name"];

在LGPerson类中实现以下方法

    //**MARK: - setKey. 的流程分析**
    - (void)setName:(NSString *)name{
        NSLog(@"%s - %@",__func__,name);
    }

    - (void)_setName:(NSString *)name{
        NSLog(@"%s - %@",__func__,name);
    }

    - (void)setIsName:(NSString *)name{
        NSLog(@"%s - %@",__func__,name);
    }
    // 没有调用
    - (void)_setIsName:(NSString *)name{
        NSLog(@"%s - %@",__func__,name);
    }

运行会发现打印

**002-KVC取值&赋值过程[66533:1767405]  -[LGPerson setName:] - LG_Cooci**

走了setName: 的方法后面的就不走了,接下来我们注释掉setName: 方法 看看情况

**002-KVC取值&赋值过程[66641:1771714] -[LGPerson _setName:] - LG_Cooci**

走了_setName: 的方法,接下来注释掉_setName: 方法

**002-KVC取值&赋值过程[66660:1772607] -[LGPerson setIsName:] - LG_Cooci**

会发现走了 setIsName: 方法,但是上面的文档中第一步并没有 setIsName: 方法,这时我们要把这个方法加进去。(这可能是文档写错了,毕竟很久没更新了) 接下来注释掉setIsName: 方法。会发现_setIsName: 并没有走。所以接下来要走文档中的第二步 先查看 accessInstanceVariablesDirectly 这方法返回是否是YES,这里看API中注释默认是返回YES,所以继续往下分析 开始查找一个实例变量,其名称依次为_Name,_isName,,或isName。 我们在LGPerson 中创建 四个成员变量 _name ,_isName,name,isName 。现在我们通过打印来查看

LGPerson *person = [[LGPerson alloc] init];

    // 1: KVC - 设置值的过程 setValue 分析调用过程
    [person setValue:@"LG_Cooci" forKey:@"name"];
    
    NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);

打印结果是

**002-KVC取值&赋值过程[66803:1779080] LG_Cooci-(null)-(null)-(null)**

我们会发现_name 上有值,其他没有,说明第一步确实是找_name进行设值;接下来我们注释掉_name 成员变量,看打印结果

    LGPerson *person = [[LGPerson alloc] init];
    // 1: KVC - 设置值的过程 setValue 分析调用过程
    [person setValue:@"LG_Cooci" forKey:@"name"];
    NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
**002-KVC取值&赋值过程[66875:1782337] LG_Cooci-(null)-(null)**

_isName 确实有值,所以证明底层中找到了_isName;这里依次注释掉 name,isName来验证,这里我就不啰嗦了,大家亲自测试下。接下来我们全部注释掉成员变量 然后运行会发现程序奔溃

image.png 系统调用setValue:forUndefinedKey: 方法抛出异常.

通过上诉的代码流程,我们充分的验证了setValue:forKey: 的底层查找流程。

image.png

valueForKey: 取值流程

1.在实例中依次搜索get<Key>, <key>, is<Key>, _<key>,的访问器方法。如果找到,则直接返回对应的值,如果找不到则进入步骤22.判断 + (BOOL)accessInstanceVariablesDirectly函数是否返回YES,找到则返回对应的值),找不到则进入步骤三;如果accessInstanceVariablesDirectly函数是否返回YES返回的是NO,则截止进入步骤33.均找不到,则调用valueForUndefinedKey:抛出异常。

代码验证

    // 2: KVC - 取值的过程
    person->name = @"name";
    NSLog(@"取值:%@",[person valueForKey:@"name"]);
    
    LGPerson.m 中
    // MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>
    - (NSString *)getName{
        return NSStringFromSelector(_cmd);
    }
    - (NSString *)name{
        return NSStringFromSelector(_cmd);
    }
    - (NSString *)isName{
        return NSStringFromSelector(_cmd);
    }
    - (NSString *)_name{
        return NSStringFromSelector(_cmd);
    }

根据上面setValue:forKey: 验证的方法我们可以验证流程是对的,大家自己玩的测试下。 如果这没找到,我们继续找成员变量

    // 2: KVC - 取值的过程
    person->_name = @"_name";
    person->_isName = @"_isName";
    person->name = @"name";
    person->isName = @"isName";
    NSLog(@"取值:%@",[person valueForKey:@"name"]);

我们还是挨个注释并运行看结果,这里注意下,注释点赋值的代码,同时要把LGPerson中对应的成员变量也得注释掉,要不打印为null,原因是能找到 _name,就不往下继续找,但是此时 person->_name = @"_name";已经注释,_name 的值为空,所以打印null。

image.png