iOS进阶之路 (十三)KVC

2,486 阅读15分钟

KVC属于Foundation框架,不开源,我们只能通过官方文档来了解它

一.KVC初探

1.1 KVC的定义

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.

KVC(键值编码)由 NSKeyValueCoding非正式协议启用的一种机制,采用该协议可以间接访问对象的属性。当一个对象与键值编码兼容时,它的属性可以通过一个简洁、统一的消息传递接口通过字符串参数寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。

Objects typically adopt key-value coding when they inherit from NSObject (directly or indirectly),

所有直接或者间接继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC (一些纯Swift类和结构体是不支持KVC的)

1.2 KVC的API

KVC常用的四个方法

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

NSKeyValueCoding类别中还有其他的一些方法

// 默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;

// KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;                 // 取值
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; // 设值

// 如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

二. KVC的使用

这段内容比较基础,只需要注意:只有继承于NSObject的数据才能使用KVC,非NSObject类型的需要做类型转换。

2.1 访问对象属性

2.1.1 valueForKey & setValue: Forkey

通过 valueForKey:setValue:Forkey:来间接的获取和设置属性值

valueForKey: - Returns the value of a property named by the key parameter. If the property named by the key cannot be found according to the rules described in Accessor Search Patterns, then the object sends itself a valueForUndefinedKey: message. The default implementation of valueForUndefinedKey: raises an NSUndefinedKeyException, but subclasses may override this behavior and handle the situation more gracefully.

  • valueForKey: 返回由 key 参数命名的属性的值。如果根据访问者搜索模式中的规则找不到由 key 命名的属性,则该对象将向自身发送 valueForUndefinedKey: 消息。valueForUndefinedKey:的默认实现会抛出 NSUndefinedKeyException 异常,但是子类可以重写此行为并更优雅地处理这种情况。

setValue:forKey:: Sets the value of the specified key relative to the object receiving the message to the given value. The default implementation of setValue:forKey: automatically unwraps NSNumber and NSValue objects that represent scalars and structs and assigns them to the property. See Representing Non-Object Values for details on the wrapping and unwrapping semantics. If the specified key corresponds to a property that the object receiving the setter call does not have, the object sends itself a setValue:forUndefinedKey: message. The default implementation of setValue:forUndefinedKey: raises an NSUndefinedKeyException. However, subclasses may override this method to handle the request in a custom manner.

  • setValue:forKey:: 将该消息接收者的指定 key 的值设置为给定值。默认实现会自动把表示标量结构体的 NSNumber 和 NSValue 对象解包然后赋值给属性。如果指定 key 所对应的属性没有对应的 setter 实现,则该对象将向自身发送 setValue:forUndefinedKey: 消息,valueForUndefinedKey:的默认实现会抛出一个 NSUndefinedKeyException 的异常。但是子类可以重写此方法以自定义方式处理请求。

Example:

AKPerson *person = [[AKPerson alloc] init];
   
[person setValue:@"akironer" forKey:@"name"];
NSLog(@"%@", [person valueForKey:@"name"]);

打印输出:akironer

2.1.2 valueForKeyPath & setValue:forKeyPath:

valueForKeyPath: - Returns the value for the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key—that is, for which the default implementation of valueForKey: cannot find an accessor method—receives a valueForUndefinedKey: message.

  • valueForKeyPath: : 返回于接受者的指定key path上的值。key path 路径序列中不符合特定键的键值编码的任何对象,都会接收到 valueForUndefinedKey: 消息。

setValue:forKeyPath: - Sets the given value at the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key receives a setValue:forUndefinedKey: message.

  • setValue:forKeyPath:: 将该消息接收者的指定 key path 的值设置为给定值。key path 路径序列中不符合特定键的键值编码的任何对象都将收到setValue:forUndefinedKey: 消息

Example:

AKTeacher *teacher = [[AKTeacher alloc] init];
teacher.subject    = @"iOS";
person.teacher     = teacher;
[person setValue:@"iOS进阶之路" forKeyPath:@"teacher.subject"];
NSLog(@"%@",[person valueForKeyPath:@"teacher.subject"]);

打印输出:iOS进阶之路

2.1.3 dictionaryWithValuesForKeys: & setValuesForKeysWithDictionary:

-> dictionaryWithValuesForKeys: - Returns the values for an array of keys relative to the receiver. The method calls valueForKey: for each key in the array. The returned NSDictionary contains values for all the keys in the array.

  • 返回接收者的 key 数组的值。该方法会为数组中的每个 key 调用valueForKey:。 返回的 NSDictionary 包含数组中所有键的值。

setValuesForKeysWithDictionary: - Sets the properties of the receiver with the values in the specified dictionary, using the dictionary keys to identify the properties. The default implementation invokes setValue:forKey: for each key-value pair, substituting nil for NSNull objects as required.

  • setValuesForKeysWithDictionary::使用字典键标识属性,将指定字典中的对应值设置成该消息接收者的属性值。默认实现会对每一个键值对调用 setValue:forKey:。设置时需要将 nil 替换成 NSNull

Collection objects, such as NSArray, NSSet, and NSDictionary, can’t contain nil as a value. Instead, you represent nil values using the NSNull object.

  • NSArray NSSetNSDictionary 等集合对象不能包含 nil 作为值, 可以使用 NSNull对象代替 nil 值。
[person setValuesForKeysWithDictionary:@{@"name": @"akironer", @"age": @(18)}, @"hobby":[NSNULL null]];
NSLog(@"%@", [person dictionaryWithValuesForKeys:@[@"name", @"age"]]);       
        
打印输出:
{
    age = 18;
    name = akironer;
    hobby = null;
}  

2.2 访问集合属性

//  方法一:普通方式
person.array = @[@"1",@"2",@"3"];
NSArray *array = [person valueForKey:@"array"]; // 不可不数组无法直接修改,用 array 的值创建一个新的数组
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"方法一:%@",[person valueForKey:@"array"]);
    
// 方法二:KVC 的方式
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"100";
NSLog(@"方法二:%@",[person valueForKey:@"array"]);

打印输出:
方法一:(
    100,
    2,
    3
)
方法二:(
    100,
    2,
) 

操作集合对象内部的元素来说,更高效的方式是使用 KVC 提供的可变代理方法。KVC 为我们提供了三种不同的可变代理方法:

  1. mutableArrayValueForKey: & mutableArrayValueForKeyPath::返回的代理对象表现为一个 NSMutableArray 对象
  2. mutableSetValueForKey: & mutableSetValueForKeyPath::返回的代理对象表现为一个 NSMutableSet 对象
  3. mutableOrderedSetValueForKey: & mutableOrderedSetValueForKeyPath::返回的代理对象表现为一个 NSMutableOrderedSet 对象

2.3 集合操作符

在使用 valueForKeyPath: 的时候,可以使用集合运算符来实现一些高效的运算操作。

  1. 聚合操作符
  • @avg: 返回操作对象指定属性的平均值
  • @count: 返回操作对象指定属性的个数
  • @max: 返回操作对象指定属性的最大值
  • @min: 返回操作对象指定属性的最小值
  • @sum: 返回操作对象指定属性值之和
  1. 数组操作符
  • @distinctUnionOfObjects: 返回操作对象指定属性的集合--去重
  • @unionOfObjects: 返回操作对象指定属性的集合
  1. 嵌套操作符
  • @distinctUnionOfArrays: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSArray
  • @unionOfArrays: 返回操作对象(集合)指定属性的集合
  • @distinctUnionOfSets: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSSet

2.4 访问非对象属性

非对象属性分为两类:

  • 基本数据类型,也就是所谓的标量(scalar)
  • 结构体(struct)。

2.4.1 访问基本数据类型(标量)

常用的基本数据类型需要在设置属性的时候包装成 NSNumber 对象

Scalar types as wrapped in NSNumber objects

2.4.2 访问结构体

除了 NSPoint NSRange NSRectNSSize,对于自定义的结构体,也需要进行 NSValue 的转换操作.

Common struct types as wrapped using NSValue.

typedef struct {
    float x, y, z;
} ThreeFloats;

// 设值
ThreeFloats floats = {1., 2., 3.};
NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslut = [person valueForKey:@"threeFloats"];
NSLog(@"%@",reslut);

// 取值
ThreeFloats result;
[reslut getValue:&result] ;
NSLog(@"%f - %f - %f",result.x, result.y, result.z);

打印输出:
{length = 12, bytes = 0x0000803f0000004000004040}
1.000000 - 2.000000 - 3.000000

三. KVC原理 -- 搜索规则

在学习KVC的搜索规则前,要先弄明白一个属性的作用,这个属性在搜索过程中起到很重要的作用。这个属性表示是否允许读取实例变量的值,如果为YES则在KVC查找的过程中,从内存中读取属性实例变量的值。

@property (class, readonly) BOOL accessInstanceVariablesDirectly;

3.1 基本 getter

Search Pattern for the Basic Getter

valueForKey:方法的默认实现:valueForKey: 方法会在调用者传入 key之后会在对象中按下列的步骤进行模式搜索:

  1. get<Key> <key> is<Key> 以及 _<key> 的顺序查找对象中是否有对应的方法。
  • 如果找到了,将方法返回值带上跳转到第 5 步
  • 如果没有找到,跳转到第 2 步
  1. 如果没有找到简单getter方法方法,则查找是否有 countOf<Key> 方法 objectIn<Key>AtIndex: 方法 (对应于 NSArray类定义的原始方法) 以及 <key>AtIndexes: 方法 (对应于 NSArray 方法 objectsAtIndexes:)
  • 如果找到其中的第一个(countOf<Key>),再找到其他两个中的至少一个,则创建一个响应所有 NSArray 方法的代理集合对象,并返回该对象。(翻译过来就是要么是 countOf<Key> + objectIn<Key>AtIndex:,要么是countOf<Key> + <key>AtIndexes:,要么是 countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:)
  • 如果没有找到,跳转到第 3 步
  1. 如果没有找到简单NSArray方法,查找名为 countOf<Key> enumeratorOf<Key> memberOf<Key> 这三个方法(对应于NSSet类定义的原始方法)
  • 如果找到这三个方法,则创建一个响应所有 NSSet 方法的代理集合对象,并返回该对象
  • 如果没有找到,跳转到第 4 步
  1. 判断类方法 accessInstanceVariablesDirectly 结果
  • 返回 YES,则以 _<key> _is<Key> <key> is<Key> 的顺序查找成员变量。如果找到了,将成员变量带上跳转到第 5 步,如果没有找到则跳转到第 6 步
  • 返回 NO,跳转到第 6 步
  1. 判断取出的属性值
  • 如果属性值是对象,直接返回
  • 如果属性值不是对象,但可以转化为 NSNumber 类型,则将属性值转化为 NSNumber 类型返回
  • 如果属性值不是对象,也不能转化为 NSNumber 类型,则将属性值转化为 NSValue 类型返回
  1. 调用 valueForUndefinedKey:, 默认情况下抛出NSUndefinedKeyException异常,但是继承于NSObject的子类可以重写该方法避免崩溃并做相应措施

Search Pattern for the Basic Getter

3.2 基本 setter

Search Pattern for the Basic Setter

  1. set<Key>: _set<Key> 顺序查找对象中是否有对应的方法
  • 找到了直接调用设值
  • 没有找到跳转第2步
  1. 判断 accessInstanceVariablesDirectly 结果
  • 为YES,按照 _<key> _is<Key> <key> is<Key> 的顺序查找成员变量,找到了就赋值;找不到就跳转第3步
  • 为NO,跳转第3步
  1. 调用setValue:forUndefinedKey:。默认情况下抛出NSUndefinedKeyException异常,但是继承于NSObject的子类可以重写该方法避免崩溃并做出相应措施

Search Pattern for the Basic Setter

3.3 编译器自动实现getter setter

这里再明确下实例变量、成员变量、属性之间的区别:

  • 成员变量:在 @interface 括号里面声明的变量
  • 成员变量实际上由两部分组成:实例变量 + 基本数据类型变量
  • 属性 = 成员变量 + getter方法 + setter方法

我们不去重写属性的 getter 和 setter 方法以及声明对应的实例变量,那么编译器就会帮我们做这件事,那么是不是说有多少个属性,就会生成多少个对应的 getter 和 setter 呢?

显然,编译器不会这么傻。编译器在objc-accessors.mm中运用通用原则给所有属性都提供了同一的入口,setter方法会根据修饰符不同调用不同方法,最后统一调用reallySetProperty方法示。

四. KVC的使用

KVC在iOS开发中是绝不可少的利器,也是许多iOS开发黑魔法的基础。列举一下KVC的使用场景。

4.1 动态取值和设值

最基本的用法,相信大家都很属性了

4.2 访问和修改私有变量

对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的。

4.3 模型和字典转换

运用了KVC和Objc的runtime组合的技巧,完成模型和字典的相互转换

4.4 修改控件的内部属性

在 iOS 13 之前,我们可以通过 KVC 去获取和设置系统的私有属性,但从 iOS 13 之后,这种方式被禁用掉了。相信不少同学适配 iOS 13的时候,已经遇到了KVC的访问限制问题。

例如UITextField中的placeHolderText已经不能修改了,这里提供两种简答的修改思路,想要深入了解的可以参考关于iOS 13 中KVC 访问限制的一些处理

  1. 通过attributedPlaceholder属性修改Placeholder颜色
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:@"请输入占位文字" attributes: @{NSForegroundColorAttributeName:[UIColor redColor], NSFontAttributeName:textField.font }];

textField.attributedPlaceholder = attrString;
  1. UITextField重新写一个方法
- (void)resetTextField: (UITextField *)textField
{
 	Ivar ivar =  class_getInstanceVariable([textField class], "_placeholderLabel");
 	
    UILabel *placeholderLabel = object_getIvar(textField, ivar);
    placeholderLabel.text = title;
    placeholderLabel.textColor = color;
    placeholderLabel.font = [UIFont systemFontOfSize:fontSize];
    placeholderLabel.textAlignment = alignment;
}

五. 异常处理及正确性验证

5.1 设置空值:setNilValueForKey

在设值时设置空值,可以通过重写setNilValueForKey来监听

In the default implementation, when you attempt to set a non-object property to a nil value, the key-value coding compliant object sends itself a setNilValueForKey: message. The default implementation of setNilValueForKey: raises an NSInvalidArgumentException, but an object may override this behavior to substitute a default value or a marker value instead, as described in Handling Non-Object Values.

在默认实现中,当您试图将非对象属性设置为nil时,KVC的对象会向自己发送一条setNilValueForKey:消息。setNilValueForKey的默认实现会引发NSInvalidArgumentException,但对象可以重写此行为以替换默认值或标记值。

Given that an invocation of -setValue:forKey: would be unable to set the keyed value because the type of the parameter of the corresponding accessor method is an NSNumber scalar type or NSValue structure type but the value is nil, set the keyed value using some other mechanism.

大体意思就是说:只对NSNumber或者NSValue类型的数据赋空值时,setNilValueForKey才会触发。下面的例子中,subject不会触发

@implementation LGPerson
- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        NSLog(@"你傻不傻: 设置 %@ 是空值",key);
        return 0;
    }
    [super setNilValueForKey:key];
}
@ end

[person setValue:nil forKey:@"age"]; 
[person setValue:nil forKey:@"subject"]; // subject不触发 - 官方注释里面说只对 NSNumber - NSValue

5.2 未定义的key:setValue:forUndefinedKey

对于未定义的key, 可以通过重写setValue:forUndefinedKey:valueForUndefinedKey:来监听。

例如:

我们在字典转模型的时候,例如服务器返回一个id字段,但是对于客户端来说id是系统保留字段,可以重写setValue:forUndefinedKey:方法并在内部处理id参数的赋值。

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"id"]) {
        self.userId = [value integerValue];
    }
}

5.3 属性验证

在调用KVC时可以先进行验证,验证通过下面两个方法进行,支持keykeyPath两种方式。

验证方法需要我们手动调用,并不会在进行KVC的过程中自动调用

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;

该方法的工作原理:

  1. 先找一下你的类中是否实现了方法 -(BOOL)validate:error;
  2. 如果实现了就会根据实现方法里面的自定义逻辑返回NO或者YES;如果没有实现这个方法,则系统默认返回YES

下面是使用验证方法的例子。在validateValue方法的内部实现中,如果传入的value或key有问题,可以通过返回NO来表示错误,并设置NSError对象。

AKPerson* person = [[AKPerson alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
    NSLog(@"%@",error);
}

六. 总结

  1. KVC 是一种 NSKeyValueCoding 隐式协议所提供的机制。
  2. KVC 通过 valueForKey:valueForKeyPath: 来取值,不考虑集合类型的话具体的取值过程如下:
  • get<Key> <key> is<Key> _<key> 的顺序查找方法
  • 如果找不到方法,则通过类方法 accessInstanceVariablesDirectly 判断是否能读取成员变量来返回属性值
  • _<key> _is<Key> <key> is<Key> 的顺序查找成员变量
  1. KVC 通过 setValueForKey:setValueForKeyPath: 来取值,不考虑集合类型的话具体的设置值过程如下:
  • set<Key> _set<Key> 的顺序查找方法
  • 如果找不到方法,则通过类方法 accessInstanceVariablesDirectly 判断是否能通过成员变量来返回设置值 以 _<key> _is<Key> <key> is<Key> 的顺序查找成员变量

这次我们依据苹果的官方文档完成了KVC的探索,其实苹果的英文注释和官方文档写的非常用心,我们在探索 iOS 底层的时候,文档思维十分重要,多阅读文档总会有新的收获。