iOS - 关于 KVC 的一些总结

KVC 大纲

目录

  • 1. 什么是 KVC
  • 2. 访问对象属性
  • 3. 访问集合属性
  • 4. 使用集合运算符
  • 5. 自定义集合运算符
  • 6. 非对象值处理
  • 7. 属性验证
  • 8. 搜索规则
  • 9. 异常处理
  • 10. 相关面试题
  • 参考

1. 什么是 KVC

  • KVC的全称是Key-Value Coding(键值编码),是由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问,可以通过字符串来访问一个对象的成员变量或其关联的存取方法(getter or setter)。
  • 通常,我们可以直接通过存取方法或变量名来访问对象的属性。我们也可以使用KVC间接访问对象的属性,并且KVC还可以访问私有变量。某些情况下,KVC还可以帮助简化代码。
  • KVC是许多其他 Cocoa 技术的基础概念,比如 KVO、Cocoa bindings、Core Data、AppleScript-ability 等等。

2. 访问对象属性

常用 API

- (nullable id)valueForKey:(NSString *)key;         // 通过 key 来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通过 keyPath 来取值

- (void)setValue:(nullable id)value forKey:(NSString *)key;         // 通过 key 来赋值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通过 keyPath 来赋值

基础操作

如下是 BankAccount 类的声明:

@interface BankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
@end

对于 BankAccount 的实例对象myAccount
我们可以使用setter方法为currentBalance属性赋值,这是直接的,但缺乏灵活性。

[myAccount setCurrentBalance:@(100.0)];

我们也可以通过KVC间接为currentBalance属性赋值,通过其键Key设置值。

[myAccount setValue:@(100.0) forKey:@"currentBalance"];

KeyPath

KVC还支持多级访问,KeyPath用法跟点语法相同。
例如:我们想对myAccountowner属性的address属性的street属性赋值,其KeyPathowner.address.street

[myAccount setValue:@"地址" forKeyPath:@"owner.address.street"];

多值操作

给定一组Key,获得一组value,以字典的形式返回。该方法为数组中的每个Key调用valueForKey:方法。

- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

将指定字典中的值设置到消息接收者的属性中,使用字典的Key标识属性。默认实现是为每个键值对调用setValue:forKey:方法 ,会根据需要用nil替换NSNull对象。

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;

3. 访问集合属性

我们可以像访问其它对象一样使用valueForKey:setValue:forKey:方法来获取或设置集合对象(主要指NSArrayNSSet)。但是,当我们要操作集合对象的内容,比如添加或者删除元素时,通过KVC的可变代理方法获取集合代理对象是最有效的。
根据KVO的实现原理,是在运行时动态生成子类并重写setter方法来达到可以通知所有观察者对象的目的,因此我们对集合对象进行操作是不会触发KVO的。当我们要使用KVO监听集合对象变化时,需要通过KVC的可变代理方法获取集合代理对象,然后对代理对象进行操作。当代理对象的内部对象发生改变时,会触发KVO的监听方法。
传送门:iOS - 关于 KVO 的一些总结

KVC提供了三种不同的代理对象访问的代理方法,每种都有KeyKeyPath两种方法。

  • mutableArrayValueForKey:mutableArrayValueForKeyPath:

    返回NSMutableArray对象的代理对象。

  • mutableSetValueForKey:mutableSetValueForKeyPath:

    返回NSMutableSet对象的代理对象。

  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:

    返回NSMutableOrderedSet对象的代理对象。

4. 使用集合运算符

KVCvalueForKeyPath:方法除了可以取出属性值以外,还可以在KeyPath中嵌套集合运算符,来对集合对象进行操作。

以下是KeyPath集合运算符的格式,主要分为 3 个部分,如图 4-1。

  • Left key path:左键路径,要操作的集合对象,如果消息接收者就是集合对象,则可以省略 Left 部分;
  • Collection operator:集合运算符;
  • Right key path:右键路径,要进行运算的集合中的属性。
图 4-1 KeyPath 集合运算符格式.png

集合运算符主要分为三类:

  • ① 聚合运算符:以某种方式合并集合中的对象,并返回右键路径中指定的属性的数据类型匹配的一个对象,一般返回NSNumber实例。
  • ② 数组运算符:根据运算符的条件,将符合条件的对象以一个NSArray实例返回。
  • ③ 嵌套运算符:处理集合对象中嵌套其他集合对象的情况,并根据运算符返回一个NSArrayNSSet实例。

示例

如下是 BankAccount 类和 Transaction 类的声明。BankAccount 中有一个 transactions 数组属性,其元素为 Transaction 类型。Transaction 类中定义了 3 个属性,分别为收款人、金额、日期。

@interface BankAccount : NSObject
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many 
@end

@interface Transaction : NSObject
@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When
@end

下表是为了演示集合运算符使用而给出的 transactions 数组的数据。

payee amount date
Green Power $120.00 Dec 1, 2015
Green Power $150.00 Jan 1, 2016
Green Power $170.00 Feb 1, 2016
Car Loan $250.00 Jan 15, 2016
Car Loan $250.00 Feb 15, 2016
Car Loan $250.00 Mar 15, 2016
General Cable $120.00 Dec 1, 2015
General Cable $155.00 Jan 1, 2016
General Cable $120.00 Feb 1, 2016
Mortgage $1,250.00 Jan 15, 2016
Mortgage $1,250.00 Feb 15, 2016
Mortgage $1,250.00 Mar 15, 2016
Animal Hospital $600.00 Jul 15, 2016

聚合运算符

以某种方式合并集合中的对象,并返回右键路径中指定的属性的数据类型匹配的一个对象,一般返回NSNumber实例。

@avg

读取集合中每个元素的右键路径指定的属性,将其转换为double类型 (nil用 0 替代),并计算这些值的算术平均值。然后将结果以NSNumber实例返回。

// 计算上表中 amount 的平均值。
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
// transactionAverage 格式化的结果为 $ 456.54。

@count

计算集合中的元素个数,以NSNumber实例返回。

// 计算 transactions 集合中的元素个数。
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
// numberOfTransactions 的值为 13。

备注: @count运算符比较特别,它不需要写右键路径,即使写了也会被忽略。

@sum

读取集合中每个元素的右键路径指定的属性,将其转换为double类型 (nil用 0 替代),并计算这些值的总和。然后将结果以NSNumber实例返回。

// 计算上表中 amount 的总和。
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
// amountSum 的结果为 $ 5935.00。

@max

返回集合中右键路径指定的属性的最大值。

// 获取日期的最大值。
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
// latestDate 的值为 Jul 15, 2016.

@min

返回集合中右键路径指定的属性的最小值。

// 获取日期的最小值。
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
// earliestDate 的值为 Dec 1, 2015.

备注: @max@min根据右键路径指定的属性在集合中搜索,搜索使用compare:方法进行比较,许多基础类 (如NSNumber类) 中都有定义。因此,右键路径指定的属性必须能响应compare:消息。搜索忽略值为nil的集合项。可以通过重写compare:方法对搜索过程进行控制。

数组运算符

根据运算符的条件,将符合条件的对象以一个NSArray实例返回。

@unionOfObjects

读取集合中每个元素的右键路径指定的属性,放在一个NSArray实例中并返回。

// 获取集合中的所有 payee 对象。
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
// payees 数组包含以下字符串:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital。

@distinctUnionOfObjects

读取集合中每个元素的右键路径指定的属性,放在一个NSArray实例中,将数组进行去重后返回。

// 获取集合中的所有不同的 payee 对象。
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
// distinctPayees 数组包含以下字符串:Car Loan, General Cable, Animal Hospital, Green Power, Mortgage。

注意: 在使用数组运算符时,如果有任何操作的对象为nil,则valueForKeyPath:方法将引发异常。

嵌套运算符

处理集合对象中嵌套其他集合对象的情况,并根据运算符返回一个NSArrayNSSet实例。

如下 moreTransactions 是装着 transaction 对象的数组,arrayOfArrays 数组中嵌套了 self.transactions 和 moreTransactions 两个数组。

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

下表是 moreTransactions 数组的数据。

payee amount date
General Cable - Cottage $120.00 Dec 18, 2015
General Cable - Cottage $155.00 Jan 9, 2016
General Cable - Cottage $120.00 Dec 1, 2016
Second Mortgage $1,250.00 Nov 15, 2016
Second Mortgage $1,250.00 Sep 20, 2016
Second Mortgage $1,250.00 Feb 12, 2016
Hobby Shop $600.00 Jun 14, 2016

@unionOfArrays

读取集合中的每个集合中的每个元素的右键路径指定的属性,放在一个NSArray实例中并返回。

// 获取 arrayOfArrays 集合中的每个集合中的所有 payee 对象。
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
// collectedPayees 数组包含以下字符串:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital, General Cable - Cottage, General Cable - Cottage, General Cable - Cottage, Second Mortgage, Second Mortgage, Second Mortgage, Hobby Shop.

@distinctUnionOfArrays

读取集合中的每个集合中的每个元素的右键路径指定的属性,放在一个NSArray实例中,将数组进行去重后返回。

// 获取 arrayOfArrays 集合中的每个集合中的所有不同的 payee 对象。
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
// collectedDistinctPayees 数组包含以下字符串:Hobby Shop, Mortgage, Animal Hospital, Second Mortgage, Car Loan, General Cable - Cottage, General Cable, Green Power.

@distinctUnionOfSets

读取集合中的每个集合中的每个元素的右键路径指定的属性,放在一个NSSet实例中,去重后返回。

NSSet *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfSets.payee"];

注意:

  • 在使用嵌套运算符时,valueForKeyPath:内部会根据运算符创建一个NSMutableArrayNSMutableSet对象,将集合中的arrayset添加进去再进行操作。如果集合中有非集合元素,会导致Crash
  • 使用unionOfArraysdistinctUnionOfArrays运算符,消息接收者应该是arrayOfArrays类型,即NSArray< NSArray* >* arrayOfArrays;;使用distinctUnionOfSets运算符,消息接收者应该是setOfSets或者arrayOfSets类型。否则会发生异常。
  • 在使用嵌套运算符时,如果有任何操作的对象为nil, 则valueForKeyPath:方法将引发异常。

拓展

如果集合中的对象都是NSNumber,右键路径可以用self

    NSArray *array = @[@1, @2, @3, @4, @5];
    NSNumber *sum = [array valueForKeyPath:@"@sum.self"];
    NSLog(@"%d",[sum intValue]); 

5. 自定义集合运算符

上面介绍了KVC为我们提供的集合运算符,我们能不能自定义呢?

我们使用Runtime打印NSArray类的方法列表:

- (void)printNSArrayMethods
{
    u_int count;
    Method *methods = class_copyMethodList([NSArray class], &count);
    for (int i = 0; i < count ; i++)
    {
        Method method = methods[i];
        SEL sel = method_getName(method);
        NSLog(@"%d---%@", i, NSStringFromSelector(sel));
    }
    free(methods);
}
0---mr_isEqualToOutputDevicesArray:
1---mr_containsAnyOf:
2---mr_map:
3---sg_enumerateChunksOfSize:usingBlock:
4---_pas_mappedArrayWithTransform:
5---_pas_shuffledArrayUsingRng:
......

方法很多,我们搜索关键字avgcountsumKVC为我们提供的集合运算符,发现都有对应的方法_<operatorKey>ForKeyPath:

267---_avgForKeyPath:
268---_countForKeyPath:
264---_sumForKeyPath:
269---_maxForKeyPath:
270---_minForKeyPath:
266---_unionOfObjectsForKeyPath:
273---_distinctUnionOfObjectsForKeyPath:
265---_unionOfArraysForKeyPath:
272---_distinctUnionOfArraysForKeyPath:
274---_distinctUnionOfSetsForKeyPath:

注意: 我们再来看一下NSSet类支持哪些集合运算符:

50---_sumForKeyPath:
51---_avgForKeyPath:
52---_countForKeyPath:
53---_maxForKeyPath:
54---_minForKeyPath:
55---_distinctUnionOfArraysForKeyPath:
56---_distinctUnionOfObjectsForKeyPath:
57---_distinctUnionOfSetsForKeyPath:

可见NSSet类不支持@unionOfObjects@unionOfArrays运算符,如果使用了就会抛出异常NSInvalidArgumentException并导致程序崩溃,reason: [<__NSSetI 0x6000017a12f0> valueForKeyPath:]: this class does not implement the unionOfArrays operation.不支持该运算符。

NSArray类虽然支持@distinctUnionOfSets运算符,但其必须是arrayOfSets类型,即NSArray< NSSet* >* arrayOfSets;。因为_distinctUnionOfSetsForKeyPath方法中会创建一个NSMutableSet实例,并调用unionSet:方法将集合中的set的元素添加进去再进行操作。如果是arrayOfArrays类型就会抛出异常NSInvalidArgumentException并导致程序崩溃,reason: '*** -[NSMutableSet unionSet:]: set argument is not an NSSet'即集合中有非NSSet元素。

我们尝试为NSArray添加一个分类,并定义一个_medianForKeyPath:方法,用来获取NSArray中的中位数。

#import <Foundation/Foundation.h>
@interface NSArray (HTOperator)
- (NSNumber *)_medianForKeyPath:(NSString *)keyPath;
@end

#import "NSArray+HTOperator.h"
@implementation NSArray (HTOperator)
- (NSNumber *)_medianForKeyPath:(NSString *)keyPath {
    //排序
    NSArray *sortedArray = [self sortedArrayUsingSelector:@selector(compare:)];
    double median;
    if (self.count % 2 == 0) {
        NSInteger index1 = sortedArray.count * 0.5;
        NSInteger index2 = sortedArray.count * 0.5 - 1;
        median = ([[sortedArray objectAtIndex:index1] doubleValue] + [[sortedArray objectAtIndex:index2] doubleValue]) * 0.5;        
    } else {
        NSInteger index = (sortedArray.count-1) * 0.5;
        median = [[sortedArray objectAtIndex:index] doubleValue];
    }
    return [NSNumber numberWithDouble:median];
}

测试。

    NSArray *array = @[@9, @7, @8, @2, @6, @3];
    NSNumber *num = [array valueForKeyPath:@"@median.self"];
    NSLog(@"%f",[num doubleValue]);
    // 6.500000

6. 非对象值处理

KVC支持基础数据类型和结构体,在使用KVC进行赋值或取值的时候,会自动在非对象值和对象值之间进行转换。

  • 当进行取值如valueForKey:时,如果返回值非对象,会使用该值初始化一个NSNumber(用于基础数据类型)或NSValue(用于结构体)实例,然后返回该实例。
  • 当进行赋值如setValue:forKey:时,如果key的数据类型非对象,则会发送一条<type>Value消息给value对象以提取基础数据,然后赋值给key

注意:

  • 因为Swift中的所有属性都是对象,所以这里仅适用于Objective-C属性。
  • 当进行赋值如setValue:forKey:时,如果key的数据类型是非对象类型,则value就禁止传nil。否则会调用setNilValueForKey:方法,该方法的默认实现抛出异常NSInvalidArgumentException,并导致程序Crash

下表是KVC对于基础数据类型和NSNumber对象之间的转换。

Data type Creation method Accessor method
BOOL numberWithBool: boolValue (in iOS)
charValue (in macOS)*
char numberWithChar: charValue
double numberWithDouble: doubleValue
float numberWithFloat: floatValue
int numberWithInt: intValue
long numberWithLong: longValue
long long numberWithLongLong: longLongValue
short numberWithShort: shortValue
unsigned char numberWithUnsignedChar: unsignedChar
unsigned int numberWithUnsignedInt: unsignedInt
unsigned long numberWithUnsignedLong: unsignedLong
unsigned long long numberWithUnsignedLongLong: unsignedLongLong
unsigned short numberWithUnsignedShort: unsignedShort

下表是KVC对于结构体类型和NSValue对象之间的转换。

Data type Creation method Accessor method
CGPoint valueWithCGPoint: CGPointValue
CGRect valueWithCGRect: CGRectValue
CGSize valueWithCGSize: CGSizeValue
NSRange valueWithRange: rangeValue

除了以上CGPointCGRectCGSizeNSRange类型的结构体可以和NSValue对象之间进行转换,我们自定义的结构体也可以包装成NSValue对象,示例如下。

typedef struct {
    float x, y, z;
} ThreeFloats;
 
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
// 取值
NSValue* result = [myClass valueForKey:@"threeFloats"];
// 赋值
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];

7. 属性验证

KVC提供了属性验证的方法,如下。我们可以在使用KVC赋值前验证能否为这个key赋值指定value
validateValue方法的默认实现是查看消息接收者类中是否实现了遵循命名规则为validate<Key>:error:的方法,如果有的话就返回调用该方法的结果;如果没有的话,则默认验证成功并返回YES。我们可以在消息接收者类中实现validate<Key>:error:的方法来自定义逻辑返回YESNO

- (BOOL)validateValue:(id  _Nullable *)value 
               forKey:(NSString *)key 
                error:(NSError * _Nullable *)error;

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

示例
Person类中实现了validateName:error:方法,验证给name赋的值是不是jack

// ViewController.m
    Person *person = [[Person alloc] init];
    NSString *value = @"rose";
    NSString *key = @"name";
    NSError  *error;
    BOOL result = [person validateValue:&value forKey:key error:&error];
    
    if (error) {
        NSLog(@"error = %@", error);
        return;
    }
    NSLog(@"%d",result);

// Person.m
- (BOOL)validateName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
{
    NSString *name = *value;
    BOOL result = NO;
    if ([name isEqualToString:@"jack"]) {
        result = YES;
    }
    return result;
}
// 打印:0

备注: 默认情况下,KVC是不会自动验证属性的。

8. 搜索规则

除了了解KVC的使用,了解KVC取值和赋值过程的工作原理也是很有必要的。

基本的 Getter 搜索模式

以下是valueForKey:方法的默认实现,给定一个key作为输入参数,在消息接收者类中操作,执行以下过程。

  • ① 按照get<Key><key>is<Key>_<key>顺序查找方法。
    如果找到就调用取值并执行⑤,否则执行②;
  • ② 查找countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:命名的方法。
    如果找到第一个和后面两个中的至少一个,则创建一个能够响应所有NSArray的方法的集合代理对象(类型为NSKeyValueArray,继承自NSArray),并返回该对象。否则执行③;
    • 代理对象随后将其接收到的任何NSArray消息转换为countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes:消息的组合,并将其发送给KVC调用方。如果原始对象还实现了一个名为get<Key>:range:的可选方法,则代理对象也会在适当时使用该方法。
    • KVC调用方与代理对象一起工作时,允许底层属性的行为如同NSArray一样,即使它不是NSArray
  • ③ 查找countOf<Key>enumeratorOf<Key>memberOf<Key>:命名的方法。
    如果三个方法都找到,则创建一个能够响应所有NSSet的方法的集合代理对象(类型为NSKeyValueSet,继承自NSSet),并返回该对象。否则执行④;
    • 代理对象随后将其接收到的任何NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>:消息的组合,并将其发送给KVC调用方。
    • KVC调用方与代理对象一起工作时,允许底层属性的行为如同NSSet一样,即使它不是NSSet
  • ④ 查看消息接收者类的+accessInstanceVariablesDirectly方法的返回值(默认返回YES)。如果返回YES,就按照_<key>_is<Key><key>is<Key>顺序查找成员变量。如果找到就直接取值并执行⑤,否则执行⑥。如果+accessInstanceVariablesDirectly方法返回NO也执行⑥。
  • ⑤ 如果取到的值是一个对象指针,即获取的是对象,则直接将对象返回。
      如果取到的值是一个NSNumber支持的数据类型,则将其存储在NSNumber实例并返回。
      如果取到的值不是一个NSNumber支持的数据类型,则转换为NSValue对象, 然后返回。
  • ⑥ 调用valueForUndefinedKey:方法,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定key做一些特殊处理。

基本的 Setter 搜索模式

以下是setValue:forKey:方法的默认实现,给定keyvalue作为输入参数,尝试将KVC调用方的属性名为key的值设置为value,执行以下过程。

  • ① 按照set<Key>:_set<Key>:顺序查找方法。
    如果找到就调用并将value传进去(根据需要进行数据类型转换),否则执行②。
  • ② 查看消息接收者类的+accessInstanceVariablesDirectly方法的返回值(默认返回YES)。如果返回YES,就按照_<key>_is<Key><key>is<Key>顺序查找成员变量(同 基本的Getter搜索模式)。如果找到就将value赋值给它(根据需要进行数据类型转换),否则执行③。如果+accessInstanceVariablesDirectly方法返回NO也执行③。
  • ③ 调用setValue:forUndefinedKey:方法,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定key做一些特殊处理。

NSMutableArray 搜索模式

以下是mutableArrayValueForKey:方法的默认实现,给定一个key作为输入参数,返回属性名为key的集合的代理对象(这里指NSMutableArray对象),在消息接收者类中操作,执行以下过程。

  • ① 查找一对方法insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:
    (相当于NSMutableArray的原始方法insertObject:atIndex:removeObjectAtIndex:),
    或者insert<Key>:atIndexes:remove<Key>AtIndexes:
    (相当于NSMutableArray的原始方法insertObjects:atIndexes:removeObjectsAtIndexes:)。

    • 如果我们至少实现了一个insertion方法和一个removal方法,则返回一个代理对象,来响应发送给NSMutableArray的消息,通过发送insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:insert<Key>:atIndexes:remove<Key>AtIndexes:组合消息给KVC调用方。否则执行②。

    该代理对象类型为NSKeyValueFastMutableArray2,继承链为NSKeyValueFastMutableArray2->NSKeyValueFastMutableArray->NSKeyValueMutableArray->NSMutableArray

    • 如果我们也实现了一个可选的replace object方法,如replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:,代理对象在适当的情况下也会使用它们,以获得最佳性能。
  • ② 查找set<Key>:方法。
    如果找到,就会向KVC调用方发送一个set<Key>:消息,来返回一个响应NSMutableArray消息的代理对象。否则执行③。

    该代理对象类型为NSKeyValueSlowMutableArray,继承链为NSKeyValueSlowMutableArray->NSKeyValueMutableArray->NSMutableArray

注意:
此步骤中描述的机制比上一步的效率低得多,因为它可能重复创建新的集合对象,而不是修改现有的集合对象。因此,在设计自己的键值编码兼容对象时,通常应该避免使用它。
给代理对象发送NSMutableArray消息都会调用set<Key>:方法。即,对代理对象进行修改,都是调用set<Key>:来重新赋值,所以效率会低很多。

  • ③ 查看消息接收者类的+accessInstanceVariablesDirectly方法的返回值(默认返回YES)。如果返回YES,就按照_<key><key>顺序查找成员变量。如果找到就返回一个代理对象,该代理对象将接收所有NSMutableArray消息,通常是NSMutableArray或其子类。否则执行④。如果+accessInstanceVariablesDirectly方法返回NO也执行④。
  • ④ 返回一个可变的集合代理对象。当它接收到NSMutableArray消息时,发送一个valueForUndefinedKey:消息给KVC调用方,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定key做一些特殊处理。

其他搜索模式

除了以上三种,KVC还有NSMutableSetNSMutableOrderedSet两种搜索模式,它们的搜索规则和NSMutableArray相同,只是搜索和调用的方法不同。具体可以查看KVC官方文档 KVC - Accessor Search Patterns

9. 异常处理

  • ① 根据KVC搜索规则,当没有搜索到对应的key或者keyPath相关方法或者变量时,会调用对应的异常方法valueForUndefinedKey:setValue:forUndefinedKey:,这两个方法的默认实现是抛出异常NSUnknownKeyException,并导致程序Crash。我们可以重写这两个方法来处理异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
  • ② 当进行赋值如setValue:forKey:时,如果key的数据类型是非对象类型,则value就禁止传nil。否则会调用setNilValueForKey:方法,该方法的默认实现是抛出异常NSInvalidArgumentException,并导致程序Crash。我们可以重写这个方法来处理异常。
- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"hidden"]) {
        [self setValue:@(NO) forKey:@”hidden”];
    } else {
        [super setNilValueForKey:key];
    }
}

10. 相关面试题

Q:通过 KVC 修改属性会触发 KVO 吗?

会,通过KVC修改成员变量值也会触发KVO

Q:通过 KVC 键值编码技术是否会破坏面向对象的编程方法,或者说违背面向对象的编程思想呢?

valueForKey:setValue:forKey:这里面的key是没有任何限制的,当我们知道一个类或实例它内部的私有变量名称的情况下,我们在外界可以通过已知的key来对它的私有变量进行访问或者赋值的操作,从这个角度来讲KVC键值编码技术会违背面向对象的编程思想。

参考

Key-Value Coding Programming Guide(苹果官方文档)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 原文:iOS 关于KVC的一些总结 本文参考: KVC官方文档 KVC原理剖析 iOS KVC详解 KVC 简介 ...
    liyoucheng2014阅读 935评论 0 3
  • 本文参考: KVC官方文档 KVC原理剖析 iOS KVC详解 KVC 简介 KVC全称是Key Value Co...
    拧发条鸟xds阅读 5,284评论 6 23
  • KVC(Key-valuecoding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS...
    榕樹頭阅读 702评论 0 2
  • 什么是KVC? KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实是指iOS的开...
    祀梦_阅读 924评论 0 7
  • 今天戴老师留了一项特殊的作业,是什么呢?画画!戴老师让我们画一副有关《江南》的画。 回到家,我写完了作业,拿了一张...
    陈泉妡阅读 285评论 0 1