iOS进阶之路 (十三)KVC

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

一.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非正式协议启用的一种机制,采用该协议能够间接访问对象的属性。当一个对象与键值编码兼容时,它的属性能够经过一个简洁、统一的消息传递接口经过字符串参数寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。bash

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

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

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类别中还有其余的一些方法ide

// 默认返回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:来间接的获取和设置属性值ui

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.this

  • 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 底层的时候,文档思惟十分重要,多阅读文档总会有新的收获。

相关文章
相关标签/搜索