KVC实现原理剖析

1、KVC简介

KVC全称是Key Value Coding,定义在NSKeyValueCoding.h文件中,翻译成中文是键值码,是由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问,这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。KVC的定义是通过NSObject的拓展类来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,所以可以说在日常开发中凡是直接或间接继承或自NSObject的对象都可以使用KVC机制。

2、KVC的基础使用

2.1、KVC处理对象属性

KVC通过 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设置为给定值。setValue:forKey:默认实现会自动把表示标量和结构体的 NSNumberNSValue 对象解包,并赋值给属性。如果指定 key 所对应的属性没有对应的 setter 实现,则该对象会向自身发送 setValue:forUndefinedKey:消息,而该消息的默认实现会抛出一个NSUndefinedKeyException 的异常。但是子类可以重写此方法以自定义方式处理请求

看下面的例子:

@interface Person : NSObject{
     // NSString *name;
    // NSString *_name;
    // NSString *_isName;
    // NSString *isName;
}
@property (nonatomic, copy) NSString *name;
@end

@implementation Person
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}
@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Person *person = [[Person alloc]init];
        [person setValue:@"刘德华" forKey:@"name"];
        NSLog(@"name:%@",[person valueForKey:@"name"]);
    }
    return 0;
}

打印结果: 2020-03-05 17:00:44.647980+0800 KVCDemo[47069:2104780] name:刘德华

你一定注意到了这段代码中Person类中的注释部分的代码,经过小编验证不论是以何种形式,程序输出的结果都是正确的。这里涉及到的其实KVC的设置和取值规则,会在下面的章节中讲解到KVC的设置和取值原理。

2.2、KVC使用KeyPath

在开发过程中,一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC获取该属性,然后再次用KVC来获取这个自定义类的属性, 但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径keyPath。顾名思义,就是按照路径寻找key。

  • 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 路径序列中不符合特定键的键值编码的任何对象(即,默认实现valueForKey:无法找到访问器方法)均会接收到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: 消息。

看下面例子:

@interface Student : NSObject
@property (nonatomic, copy) NSString *name;

@end
@implementation Student

@end

@interface Person : NSObject{
    Student *_student;
}

@end
@implementation Person


@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        //方式一
        Person *person = [[Person alloc]init];
        Student *student = [[Student alloc]init];
        [person setValue:student forKey:@"student"];
        [student setValue:@"小明" forKey:@"name"];
        NSString *name = [[person valueForKey:@"student"] valueForKey:@"name"];
        NSLog(@"name:%@",name);
        
        //方式二
        [person setValue:@"小小" forKeyPath:@"student.name"];
        NSLog(@"name:%@",[person valueForKeyPath:@"student.name"]);
    }
    return 0;
}

打印结果:
2020-03-05 17:28:35.709176+0800 KVCDemo[47571:2124832] name:小明
2020-03-05 17:28:35.709646+0800 KVCDemo[47571:2124832] name:小小

从打印结果来看我们成功的通过keyPath设置了student的值。 KVC对于keyPath搜索机制第一步就是分离key,用小数点.来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去。

2.3、KVC处理数值和结构体类型属性

在前面小节中的内容都是对对象类型的变量进行的设值取值,那么如果变量类型是数值类型或者是结构,那么KVC是否也是可以进行设置和取值的呢?答案是肯定的。如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber或者NSValue对象。这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开发者需要手动转换成原来的类型。尽管valueForKey:会自动将值类型封装成对象,但是 setValue:forKey:却不行。你必须手动将值类型转换成NSNumber或者NSValue类型,才能传递过去。因为传递进去和取出来的都是id类型,所以需要开发者自己担保类型的正确性,运行时Objective-C在发送消息的会检查类型,如果错误会直接抛出异常。

看下面例子:

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

@interface Person : NSObject

@property (nonatomic, assign) NSInteger age;

@property (nonatomic) ThreeFloats threeFloats;

@end
@implementation Person

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        //处理数值类型
        Person *person = [[Person alloc]init];
        [person setValue:[NSNumber numberWithInteger:20] forKey:@"age"];
        NSLog(@"age:%@", [person valueForKey:@"age"]);

        //处理结构体
        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 th;
        [reslut getValue:&th];
        NSLog(@"%f - %f - %f", th.x, th.y, th.z);
    }
    return 0;
}

打印结果:
2020-03-05 17:49:12.490826+0800 KVCDemo[47898:2139908] age:20
2020-03-05 17:49:12.491413+0800 KVCDemo[47898:2139908] {length = 12, bytes = 0x0000803f0000004000004040}
2020-03-05 17:49:12.491461+0800 KVCDemo[47898:2139908] 1.000000 - 2.000000 - 3.000000

需要注意的是我们不能直接将一个数值通过KVC赋值的,我们需要把数据转为NSNumber和NSValue类型传入,那到底哪些类型数据要用NSNumber封装哪些类型数据要用NSValue封装呢?看下面这些方法的参数类型就知道了:
可以使用NSNumber的数据类型有:

+ (NSNumber)numberWithChar:(char)value;
+ (NSNumber
)numberWithUnsignedChar:(unsignedchar)value;
+ (NSNumber)numberWithShort:(short)value;
+ (NSNumber
)numberWithUnsignedShort:(unsignedshort)value;
+ (NSNumber)numberWithInt:(int)value;
+ (NSNumber
)numberWithUnsignedInt:(unsignedint)value;
+ (NSNumber)numberWithLong:(long)value;
+ (NSNumber
)numberWithUnsignedLong:(unsignedlong)value;
+ (NSNumber)numberWithLongLong:(longlong)value;
+ (NSNumber
)numberWithUnsignedLongLong:(unsignedlonglong)value;
+ (NSNumber)numberWithFloat:(float)value;
+ (NSNumber
)numberWithDouble:(double)value;
+ (NSNumber)numberWithBool:(BOOL)value;
+ (NSNumber
)numberWithInteger:(NSInteger)valueNS_AVAILABLE(10_5,2_0);
+ (NSNumber*)numberWithUnsignedInteger:(NSUInteger)valueNS_AVAILABLE(10_5,2_0);

可以使用NSValue的数据类型有:

+ (NSValue)valueWithCGPoint:(CGPoint)point;
+ (NSValue
)valueWithCGSize:(CGSize)size;
+ (NSValue)valueWithCGRect:(CGRect)rect;
+ (NSValue
)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue
)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);

2.4、KVC处理集合

KVC提供了对集合类型处理的方法。

  • mutableArrayValueForKey:mutableArrayValueForKeyPath:
    这些返回行为像NSMutableArray对象的代理对象。
  • mutableSetValueForKey:mutableSetValueForKeyPath:
    这些返回行为像NSMutableSet对象的代理对象。
  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:
    这些返回行为像NSMutableOrderedSet对象的代理对象。

看下面的例子:

@interface Person : NSObject

@property (nonatomic, copy) NSArray *classArr;
@property (nonatomic, copy) NSString *name;

@end
@implementation Person

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Person *person = [[Person alloc]init];
        person.classArr = @[@"Chinese",@"Mathematics",@"English"];
        [person setValue:@"小明" forKey:@"name"];
        [person mutableArrayValueForKey:@"classArr"];
        NSLog(@"name:%@,class:%@",[person valueForKey:@"name"],[person mutableArrayValueForKey:@"classArr"]);
       
    }
    return 0;
}

打印结果:
2020-03-05 22:01:58.540062+0800 KVCDemo[52012:2424108] name:小明,class:(
Chinese, Mathematics,English)

这里只演示了mutableArrayValueForKey的使用,其他的方法使用类同,在这里就不多赘述了。

2.5、KVC处理集合运算符

KVC同时还提供了集合运算符,利用这些集合运算符可以针对集合做一些高效的统计运算。这些集合运算符主要分为三大类,如下所示:

聚合操作符

  • @avg: 返回集合中指定对象属性的平均值
  • @count: 返回集合中指定对象属性的个数
  • @max: 返回集合中指定对象属性的最大值
  • @min: 返回集合中指定对象属性的最小值
  • @sum: 返回集合中指定对象属性值之和

数组操作符

  • @distinctUnionOfObjects: 返回集合中指定对象属性的集合,且会进行去重操作
  • @unionOfObjects: 返回集合中指定对象属性的集合,并不会删除相同元素。

嵌套操作符

  • @distinctUnionOfArrays: 返回指定的属性相对应的所有集合的组合的不同对象集合,并会删除相同的元素
  • @unionOfArrays: 返回指定的属性相对应的所有集合的组合的不同对象集合,但是不会删除相同元素
  • @distinctUnionOfSets: 返回指定的属性相对应的所有集合的组合中的不同对象集合,并删除相同元素,返回的是 NSSet

看下面的例子:

@interface Book : NSObject
@property (nonatomic, copy)  NSString *name;
@property (nonatomic, assign)  CGFloat price;
@end

@implementation Book
@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Book *book1 = [Book new];
        book1.name = @"编程珠玑";
        book1.price = 50;
        Book *book2 = [Book new];
        book2.name = @"Java编程思想";
        book2.price = 20;
        Book *book3 = [Book new];
        book3.name = @"漫画算法";
        book3.price = 30;

        Book *book4 = [Book new];
        book4.name = @"算法图解";
        book4.price = 30;

        NSArray *arrBooks = @[book1, book2, book3, book4];
        //计算总价
        NSNumber *sum = [arrBooks valueForKeyPath:@"@sum.price"];
        NSLog(@"sum:%f", sum.floatValue);
        //计算平均价格
        NSNumber *avg = [arrBooks valueForKeyPath:@"@avg.price"];
        NSLog(@"avg:%f", avg.floatValue);
        //计算书本数量
        NSNumber *count = [arrBooks valueForKeyPath:@"@count"];
        NSLog(@"count:%f", count.floatValue);
        //计算最小的书本价格
        NSNumber *min = [arrBooks valueForKeyPath:@"@min.price"];
        NSLog(@"min:%f", min.floatValue);
        //计算最大的书本价格
        NSNumber *max = [arrBooks valueForKeyPath:@"@max.price"];
        NSLog(@"max:%f", max.floatValue);
        //返回书本的价格集合
        NSArray *distinctPrice = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
        NSLog(@"distinctPrice:%@", distinctPrice);
        //返回书本的价格集合
        NSArray *unionPrice = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
        NSLog(@"unionPrice:%@", unionPrice);
        
        NSArray *arr1 = @[book1,book2];
        NSArray *arr2 = @[book3,book4];
        NSArray *arr = @[arr1,arr2];
        NSArray *collectedDistinctPrice = [arr valueForKeyPath:@"@distinctUnionOfArrays.price"];
        NSLog(@"collectedDistinctPrice:%@", collectedDistinctPrice);
        NSArray *collectedPrice = [arr valueForKeyPath:@"@unionOfArrays.price"];
        NSLog(@"collectedPrice:%@", collectedPrice);
    }
    return 0;
}

打印结果:
2020-03-05 22:55:37.927846+0800 KVCDemo[52895:2466440] sum:130.000000
2020-03-05 22:55:37.928381+0800 KVCDemo[52895:2466440] avg:32.500000
2020-03-05 22:55:37.928467+0800 KVCDemo[52895:2466440] count:4.000000
2020-03-05 22:55:37.928526+0800 KVCDemo[52895:2466440] min:20.000000
2020-03-05 22:55:37.928568+0800 KVCDemo[52895:2466440] max:50.000000
2020-03-05 22:55:37.928693+0800 KVCDemo[52895:2466440] distinctPrice:(
20,30,50)
2020-03-05 22:55:37.928783+0800 KVCDemo[52895:2466440] unionPrice:(
50,20,30,30)
2020-03-05 22:55:37.928865+0800 KVCDemo[52895:2466440] collectedDistinctPrice:(
20,30, 50)
2020-03-05 22:55:37.928931+0800 KVCDemo[52895:2466440] collectedPrice:(
50, 20,30,30)

2.5、KVC处理字典

当对NSDictionary对象使用KVC时,valueForKey的表现行为和objectForKey:一样。所以使用valueForKeyPath:用来访问多层嵌套的字典是比较方便的。

  • 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.
    【译】使用字典键标识属性,然后使用字典中的对应值来设置该消息接收者的属性值。默认实现会对每一个键值对调用 setValue:forKey:。设置时需要将 nil 替换成 NSNull。

看下面的例子:

@interface Address : NSObject
@property (nonatomic, copy) NSString *country;
@property (nonatomic, copy) NSString *province;
@property (nonatomic, copy) NSString *city;
@property (nonatomic, copy) NSString *district;
@end

@implementation Address

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        //模型转字典
        Address *address = [Address new];
        address.country = @"China";
        address.province = @"Guang Dong";
        address.city = @"Shen Zhen";
        address.district = @"Nan Shan";
        NSArray *arr = @[@"country", @"province", @"city", @"district"];
        NSDictionary *dict = [address dictionaryWithValuesForKeys:arr];
        NSLog(@"%@", dict);

        //字典转模型
        NSDictionary *modifyDict = @{ @"country": @"China", @"province": @"Guang Dong", @"city": @" Shen Zhen", @"district": @"Nan Shan" };
        [address setValuesForKeysWithDictionary:modifyDict];            //用key Value来修改Model的属性
        NSLog(@"country:%@  province:%@ city:%@ district:%@", address.country, address.province, address.city, address.district);
    }
    return 0;
}

打印结果:
2020-03-05 23:07:20.645255+0800 KVCDemo[53037:2474708] {
city = "Shen Zhen";
country = China;
district = "Nan Shan";
province = "Guang Dong";
}
2020-03-05 23:07:20.646032+0800 KVCDemo[53037:2474708] country:China province:Guang Dong city: Shen Zhen district:Nan Shan

2.6、KVC处理异常

在使用KVC开发的过程中难免会出现一些失误,诸如写错了key或者在设置的时候传递了nil的值,KVC中专门提供了处理这些异常的方法。

2.6.1、KVC处理nil异常

通常情况下,KVC不允许你要在调用setValue:forKey:或者setValue:forKeyPath: 时对非对象传递一个nil的值。很简单,因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。
看下面例子:

@interface Person : NSObject
{
    int age;
}

@end

@implementation Person

- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能将%@设成nil", key);
}

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person setValue:nil forKey:@"age"];
        NSLog(@"age:%@", [person valueForKey:@"age"]);
    }
    return 0;
}

打印结果:
2020-03-05 23:17:24.713116+0800 KVCDemo[53187:2481241] 不能将age设成nil
2020-03-05 23:17:24.713661+0800 KVCDemo[53187:2481241] age:0

2.6.2、处理UndefinedKey异常

通常情况下,KVC不允许你要在调用setValue:forKey:或者setValue:forKeyPath:时对不存在的key进行操作。 否则会报错发生崩溃,重写setValue: forUndefinedKey:valueForUndefinedKey:方法避免崩溃。
看下面的例子:

@interface Person : NSObject

@end

@implementation Person

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,该key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,该key不存在%@", key);
}

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person setValue:nil forKey:@"age"];
        NSLog(@"age:%@", [person valueForKey:@"age"]);
    }
    return 0;
}

2.7、KVC键值验证(Key-Value Validation)

KVC提供了验证Key对应的Value是否可用的方法,调用validateValue:forKey:error:(或validateValue:forKeyPath:error:)方法时,协议的默认实现会在接收验证消息的对象(或keyPath的对象)中根据key搜索是否有方法validate:error:实现。如果对象没有这种方法,则默认情况下验证成功,并且默认实现返回YES。当存在特定于属性的验证方法时,默认实现将返回调用该方法的结果。
由于特定于属性的验证方法通过引用接收值和错误参数,因此验证具有三种可能的结果:

  • 验证成功,返回 YES,value不做修改。
  • 验证失败,返回 NO,value不做修改,如果调用者提供了 NSError 的话,就把错误引用设置为指示错误原因的NSError对象。
  • 验证失败,返回 YES,但是创建了一个新的有效的属性值作为替代。在返回之前,该方法将值引用修改为指向新值对象。 进行修改时,即使值对象是可变的,该方法也总是创建一个新对象,而不是修改旧对象。

看下面的例子:

@interface Person : NSObject
{
    int age;
}

@end

@implementation Person
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *_Nullable __autoreleasing *)outError {
    NSNumber *age = *ioValue;
    if (age.integerValue == 10) {
        return NO;
    }
    return YES;
}

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        NSNumber *age = @10;
        NSError *error;
        NSString *key = @"age";
        BOOL isValid = [person validateValue:&age forKey:key error:&error];
        if (isValid) {
            NSLog(@"键值匹配");
            [person setValue:age forKey:key];
        } else {
            NSLog(@"键值不匹配");
        }

        NSLog(@"age:%@", [person valueForKey:@"age"]);
    }
    return 0;
}

打印结果:
2020-03-05 23:42:22.811163+0800 KVCDemo[53621:2503057] 键值不匹配
2020-03-05 23:42:22.811752+0800 KVCDemo[53621:2503057] age:0

3、KVC设值和取值原理

在前面的章节中的我们探索了KVC的基本使用,但是还是不知道KVC的设值和取值规则,只有把这些规则都弄清楚了,才能在实际开发中得心应手。
KVC的设值和取值规则针对于对象类型、可变数组、可变有序集、可变集的规则有所不同。

3.1、基础Getter搜索模式

这是valueForKey:的默认实现,给定一个key当做输入参数,开始下面的步骤,在这个接收valueForKey:方法调用的类内部进行操作。

  1. 通过getter方法搜索实例,例如get,,is,_的拼接方案。按照这个顺序,如果发现符合的方法,就调用对应的方法并拿着结果跳转到第五步。否则,就继续到下一步。
  2. 如果没有找到简单的getter方法,则搜索其匹配模式的方法countOfobjectInAtIndex:AtIndexes:。如果找到其中的第一个和其他两个中的一个,则创建一个集合代理对象,该对象响应所有NSArray的方法并返回该对象。否则,继续到第三步。代理对象随后将NSArray接收到的countOfobjectInAtIndex:AtIndexes:的消息给符合KVC规则的调用方。当代理对象和KVC调用方通过上面方法一起工作时,就会允许其行为类似于NSArray一样。
  3. 如果没有找到NSArray简单存取方法,或者NSArray存取方法组。则查找有没有countOfenumeratorOfmemberOf:命名的方法。如果找到三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回。否则,继续执行第四步。此代理对象随后转换countOfenumeratorOfmemberOf:方法调用到创建它的对象上。实际上,这个代理对象和NSSet一起工作,使得其表象上看起来是NSSet
  4. 如果没有发现简单getter方法,或集合存取方法组,以及接收类方法accessInstanceVariablesDirectly是返回YES的。搜索一个名为__isis的实例,根据他们的顺序。如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步,否则,跳转到第六步。
  5. 如果取回的是一个对象指针,则直接返回这个结果。如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。
  6. 如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法。

3.2、基础Setter搜索模式

这是setValue:forKey:的默认实现,给定输入参数valuekey。试图在接收调用对象的内部,设置属性名为keyvalue,通过下面的步骤:

  1. 查找set:或_set命名的setter,按照这个顺序,如果找到的话,调用这个方法并将值传进去(根据需要进行对象转换)。
  2. 如果没有发现一个简单的setter,但是accessInstanceVariablesDirectly类属性返回YES,则查找一个命名规则为__isis的实例变量。根据这个顺序,如果发现则将value赋值给实例变量。
  3. 如果没有发现setter或实例变量,则调用setValue:forUndefinedKey:方法,并默认提出一个异常,但是一个NSObject的子类可以提出合适的行为。

3.3、NSMutableArray搜索模式

这是mutableArrayValueForKey:的默认实现,给一个key当做输入参数。在接收访问器调用的对象中,返回一个名为key的可变代理数组,这个代理数组就是用来响应外界KVO的对象,通过下面的步骤进行查找:

  1. 查找一对方法insertObject:inAtIndex:removeObjectFromAtIndex:(相当于NSMutableArray的原始方法insertObject:atIndex:removeObjectAtIndex:)或者方法名是insert:atIndexes:removeAtIndexes:(相当于NSMutableArray的原始方法insertObjects:atIndexes:removeObjectsAtIndexes:)。如果找到最少一个insert方法和最少一个remove方法,则返回一个代理对象,来响应发送给NSMutableArray的组合消息insertObject:inAtIndex:removeObjectFromAtIndex:insert:atIndexes:,和removeAtIndexes:消息。当对象接收一个mutableArrayValueForKey:消息并实现可选替换方法,例如replaceObjectInAtIndex:withObject:replaceAtIndexes:with:方法,代理对象会在适当的情况下使用它们,以获得最佳性能。
  2. 如果对象没有可变数组方法,查找一个替代方法,命名格式为set:。在这种情况下,向mutableArrayValueForKey:的原始响应者发送一个set:消息,来返回一个代理对象来响应NSMutableArray事件。
  3. 如果没有可变数组的方法,也没有找到访问器,但接受响应的类accessInstanceVariablesDirectly属性返回YES,则查找一个名为_的实例变量。按照这个顺序,如果找到实例变量,则返回一个代理对象。改对象将接收所有NSMutableArray发送过来的消息,通常是NSMutableArray或其子类。
  4. 如果所有情况都失败,则返回一个可变的集合代理对象。当它接收NSMutableArray消息时,发送一个setValue:forUndefinedKey:消息给接收mutableArrayValueForKey:消息的原始对象。这个setValue:forUndefinedKey:的默认实现是提出一个NSUndefinedKeyException异常,但是子类可以重写这个实现。

3.3、其他

还有NSMutableSetNSMutableOrderedSet两种搜索模式,这两种搜索模式和NSMutableArray步骤相同,只是搜索和调用的方法不同。详细的搜索方法都可以在KVC官方文档中找到,再套用上面的流程即可理解。

4、自定义KVC

在上一个章节中我们分析了KVC的设值和取值规则,那么便可以遵照规则定义自己的KVC。自定义KVC主要是基于取值和设值两个方面考虑。

4.1、自定义KVC设值

自定义KVC设值还是在setValue:forKey:方法上面做文章,大致思路如下:

  1. 首先需要判断传进来的key是否为nil,如果为nil则直接返回,否则执行第2步;
  2. 找到相关方法 set_setsetIs是否有实现,如果有实现的话这直接调用这些方法,否则执行第3步;
  3. 判断accessInstanceVariablesDirectly方法的返回结果,如果返回NO,抛出异常,否则执行第4步;
  4. 按照__isis顺序查找成员变量,如果找到了则直接赋值,否则执行第5步;
  5. 如果程序执行到这一步则说明按照搜索规则没有找到相应的key,则直接抛出异常。

主要代码如下:

- (void)ds_setValue:(nullable id)value forKey:(NSString *)key{
    
    // 1:非空判断一下
    if (key == nil  || key.length == 0) return;
    
    // 2:找到相关方法 set _set setIs
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self ds_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"执行 %@ 方法",setKey);
        return;
    }else if ([self ds_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"执行 %@ 方法",_setKey);
        return;
    }else if ([self ds_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"执行 %@ 方法",setIsKey);
        return;
    }
    
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4.找相关实例变量进行赋值,按照这个顺序查找_ _is  is
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        // 4.2 获取相应的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 对相应的 ivar 设置值
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:_isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:key]) {
       Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }
    // 5:如果找不到相关实例
    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

判断方法是否实现:

- (BOOL)ds_performSelectorWithMethodName:(NSString *)methodName value:(id)value {
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

获取实例变量数组的方法如下:

- (NSMutableArray *)getIvarListName {
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@", ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}

4.2、自定义KVC取值

自定义KVC设值还是在lg_valueForKey:方法上面做文章,大致思路如下:

  1. 首先判断key是否为nil,如果为nil,则直接返回,否则执行第2步;
  2. 按照顺序找到相关方法getcountOfobjectInAtIndex是否实现,如果有其中一个实现则直接调用这个方法,否则执行第3步;
  3. 判断accessInstanceVariablesDirectly方法的返回结果,如果返回NO,抛出异常,否则执行第4步;
  4. 按照__isis顺序查找成员变量,如果找到了则直接取值,否则执行第5步;
  5. 如果执行到这一步,则返回空。
    主要代码如下:
- (nullable id)ds_valueForKey:(NSString *)key {
    // 1:刷选key 判断非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相关方法 get  countOf  objectInAtIndex
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@", Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@", Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:", Key];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    } else if ([self respondsToSelector:NSSelectorFromString(key)]) {
        return [self performSelector:NSSelectorFromString(key)];
    } else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]) {
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i < num - 1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }
            for (int j = 0; j < num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }
#pragma clang diagnostic pop

    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****", self] userInfo:nil];
    }

    // 4.找相关实例变量进行赋值 按照顺序查找是否实现_ _is  is
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    NSString *_key = [NSString stringWithFormat:@"_%@", key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@", Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@", Key];
    if ([mArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);
    } else if ([mArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);
    } else if ([mArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);
    } else if ([mArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);
    }
    return @"";
}

在这里的自定义是比较简洁的一种写法,并不够完善,如果有兴趣的可以阅读DIS_KVC_KVO的源码,对KVC和KVO都有比较全面详细的自定义。

5、参考资料

苹果官方文档-KVC

iOS KVC和KVO详解

你可能感兴趣的:(KVC实现原理剖析)