iOS开发技巧系列---详解KVC(我告诉你KVC的一切)
KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的。目前网上关于KVC的文章在非常多,有的只是简单地说了下用法,有的讲得深入但是在使用场景和最佳实践没有说明,我写下这遍文章就是给大家详解一个最完整最详细的KVC。 KVC在iOS中的定义 无论是Swift还是Objective-C,KVC的定义都是对NSObject的扩展来实现的(Objective-c中有个显式的NSKeyValueCoding类别名,而Swift没有,也不需要)所以对于所有继承了NSObject在类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的),下面是KVC最为重要的四个方法 - (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值 - (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值 - (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值 当然NSKeyValueCoding类别中还有其他的一些方法,下面列举一些 + (BOOL)accessInstanceVariablesDirectly; //默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索 - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; //KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。 - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; //这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回 - (nullable id)valueForUndefinedKey:(NSString *)key; //如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常 - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; //和上一个方法一样,只不过是设值。 - (void)setNilValueForKey:(NSString *)key; //如果你在SetValue方法时面给Value传nil,则会调用这个方法 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys; //输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。 上面的这些方法在碰到特殊情况或者有特殊需求还是会用到的,所以也是可以了解一下。后面的代码示例会有讲到其中的一些方法。 同时苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。建议有基础的或者英文好的开发者直接去看苹果的官方文档,相信你会对KVC的理解更上一个台阶。 KVC是怎么寻找Key的 KVC是怎么使用的,我相信绝大多数的开发者都很清楚,我在这里就不再写简单的使用KVC来设值和取值的代码了,首页我们来探讨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:方法。 下面我们来让代码来测试一下上面的KVC机制 @interface Dog : NSObject @end @implementation Dog { 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 int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... Dog* dog = [Dog new]; [dog setValue:@"newName" forKey:@"name"]; NSString* name = [dog valueForKey:@"toSetName"]; NSLog(@"%@",name); } ; } 首先我们先重写accessInstanceVariablesDirectly方法让其返回NO,再运行代码(注意上面注释的部分),XCode直接打印出 -- :::] 出现异常,该key不存在name -- :::] 出现异常,该key不存在toSetName -- :::] (null) 这说明了重写+(BOOL)accessInstanceVariablesDirectly方法让其返回NO后,KVC找不到SetName:方法后,不再去找name系列成员变量,而是直接调用forUndefinedKey方法 所以开发者如果不想让自己的类实现KVC,就可以这么做。 下面那两个setter和gettr的注释取消掉,再把 NSString* name = [dog valueForKey:@"toSetName"]; 换成 NSString* name = [dog valueForKey:@"name"]; XCode就可以正确地打印出正确的值了 -- :::] newName 下面再注释到accessInstanceVariablesDirectly方法,就能测试其他的key查找顺序了,为了节省篇幅,剩下的的KVC对于key寻找机制就不在这里展示了,有兴趣的读者可以写代码去验证。 当调用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: 下面再上代码测试 @interface TwoTimesArray : NSObject -(void)incrementCount; -(NSUInteger)countOfNumbers; -(id)objectInNumbersAtIndex:(NSUInteger)index; @end @interface TwoTimesArray() @property (nonatomic,readwrite,assign) NSUInteger count; @property (nonatomic,copy) NSString* arrName; @end @implementation TwoTimesArray -(void)incrementCount{ self.count ++; } -(NSUInteger)countOfNumbers{ return self.count; -(id)objectInNumbersAtIndex:(NSUInteger)index{ //当key使用numbers时,KVC会找到这两个方法。 ); } -(NSInteger)getNum{ //第一个,自己一个一个注释试 ; } -(NSInteger)num{ //第二个 ; } -(NSInteger)isNum{ //第三个 ; } @end int main(int argc, const char * argv[]) { @autoreleasepool { TwoTimesArray* arr = [TwoTimesArray new]; NSNumber* num = [arr valueForKey:@"num"]; NSLog(@"%@",num); id ar = [arr valueForKey:@"numbers"]; NSLog(@"%@",NSStringFromClass([ar class])); NSLog(],ar[],ar[],ar[]); [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); } ; } //打印结果 -- :::] -- :::] NSKeyValueArray -- :::] : : : : //太明显了,直接调用-(id)objectInNumbersAtIndex:(NSUInteger)index;方法 -- :::] -- :::] -- :::] newName 很明显,上面的代码充分说明了说明了KVC在调用ValueforKey:@”name“时搜索key的机制。不过还有些功能没有全部列出,有兴趣的读者可以写代码去验证。 在KVC中使用KeyPath 然而在开发过程中,一个类的成员变量有可能是其他的自定义类,你可以先用KVC获取出来再该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径KeyPath。 - (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过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 int main(int argc, const char * argv[]) { @autoreleasepool { 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); } ; } //打印结果 -- :::] country1:China country2:China -- :::] 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 //打印出 -- :::] 不能将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); //打印结果 -- :::] 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:"]; } -(void)addItemObserver{ [[self mutableArrayValueForKey:"]; } -(void)removeItemObserver{ [[self mutableArrayValueForKey:@"arr"] removeLastObject]; } @end 然后再: demo* d = [demo new]; [d addItem]; [d addItemObserver]; [d removeItemObserver]; 打印结果 -- :::] { indexes = "<_NSCachedIndexSet: 0x100202c70>[number of indexes: 1 (in 1 ranges), indexes: (1)]"; kind = ; new = ( ); } -- :::] { indexes = "<_NSCachedIndexSet: 0x100202c70>[number of indexes: 1 (in 1 ranges), indexes: (1)]"; kind = ; old = ( ); } 从上面的代码可以看出,当只是普通地调用[_arr addObject:"];这样写时才能正确地触发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); //打印结果 -- :::] { city = "Shen Zhen"; country = China; district = "Nan Shan"; province = "Guang Dong"; } -- :::] 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{ ) { 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); ; i<count; i++) { Ivar var = vars[i]; NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:]; 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{ ) { 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); ; i<count; i++) { Ivar var = vars[i]; NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:]; 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:" 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); //打印结果: -- :::] country:UK province:South city:Shen Zhen postCode: -- :::] country:UK postCode: 上面就是自己写代码实现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); //打印结果 -- :::] 键值不匹配 -- :::] country:China 如上面的代码,当开发者需要验证能不能用KVC设定某个值时,可以调用validateValue: forKey:这个方法来验证,如果这个类的开发者实现了-(BOOL)validate<Key>:error:这个方法,那么KVC就会直接调用这个方法来返回,如果没有,就直接返回YES,注意,KVC在设值时不会主动去做验证,需要开发者手动去验证。所以即使你在类里面写了验证方法,但是KVC因为不会去主动验证,所以还是能够设值成功。 KVC的使用 KVC在iOS开发中是绝不可少的利器,这种基于运行时的编程方式极大地提高了灵活性,简化了代码,甚至实现很多难以想像的功能,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) //下面省略了部分属性 可以从里面看到其他还有很多东西可以修改,运用KVC设值可以获得自己想要的效果。 操作集合 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); } 打印结果 -- :::] English -- :::] Franch -- :::] Chinese -- :::] -- :::] -- :::] 方法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 = ; Book *book2 = [Book new]; book2.name = @"Time History"; book2.price = ; Book *book3 = [Book new]; book3.name = @"Wrong Hole"; book3.price = ; Book *book4 = [Book new]; book4.name = @"Wrong Hole"; book4.price = ; 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); 打印结果 -- :::] sum:256.000000 -- :::] avg:64.000000 -- :::] count:4.000000 -- :::] min:12.000000 -- :::] 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); } -- :::] distinctUnionOfObjects -- :::] 111.000000 -- :::] 12.000000 -- :::] 22.000000 -- :::] unionOfObjects -- :::] 22.000000 -- :::] 12.000000 -- :::] 111.000000 -- :::] 111.000000 前者会将重复的价格去除后返回所有价格,后者直接返回所有的图书价格。(因为只返回价格,没有返回图书,感觉用处不大。) ③Array和Set操作符 这种情况更复杂了,说的是集合中包含集合的情况,我们执行了如下的一段代码: @distinctUnionOfArrays @unionOfArrays @distinctUnionOfSets @distinctUnionOfArrays:该操作会返回一个数组,这个数组包含不同的对象,不同的对象是在从关键路径到操作器右边的被指定的属性里 @unionOfArrays 该操作会返回一个数组,这个数组包含的对象是在从关键路径到操作器右边的被指定的属性里和@distinctUnionOfArrays不一样,重复的对象不会被移除 @distinctUnionOfSets 和@distinctUnionOfArrays类似。因为Set本身就不支持重复。 KVO 你没看错,KVO是基于KVC实现的。那么是怎么用KVC实现KVO的呢,请期待下章。 总结 本文全方位介绍了KVC的原理和各种用法。相信读者看完后对会KVC会有更完全的理解,也会在项目里更好的运用KVC。其实这里面所有的东西在官方文档里都有详细的讲解说明。只不过全是英文的,我也看过几遍,但是英语不好会看得很吃力,比如官方在介绍@distinctUnionOfArrays时的那句话我想了很好久也不是很明白,而且官方的示例代码也做得不够好,所以很难找出某些功能的适用场景。但我还是推荐各位开发者能够学好英语去看官方文档。再结合StackOverFlow和Google。真的可以解决绝大多数开发中碰到的难题了。这篇文章就到这里,下篇我向大家介绍KVO。
iOS开发技巧系列---详解KVC(我告诉你KVC的一切)的更多相关文章
- iOS开发技巧系列---使用链式编程和Block来实现UIAlertView
UIAlertView是iOS开发过程中最常用的控件之一,是提醒用户做出选择最主要的工具.在iOS8及后来的系统中,苹果更推荐使用UIAlertController来代替UIAlertView.所以本 ...
- iOS开发——GCD多线程详解
GCD多线程详解 1. 什么是GCD Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,简单来说,GCD就是iOS一套解决多线程的机制,使用GCD能够最大限度简化多线程 ...
- iOS开发——开发技巧&LLDB详解
开胃小菜--简单的断点调试 在xcode中打开一个app,在想要break的行号上单击,即可生成一个深色的箭头标识--断点.如下图,在viewDidLoad:中设置了断点. 运行app,等待...就可 ...
- iOS开发-继承特征详解
面向对象的三大特性:封装,继承,多态. 1.继承 继承既能保证类的完整,又能简化代码. 把公共的方法和实例变量写在子类,子类只需要写父类独有的实例变量和方法即可. 继承是面向对象三大特性之一,合理的继 ...
- ios开发之--UIViewContentMode详解
在开发当中有时会有这样的需求,将从服务器端下载下来的图片添加到imageView 当中展示,但是下载下来的图片尺寸大小不固定,宽高也有可能不成比例 如果直接设置imageView的image属性而不设 ...
- C#二次开发BIMFACE系列61 File Management文件管理服务接口二次开发及实战详解
系列目录 [已更新最新开发文章,点击查看详细] 在我的博客<C#二次开发BIMFACE系列61 File Management文件管理服务接口二次开发及实战详解>最后列出了 Fil ...
- iOS中—触摸事件详解及使用
iOS中--触摸事件详解及使用 (一)初识 要想学好触摸事件,这第一部分的基础理论是必须要学会的,希望大家可以耐心看完. 1.基本概念: 触摸事件 是iOS事件中的一种事件类型,在iOS中按照事件划分 ...
- ios新特征 ARC详解
IOS ARC 分类: IOS ARC2013-01-17 09:16 2069人阅读 评论(0) 收藏 举报 目录(?)[+] 关闭工程的ARC(Automatic Reference Co ...
- Linux驱动开发必看详解神秘内核(完全转载)
Linux驱动开发必看详解神秘内核 完全转载-链接:http://blog.chinaunix.net/uid-21356596-id-1827434.html IT168 技术文档]在开始步入L ...
随机推荐
- HTTP协议中的长连接和短连接(keep-alive状态)
什么是长连接 HTTP1.1规定了默认保持长连接(HTTP persistent connection ,也有翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包.不四次握手),等待在同 ...
- Android的AutoCompleteTextView在API17高版本添加的setText函数在低版本系统居然能正常调用?官方文档是不是不靠谱了?
官方文档:https://developer.android.com/reference/android/widget/AutoCompleteTextView.html#setText(java.l ...
- DataSet DataTable操作
DataSet ds = new DataSet(); DataTable dt = new DataTable("OrderList"); ...
- C++一些特殊的类的设计
一.设计一个只能在栈上分配空间的类 重写类的opeator new 操作,并声明为private,一个大概的代码如下: class StackOnly { public: StackOnly(){ ...
- ASP.NET MVC中使用ASP.NET AJAX异步访问WebService
使用过ASP.NET AJAX的朋友都知道,怎么通过ASP.NET AJAX在客户端访问WebService,其实在ASP.NET MVC中使用ASP.NET AJAX异步访问WebService 也 ...
- C语言相关图书推荐
C Primer Plus(第5版 中文版) 作 者 [美] 普拉塔(Prata S.) 著:云巅工作室 编 出 版 社 人民邮电出版社 出版时间 2005-02-01 版 次 1 ...
- 从lighttpd学到的代码技巧
平时写开脚本,很多时候我们都可以不怎样注意效率,但是看c代码的时候,你会发现,才意思自己真的是一个coder啦 1,单位转换 (根据传入的数返回相应的kb,mb,gb等等) 可能我们直觉来想就会这样做 ...
- NetAdvantage 笔记
1.UltraControlBase Class Members 1.BeginUpdate Method Sets the IsUpdating flag to true which prevent ...
- 时间都去哪了?——安卓GTD工具
GTD是英文Getting Things Done的缩写,是一种行为管理的方法,也是David Allen写的一本书的书名. GTD的主要原则在于一个人需要通过记录的方式把头脑中的各种任务移出来.通过 ...
- centos5 vim升级到7.4
vim在centos中的版本为7.0,导致很多插件都无法使用,所以想到升级一下. wget ftp://ftp.vim.org/pub/vim/unix/vim-7.4.tar.bz2 tar jvz ...