KVC(Key-value Coding)键值编码, 一个非正式的
Protocol, 提供一种机制来间接访问对象的属性, 通俗来说就是可以者通过Key直接访问对象的属性,或者给对象的属性赋值, 而不需要显示的调用存取方法. 苹果的又一大黑魔法~~
用法想必大家都很熟了, 这次来探究其内部原理.
NSObject的Category,NSKeyValueCoding中, 有这么些方法:
1 | @property (class, readonly) BOOL accessInstanceVariablesDirectly; |
- (void)setValue:(nullable id)value forKey:(NSString *)key;
先来看下- setValue: forKey:这个方法, 给定一个标识属性的值和键,设置属性的值, 也就是说只要拿到属性名就可以给它赋值了.
其内部查找机制是:
- 在对象所属类中,查找
-set<key>方法, 如果找到了, 则先检查其参数类型, 如果不是对象指针类型且值为nil的话,则-setNilValueForKey:会被调用, 其内部会抛出NSInvalidArgumentException, 我们可以通过覆盖-setNilValueForKey:方法做一些操作. 否则, 如果-set<key>方法的参数类型是一个对象指针类型, 则该方法就会以值作为参数调用. 如果方法的参数类型是其他类型, 则是在调用方法之前由- valueForKey:执行的NSNumber/NSValue转换的逆函数. - 没有找到
-set<key>方法, 如果对象类实现了+ accessInstanceVariablesDirectly并返回了YES, 那么则会按_<key>,_is<Key>,<key>,is<Key>顺序查找与之匹配的实例变量, 如果找到了这样的一个实例变量,并且它的类型是一个对象指针类型,那么在实例变量的旧值首次被释放之后,该值被保留并且结果赋值给实例变量, 如果实例变量的类型是某种其他类型, 则其值将在与NSNumber\NSValue相同的转换类型之后设置. - 没有找到
-set<key>方法, 也没有找到与之相匹配的实例变量,-setValue:forUndefinedKey:方法则会被调用, 其内部抛出一个NSUndefinedKeyException, 我们也可以覆盖它对异常做些其他的操作.
接下来我们看一个例子:
1 | @interface Test : NSObject |
1 | Test *test = [Test new]; |
1 | 2017-10-29 18:25:39.179854+0800 Test[52096:19683376] -setValue:forUndefinedKey:, value = hello, key = test |
- 先寻找
-set<key>方法, 这里使用了@dynamic test;也就是说,它没有了set方法,就是找不到-set<key>方法, 内部会调用- setValue:forUndefinedKey这个方法; 然后给result赋值为nil,参数类型不是指针类型, 会发现其内部调用了-setNilValueForKey:, 这也就是验证了上面所说的第一点
1 | @interface Test : NSObject { |
如果我们为
Test类增加几个成员变量,+ accessInstanceVariablesDirectly并返回了YES,test依然是没有set方法, 但是打印后却发现test->_test的值为hello, 如果把NSString *_test删掉,test-> _isTest的变成了hello, 再把NSString *_isTest删掉,test->test的值变成了hello, 最后把NSString *test删掉,test->isTest的的值是hello, 这也就验证上面所说的第二点. 如果+ accessInstanceVariablesDirectly返回了NO,那么这些成员变量将不会被赋值当按步骤一和步骤二都没有找到相匹配的方法或者实例变量时, 直接调用
-setValue:forUndefinedKey:, 打印出2017-10-29 19:30:59.330638+0800 Test[52548:19738481] -setValue:forUndefinedKey:, value = hello, key = test
- (nullable id)valueForKey:(NSString *)key;
给定一个标识属性或一对一关系的键,返回属性值或相关对象. 给出一个标识一对多关系的键,返回一个不可变数组或一个包含所有相关对象的不可变集
其内部查找机制:
- 按照
-get<Key>,-<key>,-is<Key>的顺序在对象类中查找这些方法, 如果找到这样的方法, 如果方法的结果类型是一个对象指针类型,则只返回结果, 如果结果的类型是NSNumber转换支持的一个类型,并返回一个NSNumber, 否则,转换完成并返回NSValue - 找不到简单的访问方法,则会按
-countOf<Key>,-indexIn<Key> OfObject:,- objectIn <Key> AtIndex:以及- <key> AtIndexes :(对应于-NSOrderedSet objectsAtIndexes)的顺序查找. 如果找到计数方法和indexOf方法以及其他两种可能的方法中的至少一种方法,则返回响应所有NSOrderedSet方法的集合代理对象. 发送到集合代理对象的每个NSOrderedSet消息将导致-countOf <Key>,-indexIn <Key> OfObject :,-objectIn <Key> AtIndex :,和- <key> AtIndexes:被发送到-valueForKey的原始接收者. 如果接收者的类也实现了一个可选方法,其名称与模式-get <Key>:range的匹配:该方法将在适当的情况下被使用以获得最佳性能. - 如果找不到一个简单的访问方法或一组有序的访问方法, 在接收方的类中搜索其名称与模式匹配的方法
-countOf<Key>和-objectIn<Key>AtIndex:(对应于NSArray类定义的原始方法-<key>AtIndexes:(对应于 —[NSArray objectsAtIndexes:]). 如果找到一个count方法和其他两种可能的方法中的一个, 那么将返回一个响应所有NSArray方法的集合代理对象.发送到集合代理对象的每个NSArray消息都会导致一些消息-countOf<Key>,-objectIn<Key>AtIndex:, 和-<key>AtIndexes:被发送到-valueForKey:的原始接收者. 如果接收方的类也实现了一个可选的方法, 与它的名称匹配的-get<Key>:range:方法将在适合最佳性能时使用 - 如果找不到一个简单的存取器方法或一组有序集或数组访问方法,搜索接收者类的三个方法, 这些方法的名称按
-countOf<Key>,-enumeratorOf<Key>,-memberOf<Key>:匹配(对应于NSSet类定义的原始方法). 如果找到所有三个这样的方法, 那么将返回一个响应所有NSSet方法的集合代理对象. 发送到集合代理对象的每个NSSet消息都会导致一些消息-countOf<Key>,-enumeratorOf<Key>, 和-memberOf<Key>:被发送到-valueForKey:的原始接收者. - 如果找不到简单的访问方法或集合访问方法集, 如果接收者的类
+ accessInstanceVariablesDirectly属性返回YES,搜索对象的类的一个实例变量名称这个顺序与_<key>,_is<Key>,<key>,is<Key>匹配. 如果找到了这样一个实例变量, 那么返回的实例变量的值将返回, 与第1步中的NSNumber或NSValue的转换相同. - 如果找不到简单的访问方法、集合访问方法集或实例变量, 则调用
-valueForUndefinedKey:并返回. 其内部会抛出NSUndefinedKeyException, 我们也可以覆盖这个方法来做一些操作
我们继续来看下例子,
1 | Test *test = [Test new]; |
1 | @implementation Test |
- 先找是否有
-get<Key>方法, 我们发现打印出了-getTest, 我们接着把- (id)getTest方法删掉, 发现会打印出-test,如果在把- (id)test删掉, 则会打印出-isTest, 最后在把-(id)isTest删了, 我们发现打印了-valueForUndefinedKey:, key = test, 这也就验证了第一点所说
我们也增加了这几个成员变量,
1 | @interface Test : NSObject { |
1 | Test *test = [Test new]; |
我们会发现
test->_test的值会是hello,删掉NSString *_test之后发现test->_isTest的值是_isTest, 删除NSString *_isTest后,test->test的值是hello,最后删除NSString *test,test->isTest的值是hello,查找顺序是按_<key>,_is<Key>,<key>,is<Key>最后我们将变量也去掉,最后就会打印出
-valueForUndefinedKey:, key = test, 也就是找不到值了.
- (BOOL)validateValue:(id)ioValue forKey:(NSString *)inKey error:(NSError)outError;
这是KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因
1 | @implementation Test |
1 | Test *test = [Test new]; |
最后则打印出:
1 | 2017-10-29 20:53:11.663274+0800 Test[53775:19851095] 匹配成功 |
这样就给了我们一次纠错的机会, 需要指出的是,KVC是不会自动调用键值验证方法的,就是说我们如果想要键值验证则需要手动验证. 此方法的默认实现将在接收者的类中搜索名称与模式-validate <Key>:error:匹配的验证器方法, 如果找到这样的方法,则调用该方法并返回结果, 如果没有找到这样的方法,则返回YES.
通过- (nullable id)valueForKey:(NSString *)key与- (void)setValue:(nullable id)value forKey:(NSString *)key, 我们大概了解了KVC内部的实现原理, 也了解了KVC给我们提供了一些纠错的方法,接下来我们也可以自己手动实现下KVC
KVC 的简单实现
我们先来看下- (void)setValue:(id)value forKey:(NSString *)key的实现:
1 | Class selfClass = [self class]; |
- 先从当前类寻找是否有
getter方法, 然后并获取到该方法, 通过method_copyArgumentType获取到方法的参数类型,如果不是指针类型并且value为nil, 则内部会直接调用setNilValueForKey方法,并抛出异常. 如果是非指针类型value不为空,则会通过参数类型调用其setter方法. 类型我们可以参照苹果官方的文档. 由于涉及到参数类型的改变, 我是直接通过msgsend方法来进行的,((void (*)(id,SEL,char))objc_msgSend)(self,sel,[value charValue]);
1 | // 无setter方法则查找成员变量, _<key>, _is<Key>, <key>, is<Key> |
如果我们没能从类中找到
setter方法, 则会按照_<key>, _is<Key>, <key>, is<Key>顺序进行变量查找,在此之前我们需要先进行判断accessInstanceVariablesDirectly, 是否允许查找变量, 默认则为YES.
我们可以通过class_copyIvarList方法拿到类中所有实例变量, 通过ivar_getName拿到变量名, 通过ivar_getTypeEncoding拿到变量的类型,然后对key进行逐一比对. 如果是非指针类型而且值还为nil,则会调用setNilValueForKey,否则会通过object_setIvar方法给变量进行赋值, 这里仍然需要对类型进行转换.如果
setter方法跟变量都找不到的话则会调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法并抛出异常.
- (id)valueForKey:(NSString *)key 的实现
1 | // 查找是否有这些方法 -get<Key>, -<key>, -is<Key> |
- 首先我们可以通过
class_copyMethodList方法可以拿到类中所有的实例方法, 通过method_getName获取方法名, 通过method_copyReturnType获得方法的返回类型, 最后按-get<Key>, -<key>, -is<Key>顺序查找是否有这些方法,如果有且返回类型为非指针类型,则需要通过类型转换之后获取到返回值,如果是指针类型,则直接通过id resultValue = ((id (*)(id,SEL))objc_msgSend)(self,sel);拿到返回值.
1 | // 判断accessInstanceVariablesDirectly值,然后做是否搜寻实例变量操作 |
- 也是需要先判断
accessInstanceVariablesDirectly允许是否查找变量, 如果是则按_<key>, _is<Key>, <key>, is<Key>顺序进行变量的查找, 步骤与-setValue:forkey:一致,这里就不再重复. 类型转换之后最后通过object_getIvar方法获取到变量的值. - 如果如果
-get<Key>, -<key>, -is<Key>方法跟_<key>, _is<Key>, <key>, is<Key>变量都找不到的话则会调用- (id)valueForUndefinedKey:(NSString *)key方法并抛出异常.
这里简单的实现了两个常用的方法,这里重点主要是类型的转换,通过这次学习,对KVC的原理有了进一步理解。
###PS:自己实现的KVC源码放在github上, 戳地址~~
最后更新: 2023年03月25日 22:39:55
本文链接: http://aeronxie.github.io/post/d5a83d96.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!