学习MJ的视频课程,整理并记录知识点–KVC的本质

[TOC]

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

常见的API有

1
2
3
4
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;

基本使用

设值

1、我们可以通过-setValue:forKey:,或者-setValue:forKeyPath:给对象的属性赋值,也可以直接调用对象的属性赋值,如下三个方法的效果等价。

1
2
3
person.age = 10;
[person setValue:@10 forKey:@"age"];
[person setValue:@10 forKeyPath:@"age"];

-setValue:forKey:-setValue:forKeyPath:这两个方法的调用结果一样,函数名很相似,它们两个的区别是什么?

我们先看它的API文档

Discussion

The default implementation of this method gets the destination object for each relationship using valueForKey:, and sends the final object a setValue:forKey: message.

可以概述如下:-setValue:forKeyPath:支持路径方式的参数([person setValue:@10 forKeyPath:@"cat.weight"];),
它会新利用路径查找到指定的层级,内部最终是会调用-setValue:forKey:

2、KVC的另一种使用场景就是我么利用它进行对象的私有属性赋值。

1
[searchField setValue:[UIColor whiteColor] forKeyPath:@"_placeholderLabel.textColor"];

但是在iOS13以后,系统对象通过KVC设置私有属性已被禁止。

取值

相对应的取值方式为-valueForKey:-valueForKeyPath:,使用很简单

1
2
3
person.age;
[person valueForKey:@"age"];
[person valueForKeyPath:@"age"];

-setValue:forKey:的原理

-setValue:forKey:Foundation的函数,我们看不到源码,不过这个API的接口文档很详细。-setValue:forKey:

同时,在Foundation的头文件中,函数的描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Given a value and a key that identifies an attribute, set the value of the attribute. Given an object and a key that identifies a to-one relationship, relate the object to the receiver, unrelating the previously related object if there was one. Given a collection object and a key that identifies a to-many relationship, relate the objects contained in the collection to the receiver, unrelating previously related objects if there were any.

The default implementation of this method does the following:
1. Searches the class of the receiver for an accessor method whose name matches the pattern -set<Key>:. If such a method is found the type of its parameter is checked. If the parameter type is not an object pointer type but the value is nil -setNilValueForKey: is invoked. The default implementation of -setNilValueForKey: raises an NSInvalidArgumentException, but you can override it in your application. Otherwise, if the type of the method's parameter is an object pointer type the method is simply invoked with the value as the argument. If the type of the method's parameter is some other type the inverse of the NSNumber/NSValue conversion done by -valueForKey: is performed before the method is invoked.
2. Otherwise (no accessor method is found), if the receiver's class' +accessInstanceVariablesDirectly property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern _<key>, _is<Key>, <key>, or is<Key>, in that order. If such an instance variable is found and its type is an object pointer type the value is retained and the result is set in the instance variable, after the instance variable's old value is first released. If the instance variable's type is some other type its value is set after the same sort of conversion from NSNumber or NSValue as in step 1.
3. Otherwise (no accessor method or instance variable is found), invokes -setValue:forUndefinedKey:. The default implementation of -setValue:forUndefinedKey: raises an NSUndefinedKeyException, but you can override it in your application.

Compatibility notes:
- For backward binary compatibility with -takeValue:forKey:'s behavior, a method whose name matches the pattern -_set<Key>: is also recognized in step 1. KVC accessor methods whose names start with underscores were deprecated as of Mac OS 10.3 though.
- For backward binary compatibility, -unableToSetNilForKey: will be invoked instead of -setNilValueForKey: in step 1, if the implementation of -unableToSetNilForKey: in the receiver's class is not NSObject's.
- The behavior described in step 2 is different from -takeValue:forKey:'s, in which the instance variable search order is <key>, _<key>.
- For backward binary compatibility with -takeValue:forKey:'s behavior, -handleTakeValue:forUnboundKey: will be invoked instead of -setValue:forUndefinedKey: in step 3, if the implementation of -handleTakeValue:forUnboundKey: in the receiver's class is not NSObject's.
*/
- (void)setValue:(nullable id)value forKey:(NSString *)key;

我们可以结合文档,整理出-setValue:forKey:内部的主要逻辑如下:
-setValue:forKey:

KVC底层调用
willChangeValueForKey:didChangeValueForKey:

-valueForKey:的原理

-valueForKey:函数核心原理和-setValue:forKey:很类似,一个是设置值,一个是取值。它的原理如下:
-valueForKey:

KVC触发KVO的observer

KVC是键值编码、KVO的键值监听,从定义的描述可推测两者之前似乎有关联。他们关联也就是键值,一个是设置修改键值、一个是监听键值的修改。那么通过KVC修改属性会触发KVO么?答案是:会

1
2
3
4
5
// 添加KVO监听
[person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

// 通过KVC修改age属性
[person setValue:@10 forKeyPath:@"age"];

我们在KVO的那一章中介绍过,触发KVO的关键代码是-willChangeValueForKey:-didChangeValueForKey:

1
2
3
[self.person1 willChangeValueForKey:@"age"];
self.person1->_age = 10;
[self.person1 didChangeValueForKey:@"age"];

我们可以在person的class中实现-willChangeValueForKey:-didChangeValueForKey:,进行调试,结论是,通过KVC设置属性会调用这两个方法,也就会触发KVO

总结

知道KVC的调用顺序
通过KVC设置属性会触发KVO

源码和参考

apple document:setvalue

评论