KVC详解

KVC简单介绍

KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名间接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定。

KVC在iOS中的定义

无论是Swift还是Objective-C,KVC的定义都是对NSObject的扩展来实现的(Objective-c中有个显式的NSKeyValueCoding类别名,而Swift没有,也不需要)所以对于所有继承了NSObject在类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的),下面是KVC最为重要的四个方法

// 直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;
// 通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
// 通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(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;

代码示例

Person类:
@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) int age;
@end

Controller类:
Person *p1 = [[Person alloc]init];
[p1 setValue:@"LianXi" forKeyPath:@"name"];
[p1 setValue:@"24" forKeyPath:@"age"];

Person *p2 = [[Person alloc]init];
[p2 setValue:@"XueXi" forKeyPath:@"name"];
[p2 setValue:@"25" forKeyPath:@"age"];

NSLog(@"%@, %@", [p1  valueForKeyPath:@"name"], [p1 valueForKeyPath:@"age"]);

使用KVC间接修改对象属性时,系统会自动判断对象属性的类型,并完成转换。如该程序中的“24”。
如果需要只生成name的数组,并打印应该怎么办?
常规方法:
NSArray *persons = @[p1, p2];
        
NSMutableArray *arrayM = [NSMutableArray array];
for (Person *p in persons) {
      [arrayM addObject:[p valueForKeyPath:@"name"]];
}
NSLog(@"%@", arrayM);
打印效果:(LianXi, XueXi)

KVC方法:
NSArray *persons = @[p1, p2];

NSMutableArray *arrayM = [NSMutableArray array];
[arrayM addObject:[persons valueForKeyPath:@"name"]];
NSLog(@"%@", arrayM);
打印效果:((LianXi, XueXi))

KVC在按照键值路径取值时,会自动层层深入,获取对应的键值。

Book类:
@interface Book : NSObject
@property(nonatomic, copy) NSString *bookname;
@end

Person类:
@class Book;
@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) int age;
@property(nonatomic, strong) Book *book;
@end

UIViewController类:
Person *p1 = [[Person alloc]init];
[p1 setValue:@"LianXi" forKeyPath:@"name"];
[p1 setValue:@"24" forKeyPath:@"age"];

Book *b1 = [[Book alloc] init];
b1.bookname = @"iPhone";
p1.book = b1;
 
Person *p2 = [[Person alloc]init];
[p2 setValue:@"XueXi" forKeyPath:@"name"];
[p2 setValue:@"25" forKeyPath:@"age"];

Book *b2 = [[Book alloc] init];
b2.bookname = @"Mac";
p2.book = b2;

// 建立一个存储person对象的数组,并打印
NSArray *persons = @[p1, p2];

NSArray *arrayM = [persons valueForKeyPath:@"book.bookname"];
NSLog(@"%@", arrayM);

KVC在内部是按什么样的顺序来寻找key的?

#######当调用setValue:属性值 forKey:@”name“的代码时,底层的执行机制如下:

  • 程序优先调用set<Key>:属性值方法,代码通过setter方法完成设置。注意,这里的<key>是指成员变量名,首字母大清写要符合KVC的全名规则。
  • 如果没有找到setName:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUNdefinedKey:方法,不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为<key>的成员变量,无论该变量是在类接口部分定义,还是在类实现部分定义,也无论用了什么样的访问修饰符,只在存在以<key>命名的变量,KVC都可以对该成员变量赋值。
  • 如果该类即没有set<Key>:方法,也没有_<key>成员变量,KVC机制会搜索_is<Key>的成员变量。
  • 如果该类即没有set<Key>:方法,也没有_<key>和_is<Key>成员变量,KVC机制再会继续搜索<key>和is<Key>的成员变量。再给它们赋值。
  • 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUNdefinedKey:方法,默认是抛出异常。
  • 如果开发者想让这个类禁用KVC里,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set<Key>:属性名时,会直接用setValue:forUNdefinedKey:方法。

代码示例

Person类:
@implementation Person
{
    NSString* toSetName;
    NSString* isName;
    //NSString* name;
    NSString* _name;
    NSString* _isName;
}
//- (void)setName:(NSString*)name {
//     toSetName = name;
// }
//- (NSString*)getName {
//    return toSetName;
//}
+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,该key不存在%@", key);
    return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
     NSLog(@"出现异常,该key不存在%@", key);
}
@end

Controller类:
Person *person = [Person new];
person setValue:@"newName" forKey:@"name"];
NSString *name = [person valueForKey:@"toSetName"];
NSLog(@"%@", name);

重写accessInstanceVariablesDirectly方法让其返回NO,再运行代码打印出:
2017-04-16 23:12:**.302 KVC: 出现异常,该key不存在name
2017-04-16 23:12:**.302 KVC: 出现异常,该key不存在toSetName
2017-04-16 23:12:**.302 KVC: (null)
这说明了重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO后, KVC找不到SetName:方法后,不再去找name系列成员变量,而是直接调用forUndefinedKey方法。

当调用ValueforKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:
  • 首先按get<Key>,<key>,is<Key>的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者int等值类型, 会做NSNumber转换
  • 如果上面的getter没有找到,KVC则会查找countOf<Key>,objectIn<Key>AtIndex,<Key>AtIndex格式的方法。如果countOf<Key>和另外两个方法中的要个被找到,那么就会返回一个可以响应NSArray所的方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex,<Key>AtIndex这几个方法组合的形式调用。还有一个可选的get<Ket>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
  • 如果上面的方法没有找到,那么会查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,以送给这个代理集合消息方法,就会以countOf<Key>,enumeratorOf<Key>,memberOf<Key>组合的形式调用。
  • 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:
  • 还没有找到的话,调用valueForUndefinedKey:

代码示例

TimesArray类:
@interface TimesArray : NSObject
- (void)incrementCount;
- (NSUInteger)countOfNumbers;
- (id)objectInNumbersAtIndex:(NSUInteger)index;
@end

@interface TimesArray()
@property(nonatomic, readwrite, assign) NSUInteger count;
@property(nonatomic, copy) NSString *arrName;
@end
@implementation TimesArray
- (void)incrementCount {
    self.count ++;
}
- (NSUInteger)countOfNumbers {
    return self.count;
}
- (id)objectInNumbersAtIndex:(NSUInteger)index {// 当key使用numbers时,KVC会找到这两个方法。
    return @(index * 2);
}
- (NSInteger)getNum { //第一个,自己一个一个注释试
    return 10;
}
- (NSInteger)num { //第二个
    return 11;
}
- (NSInteger)isNum { //第三个
    return 12;
}
@end

Controller类:
TimesArray *arr = [TimesArray new];

NSNumber *num = [arr valueForKey:@"num"];
NSLog(@"%@", num);
    
id ar = [arr valueForKey:@"numbers"];
NSLog(@"%@", NSStringFromClass([ar class]));
NSLog(@"0:%@ 1:%@ 2:%@ 3:%@", ar[0], ar[1], ar[2], ar[3]);
    
[arr incrementCount]; //count加1
NSLog(@"%lu",(unsigned long)[ar count]); //打印出1
    
[arr incrementCount]; //count再加1
NSLog(@"%lu", (unsigned long)[ar count]); //打印出2
    
[arr setValue:@"newName" forKey:@"arrName"];
NSString *name = [arr valueForKey:@"arrName"];
NSLog(@"%@", name);

代码打印出:
2017-04-20 11:03:08.325 KVC[86611:34004819] 10
2017-04-20 11:03:08.326 KVC[86611:34004819] NSKeyValueArray
2017-04-20 11:03:08.326 KVC[86611:34004819] 0:0 1:2 2:4 3:6 //太明显了,直接调用- (id)objectInNumbersAtIndex:(NSUInteger)index;方法
2017-04-20 11:03:08.326 KVC[86611:34004819] 1
2017-04-20 11:03:08.326 KVC[86611:34004819] 2
2017-04-20 11:03:08.326 KVC[86611:34004819] newName

KVC中使用KeyPath

然而在开发过程中,一个类的成员变量有可能是其他的自定义类,你可以先用KVC获取出来再该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径KeyPath。

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

代码示例

@interface Address : NSObject
@end

@interface Address()
@property(nonatomic, copy)NSString *country;
@end

@implementation Address
@end

@interface People : NSObject
@end

@interface People()
@property(nonatomic, copy) NSString *name;
@property(nonatomic, strong) Address *address;
@property(nonatomic, assign) NSInteger age;
@end

@implementation People
@end

UIViewController类:
People *people1 = [People new];
Address *add = [Address new];
add.country = @"China";
people1.address = add;
NSString *country1 = people1.address.country;
NSString * country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@   country2:%@",country1,country2);
[people1 setValue:@"USA" forKeyPath:@"address.country"];
country1 = people1.address.country;
country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@   country2:%@",country1,country2);

代码打印出: 
2017-04-16 23:12:**.302 KVC: country1:China   country2:China
2017-04-16 23:12:**.302 KVC: country1:USA   country2:USA

上面的代码简单在展示了KeyPath是怎么用的。如果你不小心错误的使用了key而非KeyPath的话,KVC会直接查找address.country这个属性,很明显,这个属性并不存在,所以会再调用UndefinedKey相关方法。而KVC对于KeyPath是搜索机制第一步就是分离key,用小数点.来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去。

KVC如何处理异常

KVC中最常见的异常就是不小心使用了错误的Key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常。
通常在用KVC操作Model时,抛出异常的那两个方法是需要重写的。虽然一般很小出现传递了错误的Key值这种情况,但是如果不小心出现了,直接抛出异常让APP崩溃显然是不合理的。
一般在这里直接让这个Key打印出来即可,或者有些特殊情况需要特殊处理。
通常情况下,KVC不允许你要在调用setValue:属性值 forKey:@”name“(或者keyPath)时对非对象传递一个nil的值。很简单,因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。

[people1 setValue:nil forKey:@"age"]
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<People 0x100200080> setNilValueForKey]: could not set nil as the value for the key age.' // 调用setNilValueForKey抛出异常

如果重写setNilValueForKey:就没问题了

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

代码打印出: 
2017-04-16 23:12:**.302 KVC: 不能将age设成nil

KVC处理非对象和自定义对象

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

代码示例

Address *add2 = [Address new];
add2.country = @"England";

[people1 setValue:add2 forKey:@"address"];
NSString *country1 = people1.address.country;
NSString *country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@ country2:%@", country1, country2);
代码打印出: 
2017-04-16 23:12:**.302 KVC: country1:England country2:England

KVC与容器类

对象的属性可以是一对一的,也可以是一对多的。一对多的属性要么是有序的(数组),要么是无序的(数组)

  • 不可变的有序容器属性(NSArray)和无序容器属性(NSSet)一般可以使用valueForKey:来获取。比如有一个叫items的NSArray属性,你可以用valurForKey:@"items"来获取这个属性。前面valueForKey:的key搜索模式中,我们发现其实KVC使用了一种更灵活的方式来管理容器类。
  • 而当对象的属性是可变的容器时,对于有序的容器,可以用下面的方法:
    - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
    该方法返回一个可变有序数组,如果调用该方法,KVC的搜索顺序如下
  • 搜索insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes 格式的方法
  • 如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableArray所有方法代理集合(类名是NSKeyValueFastMutableArray2),那么给这个代理集合发送NSMutableArray的方法,以insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes组合的形式调用。还有两个可选实现的接口:replaceOnjectAtIndex:withObject: , replace<Key>AtIndexes:with<Key>: 。
  • 如果上步的方法没有找到,则搜索set<Key>: 格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set<Key>:方法。 也就是说,mutableArrayValueForKey:取出的代理集合修改后,用·set<Key>:· 重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。
  • 如果上一步的方法还还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_<key>,<key>,的顺序搜索成员变量名,如果找到,那么发送的NSMutableArray消息方法直接交给这个成员变量处理。
  • 如果还是找不到,调用valueForUndefinedKey:
  • 关于mutableArrayValueForKey:的适用场景,我在网上找了很多,发现其一般是用在对NSMutableArray添加Observer上。
  • 如果对象属性是个NSMutableAArray、NSMutableSet、NSMutableDictionary等集合类型时,你给它添加KVO时,你会发现当你添加或者移除元素时并不能接收到变化。因为KVO的本质是系统监测到某个属性的内存地址或常量改变时,会添加上- (void)willChangeValueForKey:(NSString *)key
    和- (void)didChangeValueForKey:(NSString *)key方法来发送通知,所以一种解决方法是手动调用者两个方法,但是并不推荐,你永远无法像系统一样真正知道这个元素什么时候被改变。另一种便是利用使用mutableArrayValueForKey:了。

代码示例

@interface demo : NSObject
@property(nonatomic, strong) NSMutableArray *arr;
@end

@implementation demo
- (id)init {
    if (self == [super init]) {
        _arr = [NSMutableArray new];
        [self addObserver:self forKeyPath:@"arr" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    }
    return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    NSLog(@"%@",change);
}
- (void)dealloc {
    [self removeObserver:self forKeyPath:@"arr"]; //一定要在dealloc里面移除观察
}
- (void)addItem {
    [_arr addObject:@"1"];
}
- (void)addItemObserver {
    [[self mutableArrayValueForKey:@"arr"] addObject:@"1"];
}
- (void)removeItemObserver {
    [[self mutableArrayValueForKey:@"arr"] removeLastObject];
}
@end

然后再:
demo *d = [demo new];
[d addItem];
[d addItemObserver];
[d removeItemObserver];

打印结果:
2017-04-16 23:12:**.302 KVC: [32647:505864] {
    indexes = "<_NSCachedIndexSet: 0x100202c70>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 2;
    new =     (
        1
    );
}
2017-04-16 23:12:**.302 KVC: [32647:505864] {
    indexes = "<_NSCachedIndexSet: 0x100202c70>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 3;
    old =     (
        1
    );
}

从上面的代码可以看出,当只是普通地调用[_arr addObject:@"1"]时,Observer并不会回调,只有[[self mutableArrayValueForKey:@"arr"] addObject:@"1"];这样写时才能正确地触发KVO。打印出来的数据中,可以看出这次操作的详情,kind可能是指操作方法(我还不是很确认),old和new并不是成对出现的,当加添新数据时是new,删除数据时是old

而对于无序的容器,可以用下面的方法:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
该方法返回一个可变的无序数组如果调用该方法,KVC的搜索顺序如下:

  • 搜索addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key> 格式的方法
  • 如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableSet所有方法代理集合(类名是NSKeyValueFastMutableSet2),那么给这个代理集合发送NSMutableSet的方法,以addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key>组合的形式调用。还有两个可选实现的接口:intersect<Key> , set<Key>: 。
  • 如果reciever是ManagedObject,那么就不会继续搜索。
  • 如果上步的方法没有找到,则搜索set<Key>: 格式的方法,如果找到,那么发送给代理集合的NSMutableSet最终都会调用set<Key>:方法。 也就是说,mutableSetValueForKey取出的代理集合修改后,用set<Key>: 重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。
  • 如果上一步的方法还还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_<key>,<key>,的顺序搜索成员变量名,如果找到,那么发送的NSMutableSet消息方法直接交给这个成员变量处理。
  • 如果还是找不到,调用valueForUndefinedKey:
  • 可见,除了检查reciever是ManagedObject以外,其搜索顺序和mutableArrayValueForKey基本一至,
同样,它们也有对应的keyPath版本
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

iOS5和OSX10.7以后还有个mutableOrdered版本

- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key

这两种KVC的用法我还不是清楚,目前只能找到用于KVO的例子。

KVC和字典

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

KVC里面还有两个关于NSDictionary的方法

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

dictionaryWithValuesForKeys:是指输入一组key,返回这组key对应的属性,再组成一个字典。
setValuesForKeysWithDictionary是用来修改Model中对应key的属性。代码会更直观一点。

Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";
NSArray* arr = @[@"country",@"province",@"city",@"district"];
NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把对应key所有的属性全部取出来
NSLog(@"%@",dict);

NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
[add setValuesForKeysWithDictionary:modifyDict]; //用key Value来修改Model的属性
NSLog(@"country:%@  province:%@ city:%@",add.country,add.province,add.city);

//打印结果
2017-04-16 23:12:**.302 KVC: [6607:198900] {
    city = "Shen Zhen";
    country = China;
    district = "Nan Shan";
    province = "Guang Dong";
}
2017-04-16 23:12:**.302 KVC: country:USA  province:california city:Los angle
打印出来的结果完全符合预期。

KVC的内部实现机制

前面我们对析了KVC是怎么搜索key的。所以如果明白了key的搜索顺序,是可以自己写代码实现KVC的。在考虑到集合和keyPath的情况下,KVC的实现会比较复杂,我们只写代码实现最普通的取值和设值即可。

@interface NSObject(MYKVC)
- (void)setMyValue:(id)value forKey:(NSString*)key;
- (id)myValueforKey:(NSString*)key;
@end

@implementation NSObject(MYKVC)
- (void)setMyValue:(id)value forKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        return;
    }
    if ([value isKindOfClass:[NSNull class]]) {
        [self setNilValueForKey:key]; //如果需要完全自定义,那么这里需要写一个setMyNilValueForKey,但是必要性不是很大,就省略了
        return;
    }
    if (![value isKindOfClass:[NSObject class]]) {
        @throw @"must be s NSobject type";
        return;
    }

    NSString *funcName = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
        [self performSelector:NSSelectorFromString(funcName) withObject:value];
        return;
    }
    unsigned int count;
    BOOL flag = false;
    Ivar *vars = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i<count; i++) {
        Ivar var = vars[i];
        NSString *keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];

        if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
            flag = true;
            object_setIvar(self, var, value);
            break;
        }
        if ([keyName isEqualToString:key]) {
            flag = true;
            object_setIvar(self, var, value);
            break;
        }
    }
    if (!flag) {
        [self setValue:value forUndefinedKey:key];//如果需要完全自定义,那么这里需要写一个self setMyValue:value forUndefinedKey:key,但是必要性不是很大,就省略了
    }
}

- (id)myValueforKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        return [NSNull new]; //其实不能这么写的
    }
    //这里为了更方便,我就不做相关集合的方法查询了
    NSString* funcName = [NSString stringWithFormat:@"gett%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
       return [self performSelector:NSSelectorFromString(funcName)];
    }

    unsigned int count;
    BOOL flag = false;
    Ivar* vars = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i<count; i++) {
        Ivar var = vars[i];
        NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
        if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
            flag = true;
            return     object_getIvar(self, var);
            break;
        }
        if ([keyName isEqualToString:key]) {
            flag = true;
            return     object_getIvar(self, var);
            break;
        }
    }
    if (!flag) {
        [self valueForUndefinedKey:key];//如果需要完全自定义,那么这里需要写一个self myValueForUndefinedKey,但是必要性不是很大,就省略了
    }
   return [NSNull new]; //其实不能这么写的
}
@end

Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";

[add setMyValue:nil forKey:@"area"]; //测试设置 nil value
[add setMyValue:@"UK" forKey:@"country"];
[add setMyValue:@"South" forKey:@"area"];
[add setMyValue:@"300169" forKey:@"postCode"];
NSLog(@"country:%@  province:%@ city:%@ postCode:%@",add.country,add.province,add.city,add._postCode);
NSString* postCode = [add myValueforKey:@"postCode"];
NSString* country = [add myValueforKey:@"country"];
NSLog(@"country:%@ postCode: %@",country,postCode);

//打印结果:
2017-04-16 23:12:**.302 KVC: country:UK  province:South city:Shen Zhen postCode:300169
2017-04-16 23:12:**.302 KVC: country:UK postCode: 300169

上面就是自己写代码实现KVC的部分功能。其中我省略了自定义KVC错误方法,省略了部分KVC搜索key的步骤,但是逻辑是很清晰明了的,后面的测试也符合预期。当然这只是我自己实现KVC的思路,Apple也许并不是这么做的。

KVC的正确性验证

KVC提供了属性值,用来验证key对应的Value是否可用的方法
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
这个方法的默认实现是去探索类里面是否有一个这样的方法:-(BOOL)validate<Key>:error:如果有这个方法,就调用这个方法来返回,没有的话就直接返回YES

@implementation Address
- (BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{  //在implementation里面加这个方法,它会验证是否设了非法的value
    NSString* country = *value;
    country = country.capitalizedString;
    if ([country isEqualToString:@"Japan"]) {
        return NO;                                                                             //如果国家是日本,就返回NO,这里省略了错误提示,
    }
    return YES;
}
@end
NSError* error;
id value = @"japan";
NSString* key = @"country";
BOOL result = [add validateValue:&value forKey:key error:&error]; //如果没有重写-(BOOL)-validate<Key>:error:,默认返回Yes
if (result) {
    NSLog(@"键值匹配");
    [add setValue:value forKey:key];
}
else{
    NSLog(@"键值不匹配"); //不能设为日本,基他国家都行
}
NSString* country = [add valueForKey:@"country"];
NSLog(@"country:%@",country);
//打印结果 
2016-04-20 14:55:12.055 KVCDemo[867:58871] 键值不匹配
2016-04-20 14:55:12.056 KVCDemo[867:58871] country:China

如上面的代码,当开发者需要验证能不能用KVC设定某个值时,可以调用validateValue: forKey:这个方法来验证,如果这个类的开发者实现了-(BOOL)validate<Key>:error:这个方法,那么KVC就会直接调用这个方法来返回,如果没有,就直接返回YES,注意,KVC在设值时不会主动去做验证,需要开发者手动去验证。所以即使你在类里面写了验证方法,但是KVC因为不会去主动验证,所以还是能够设值成功。

KVC的使用

KVC在iOS开发中是绝不可少的利器,这种基于运行时的编程方式极大地提高了灵活性,简化了代码,甚至实现很多难以想像的功能,iOS开发中KVC的使用场景列举。

①动态地取值和设值

利用KVC动态的取值和设值是最基本的用途了。相信每一个iOS开发者都能熟练掌握。

②用KVC来访问和修改私有变量

对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的,请参考本文前面的Dog类的例子。

③Model和字典转换

这是KVC强大作用的又一次体现,请参考我写的iOS开发技巧系列---打造强大的BaseMod系列文章,里面
充分地运用了KVC和Objc的runtime组合的技巧,只用了短短数行代码就是完成了很多功能。

④修改一些控件的内部属性

这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些空间的API,这样我们就无法正常地访问和修改这些控件的样式。而KVC在大多数情况可下可以解决这个问题。最常用的就是个性化UITextField中的placeHolderText了。
下面演示如果修改placeHolder的文字样式。这里的关键点是如果获取你要修改的样式的属性名,也就是key或者keyPath名。
修改placeHolder的样式

⑤一般情况下可以运用runtime来获取Apple不想开放的属性名
let count:UnsafeMutablePointer<UInt32> =  UnsafeMutablePointer<UInt32>()
var properties = class_copyIvarList(UITextField.self, count)
while properties.memory.debugDescription !=  "0x0000000000000000"{
    let t = ivar_getName(properties.memory)
    let n = NSString(CString: t, encoding: NSUTF8StringEncoding)
    print(n) //打印出所有属性,这里我用了Swift语言
    properties = properties.successor()
}

//上面省略了部分属性
Optional(_disabledBackgroundView)
Optional(_systemBackgroundView)
Optional(_floatingContentView)
Optional(_contentBackdropView)
Optional(_fieldEditorBackgroundView)
Optional(_fieldEditorEffectView)
Optional(_displayLabel)
Optional(_placeholderLabel) //这个正是我想要修改的属性。
Optional(_dictationLabel)
Optional(_suffixLabel)
Optional(_prefixLabel)
Optional(_iconView)
//省略了部分属性

操作集合

Apple对KVC的valueForKey:方法作了一些特殊的实现,比如说NSArray和NSSet这样的容器类就实现了这些方法。所以可以用KVC很方便地操作集合
用KVC实现高阶消息传递

当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。

NSArray* arrStr = @[@"english",@"franch",@"chinese"];
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString* str  in arrCapStr) {
    NSLog(@"%@",str);
}
NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber* length  in arrCapStrLength) {
    NSLog(@"%ld",(long)length.integerValue);
}
打印结果
2017-04-16 23:12:**.302 KVC: English
2017-04-16 23:12:**.302 KVC: Franch
2017-04-16 23:12:**.302 KVC: Chinese
2017-04-16 23:12:**.302 KVC: 7
2017-04-16 23:12:**.302 KVC: 6
2017-04-16 23:12:**.302 KVC: 7

方法capitalizedString被传递到NSArray中的每一项,这样,NSArray的每一员都会执行capitalizedString并返回一个包含结果的新的NSArray。从打印结果可以看出,所有String都成功以转成了大写。
同样如果要执行多个方法也可以用valueForKeyPath:方法。它先会对每一个成员调用 capitalizedString方法,然后再调用length,因为lenth方法返回是一个数字,所以返回结果以NSNumber的形式保存在新数组里。
用KVC中的函数操作集合

KVC同时还提供了很复杂的函数,主要有下面这些

①简单集合运算符

简单集合运算符共有@avg, @count , @max , @min ,@sum5种,都表示啥不用我说了吧, 目前还不支持自定义。

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

@implementation Book
@end

Book *book1 = [Book new];
book1.name = @"The Great Gastby";
book1.price = 22;
Book *book2 = [Book new];
book2.name = @"Time History";
book2.price = 12;
Book *book3 = [Book new];
book3.name = @"Wrong Hole";
book3.price = 111;

Book *book4 = [Book new];
book4.name = @"Wrong Hole";
book4.price = 111;

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);

打印结果
2017-04-16 23:12:**.302 KVC: sum:256.000000
2017-04-16 23:12:**.302 KVC: avg:64.000000
2017-04-16 23:12:**.302 KVC: count:4.000000
2017-04-16 23:12:**.302 KVC: min:12.000000
2017-04-16 23:12:**.302 KVC: max:111.000000
②对象运算符

比集合运算符稍微复杂,能以数组的方式返回指定的内容,一共有两种:
@distinctUnionOfObjects

@unionOfObjects
它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。
用法如下:

NSLog(@"distinctUnionOfObjects");
NSArray *arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
for (NSNumber *price in arrDistinct) {
    NSLog(@"%f", price.floatValue);
}
NSLog(@"unionOfObjects");
NSArray *arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
for (NSNumber *price in arrUnion) {
    NSLog(@"%f", price.floatValue);
}

2017-04-16 23:12:**.302 KVC: distinctUnionOfObjects
2017-04-16 23:12:**.302 KVC: 111.000000
2017-04-16 23:12:**.302 KVC: 12.000000
2017-04-16 23:12:**.302 KVC: 22.000000
2017-04-16 23:12:**.302 KVC: unionOfObjects
2017-04-16 23:12:**.302 KVC: 22.000000
2017-04-16 23:12:**.302 KVC: 12.000000
2017-04-16 23:12:**.302 KVC: 111.000000
2017-04-16 23:12:**.302 KVC: 111.000000

前者会将重复的价格去除后返回所有价格,后者直接返回所有的图书价格。
③Array和Set操作符

这种情况更复杂了,说的是集合中包含集合的情况,我们执行了如下的一段代码:
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets
@distinctUnionOfArrays:该操作会返回一个数组,这个数组包含不同的对象,不同的对象是在从关键路径到操作器右边的被指定的属性里
@unionOfArrays 该操作会返回一个数组,这个数组包含的对象是在从关键路径到操作器右边的被指定的属性里和@distinctUnionOfArrays不一样,重复的对象不会被移除
@distinctUnionOfSets 和@distinctUnionOfArrays类似。因为Set本身就不支持重复。

------来自网络转载

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

推荐阅读更多精彩内容

  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    Fendouzhe阅读 527评论 0 6
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    黑暗中的孤影阅读 49,364评论 74 440
  • 简介 KVC(Key-value coding)键值编码,翻译一下就是指iOS的开发中,可以允许开发者通过Key名...
    6ffd6634d577阅读 1,150评论 1 9
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    朽木自雕也阅读 1,404评论 6 1
  • KVC(Key-value coding)键值编码,iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,...
    CALayer_Sai阅读 2,393评论 0 4