发布于 

KVC 梳理

日常开发中常会用到 KVC,可能一些细节没有深入了解过,本篇通过官方文档来全面了解下 KVC。

一、关于 KVC

1.1 概述

KVC(Key-value coding - 键值编码)是由 NSKeyValueCoding 非正式协议启用的一种机制,对象采用这种机制来提供对其属性/成员变量的间接访问。当一个对象符合键值编码时,它的属性/成员变量可以通过一个简洁、统一的消息传递接口(setValue:forKey:)借助字符串参数寻址。这种间接访问机制补充了实例变量(自动生成的 _<name>)及其相关访问器方法(getter 方法)提供的直接访问。

通常使用访问器方法来访问对象的属性。如,get 访问器(或 getter)返回属性的值;set 访问器(或 setter)设置属性的值。在 objc 中,还可以直接访问属性的底层实例变量(由编译器生成的对应于属性的_+ <property_name>的实例变量)。以上述任何一种方式访问对象属性都是简单的,但需要调用特定于属性的方法或变量名。随着属性列表的增长或更改,访问这些属性的代码也必须随之增长或更改。相反,KVC 兼容对象提供了一个简单的消息传递接口,该接口在其所有属性中都是一致的。

KVC 也是许多其他 Cocoa 技术的基础,例如 KVO(key-value observing)、Cocoa bindings、Core Data 和 AppleScript-ability。在某些情况下,KVC 还可以帮助简化代码。

1.2 为什么说 NSKeyValueCoding 是非正式协议?

前面提到 “KVC 是由 NSKeyValueCoding 非正式协议启用的一种机制”。为什么说NSKeyValueCoding是非正式协议?

它不同于我们常见的 NSCopying、NSCoding 等协议是通过 @protocol 直接来定义的,之后当其他类要遵循此协议时,在其类声明或者类延展后面添加 <NSCopying, NSCoding> 表示该类遵循此协议。而 NSKeyValueCoding 机制是通过分类来实现的,Foundation 框架下有一个 NSKeyValueCoding.h 接口文件,其内部定义了多组分类接口,其中包括:

1
2
3
4
5
6
@interface NSObject(NSKeyValueCoding)
@interface NSArray(NSKeyValueCoding)
@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)
@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)
@interface NSOrderedSet(NSKeyValueCoding)
@interface NSSet(NSKeyValueCoding)

可以看到 NSObject 基类已经实现了 NSKeyValueCoding 机制的所有接口,然后 NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet 这些子类则是对 setValue:forKey:valueForKey: 函数进行重载。

例如,当对一个 NSArray 对象调用 setValue:forKey: 函数时,它内部是对数组中的每个元素调用 setValue:forKey: 函数。当对一个 NSArray 对象调用 valueForKey: 函数时,它返回一个数组,同时会返回数组每个元素调用 valueForKey: 的结果。返回的数组若包含NSNull元素,指代的是数组中某些元素调用 valueForKey: 函数返回 nil 的情况。

二、KVC 的使用

2.1 读

1
2
3
4
5
6
7
8
9
10
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
/// keys 不能包含 path 类型,如使用 path 且未实现 `valueForUndefinedKey:`,会 crash
- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
///由 `valueForKey:` 调用,当找不到与给定键对应的属性时,触发
///子类可以重写该方法以返回未定义键的兜底值。默认实现引发 `NSUndefinedKeyException`
- (id)valueForUndefinedKey:(NSString *)key;
///可以帮助我们更快速的修改`可变/不可变集合类型的属性`
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
///

举例,我们给 Student 添加一个这样的属性:

1
2
3
@property (nonatomic, strong) NSArray<Person *> *personArray;

NSLog(@"❇️❇️ %@", [self.student valueForKeyPath:@"personArray.name"]);

然后打印的就是 personArray 属性中的每个 Person 对象的 name 构成的一个字符串数组。

2.2 写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)setValue:(id)value forKey:(NSString *)key;
///此方法的默认实现使用 `valueForKey:` 获取每个相关的目标对象,并向最终对象发送 `setValue:forKey:` 消息
///即先读取最终对象然后为其赋值
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
///子类可重写此方法,默认会引发 `NSInvalidArgumentException` 的 crash
///基础类型会 crash,对象类型不会
- (void)setNilValueForKey:(NSString *)key;
///默认实现为 `keyedValues` 中的每个键值对调用 `setValue:forKey:`,用 nil 替换 keyedValues 中的 NSNull 值
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;
///由 setValue:forKey: 调用,当它找不到给定键的属性时,默认实现引发 `NSUndefinedKeyException`
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
///子类可以覆盖它以返回 NO,在这种情况下,KVC 方法将无法访问实例变量
@property(class, readonly) BOOL accessInstanceVariablesDirectly;
///ioValue 指向由 inKey 标识的属性的新值的指针。该方法可以修改或替换该值以使其有效
- (BOOL)validateValue:(inout id _Nullable *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable *)outError;
- (BOOL)validateValue:(inout id _Nullable *)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError * _Nullable *)outError;

默认情况下,从 NSObject 或其子类继承的 Swift 对象的属性符合键值编码。

2.3 集合运算符

Operator key path format

1
2
///集合运算符格式
keypathToCollection.@collectionOperator.keypathToProperty

2.3.1 聚合运算符

聚合运算符处理一个数组或一组属性,生成反映集合某些方面的单个值。

1
2
3
4
5
6
7
8
9
10
11
12
///transactionAverage 是 self.transactions 数组中的每个 Transaction 对象中 amount 属性的平均值
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
///numberOfTransactions 是 self.transactions 数组中的元素的个数
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
///当指定 @max 运算符时,valueForKeyPath: 在由右键路径命名的集合条目中搜索并返回最大的条目
///搜索使用 `compare:` 方法进行比较,该方法由许多 Foundation 类(例如 NSNumber 类)定义。因此,由右键路径指示的属性必须包含一个对该消息有意义响应的对象(即集合中的元素必须实现了 compare: 函数)
///搜索将忽略 nil 的集合条目
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
///指定 @sum 运算符时,`valueForKeyPath:` 读取由右键路径为集合的每个元素指定的属性,将其转换为 double(用 0 代替 nil 值),并计算这些值的和
///然后返回存储在 NSNumber 实例中的结果
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];

2.3.2 数组运算符

数组运算符使 valueForKeyPath: 返回一个对象数组,该对象数组与右键路径指示的一组特定对象相对应。如果使用数组运算符时,任何子对象为 nil,则 valueForKeyPath: 方法将引发异常。

1
2
3
4
///distinctPayees 是 self.transactions 数组中的每个 Transaction 对象的 payee 的值组成的字符串数组(忽略重复的 payee)
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
///payees 是 self.transactions 数组中的每个 Transaction 对象的 payee 的值组成的字符串数组(不忽略重复的 payee)
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];

2.3.3 嵌套运算符

嵌套运算符对嵌套集合进行操作,其中集合本身的每个条目都包含一个集合。使用嵌套运算符时,如果任何子对象为 nil,则 valueForKeyPath: 方法将引发异常。

1
2
NSArray *moreTransactions = @[<# transaction data #>];
NSArray *arrayOfArrays = @[self.transactions, moreTransactions];

案例:

1
2
3
4
///要在 arrayOfArrays 中的所有数组之间获取 payee 属性的不同值
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
///@distinctUnionOfSets 运算符结果与 @distinctUnionOfArrays 的结果相同

2.4 包装和展开 Struct

包装和解包常见的 Struct 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct CGPoint {
CGFloat x;
CGFloat y;
};
typedef struct _NSRange {
NSUInteger location;
NSUInteger length;
} NSRange;
struct CGRect {
CGPoint origin;
CGSize size;
};
struct CGSize {
CGFloat width;
CGFloat height;
};

自定义结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct {
float x, y, z;
} ThreeFloats;

@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end

///取。`valueForKey:` 的默认实现会调用 threeFloats getter,然后返回包装在 NSValue 对象中的结果
NSValue *result = [myClass valueForKey:@"threeFloats"];
///存。KVC 设置 threeFloats 值
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];

三、KVC 的原理

3.1 基本的 getter

在给定键参数作为输入的情况下,valueForKey: 的默认实现执行以下过程。(在接收 valueForKey: 调用的类实例内部进行如下操作)

  1. 按序查找 get<Key> <Key> is<Key> 的 getter 方法,若找到直接调用;
  • 若方法的返回结果类型是一个对象指针,则直接返回结果;
  • 若类型为基本数据类型,则转为 NSNumber 返回;否则转为 NSValue 返回;
  1. 若上述未找到 getter,则查找 countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes 方法;
  • countOf<Key> 和另外两方法中一个找到,则返回一个可以响应 NSArray 所有方法的集合代理对象,否则执行步骤3。代理对象随后将接收到的任何 NSArray 消息转化为 countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes 消息组合,并将其转换为创建它的KVC兼容对象;
  1. 若上述未找到,继续查找 countOf<Key> enumeratorOf<Key> memberOf<Key> 方法。若都查到,返回一个可以相应 NSSet 所有方法的集合代理对象。否则执行步骤4。代理对象随后将接收到的任何 NSSet 消息转换为 countOf<Key> enumeratorOf<Key> memberOf<Key>消息组合,以创建对象;
  2. 若上述简单的访问器方法或集合访问方法组未找到,且 receiver 的类方法 accessInstanceVariablesDirectly 返回 YES,则按序查找 _<Key> _is<Key> <Key> is<Key> 的实例变量。若找到,直接获取实例变量的值并执行步骤5。否则,继续步骤6;
  3. 若检索到的属性值是对象指针,只需返回结果;若该值是 NSNumber 支持的基础类型,则返回 NSNumber 实例;若该值 NSNumber 不支持,则转换为 NSValue 对象;最终返回该对象;
  4. 如果所有方法均失败,则调用 valueForUndefinedKey:,默认情况下会引发一个异常。NSObject 子类可通过重写 valueForUndefinedKey: 来避免异常;

3.2 基本的 setter

在接收到调用的对象内部 setValue:forKey: 的默认实现使用了以下过程。

  1. 按顺序查找访问器 set 或 **_set**,如果找到,调用这个方法并传入值,完成操作;
  2. 若未找到对应的 setter,且 accessInstanceVariablesDirectly 类属性返回 YES,按序查找命名规则为 _key_isKeykeyisKey 的实例变量。若找到则将 value 赋值给实例变量,完成操作;
  3. 在找不到访问器及实例变量后,调用 setValue:forUndefinedKey: 方法,默认会抛出一个异常。子类可重写 setValue:forUndefinedKey: 来避免;

3.3 可变数组

mutableArrayValueForKey: 的默认实现,给定一个 key 参数作为输入,为接收访问器调用的对象内的名为 key 的属性返回一个可变的代理数组,具体过程如下:

  1. 查找方法 insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:(分别对应于 NSMutableArray 的原始方法 insertObject:atIndex:removeObjectAtIndex:);或方法 insert<Key>:atIndexes:remove<Key>AtIndexes:(分别对应于 NSMutableArray 的 insertObjects:atIndexes:removeObjectsAtIndexes:)。如果对象具有至少一种插入方法和至少一种删除方法,通过发送 insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:insert<Key>:atIndexes:remove<Key>AtIndexes: 消息的组合,并将消息发送给 mutableArrayValueForKey: 的原始接收者,返回一个响应 NSMutableArray 消息的代理对象。当接收到 mutableArrayValueForKey: 消息的对象还实现了一个可选的 replace 对象方法,方法名类似 replaceObjectInAtIndex:withObject:replaceAtIndexes:with:,代理对象也会在适当的时候也利用这些对象以获得最佳性能;
  2. 如果对象没有可变数组方法,查找与 set: 匹配的访问器方法。这种情况下,通过向 mutableArrayValueForKey: 的原始接收者发出 set: 消息,返回响应 NSMutableArray 消息的代理对象。
  • 该步骤中的机制比上一个机制效率低很多,它可能会重复创建新的集合对象而不是修改现有的集合对象。在设计自己的 KVC 兼容对象时,通常应避免使用它;
  1. 若即没有找到可变数组方法,也未找到访问器,且若 receiver 的类方法 accessInstanceVariablesDirectly 返回 YES。则按序检索实例变量 _<key><key>,若找到这些实例变量,返回对应的代理对象;
  2. 如果所有的操作都失败,则发送 mutableArrayValueForKey: 消息给原始接收方,只要接收到 NSMutableArray 消息则发出 setValue:forUndefinedKey: 消息,该消息会引发 NSUndefinedKeyException。子类可通过重写该方法来避免 crash;

四、iOS13 起遇到的问题

2019.07.12 更新,记录 iOS13 系统禁止 KVC 访问的几种解决方案。

4.1 UITextField

1
2
UITextField *textField = [UITextField new];
[textField valueForKey:@"_placeholderLabel"];

但 iOS13 中 UITextField 重写了 valueForKey:,拦截了外部的取值:

1
2
3
4
5
6
7
8
9
10
@implementation UITextField

- (id)valueForKey:(NSString *)key {
if ([key isEqualToString:@"_placeholderLabel"]) {
[NSException raise:NSGenericException format:@"Access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug"];
}
[super valueForKey:key];
}

@end

解决方案:1.取消下划线; 2.为 UITextField 重写一个方法。

1
2
3
4
5
6
7
8
9
10
11
12
///方案一
[textField valueForKey:@"placeholderLabel"];

///方案二
- (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;
}

4.2 UISearchBar

1
2
3
UISearchBar *bar = [UISearchBar new];
[bar setValue:@"test" forKey:@"_cancelButtonText"]
UIView *searchField = [bar valueForKey:@"_searchField"];

iOS13 中重写了 set_cancleButtonText,拦截了 KVC:

1
2
3
4
5
6
7
8
9
- (void)set_cancelButtonText:(NSString *)text {
[NSException raise:NSGenericException format:@"Access to UISearchBar's set_cancelButtonText: ivar is prohibited. This is an application bug"];
[self _setCancelButtonText];
}

- (void)_searchField {
[NSException raise:NSGenericException format:@"Access to UISearchBar's _searchField ivar is prohibited. This is an application bug"];
[self searchField];
}

解决方案:直接调用 _setCancelButtonTextsearchField

1
2
3
UISearchBar *bar = [UISearchBar new];
[bar setValue:@"test" forKey:@"_setCancelButtonText"]
UIView *searchField = [bar valueForKey:@"searchField"];

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本站由 @JonyFang 创建,使用 Stellar 作为主题,您可以在 GitHub 找到本站源码。