==================================================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来设值


 


 


其他的方法


 



+ (BOOL)accessInstanceVariablesDirectly;


//默认返回YES,表示如果没有找到Set方法的话,会按照_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,那么可以用这个方法来返回



- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

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

- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;

- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;


- (nullable id)valueForUndefinedKey:(NSString *)key;


//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常


- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;


//和上一个方法一样,只不过是设值。


- (void)setNilValueForKey:(NSString *)key;


//如果你在SetValue方法时面给Value传nil,则会调用这个方法


- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;


//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。



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

 


 


 


上面的这些方法在碰到特殊情况或者有特殊需求还是会用到的,所以也是可以了解一下。后面的代码示例会有讲到其中的一些方法。

同时苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。建议有基础的或者英文好的开发者直接去看苹果的官方文档,相信你会对KVC的理解更上一个台阶。(本人一直认为英文文档太繁琐,一直没有看过,有空看看)


 



KVC是怎么寻找Key的

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

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

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

如果开发者想让这个类禁用KVC里,那么重写​​+ (BOOL)accessInstanceVariablesDirectly​​方法让其返回NO即可,这样的话如果KVC没有找到​​set:​​属性名时,会直接用​​setValue:forUNdefinedKey:​​方法。

 

在KVC中使用KeyPath

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



 


 




1


2




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


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



 

KVC如何处理异常

KVC中最常见的异常就是不小心使用了错误的Key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常。

通常在用KVC操作Model时,抛出异常的那两个方法是需要重写的。虽然一般很小出现传递了错误的Key值这种情况,但是如果不小心出现了,直接抛出异常让APP崩溃显然是不合理的。

一般在这里直接让这个Key打印出来即可,或者有些特殊情况需要特殊处理。

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



 


 




1


2




  [people1 setValue:nil forKey:@"age"]


   *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[ setNilValueForKey]: could not set nil as the value for the key age.' // 调用setNilValueForKey抛出异常



如果重写​​setNilValueForKey:​​就没问题了



 


 




1


2


3


4


5


6


7


8


9




@implementation People


 


-(void)setNilValueForKey:(NSString *)key{


    NSLog(@"不能将%@设成nil",key);


}


 


@end


//打印出


2016-04-17 16:19:55.298 KVCDemo[1304:92472] 不能将age设成nil



 

KVC的内部实现机制

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



 


 




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21


22


23


24


25


26


27


28




@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



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

 

KVC的使用

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

动态地取值和设值

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

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

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

Model和字典转换

充分地运用了KVC和Objc的runtime组合的技巧,只用了短短数行代码就是完成了很多功能。

修改一些控件的内部属性

这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些空间的API,这样我们就无法正常地访问和修改这些控件的样式。而KVC在大多数情况可下可以解决这个问题。最常用的就是个性化UITextField中的placeHolderText了。

下面演示如果修改placeHolder的文字样式。这里的关键点是如果获取你要修改的样式的属性名,也就是key或者keyPath名。



修改placeHolder的样式


一般情况下可以运用runtime来获取Apple不想开放的属性名



 


 




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21


22


23




let count:UnsafeMutablePointer =  UnsafeMutablePointer()


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:​​将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。



 


 




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16




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


}


打印结果


2016-04-20 16:29:14.239 KVCDemo[1356:118667] English


2016-04-20 16:29:14.240 KVCDemo[1356:118667] Franch


2016-04-20 16:29:14.240 KVCDemo[1356:118667] Chinese


2016-04-20 16:29:14.240 KVCDemo[1356:118667] 7


2016-04-20 16:29:14.241 KVCDemo[1356:118667] 6


2016-04-20 16:29:14.241 KVCDemo[1356:118667] 7



方法​​capitalizedString​​被传递到NSArray中的每一项,这样,NSArray的每一员都会执行​​capitalizedString​​并返回一个包含结果的新的NSArray。从打印结果可以看出,所有String都成功以转成了大写。

同样如果要执行多个方法也可以用​​valueForKeyPath:​​方法。它先会对每一个成员调用 ​​capitalizedString​​方法,然后再调用length,因为lenth方法返回是一个数字,所以返回结果以NSNumber的形式保存在新数组里。

用KVC中的函数操作集合

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

①简单集合运算符

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



 


 




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21


22


23


24


25


26


27


28


29


30


31


32


33


34


35


36


37


38


39


40




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


 


打印结果


2016-04-20 16:45:54.696 KVCDemo[1484:127089] sum:256.000000


2016-04-20 16:45:54.697 KVCDemo[1484:127089] avg:64.000000


2016-04-20 16:45:54.697 KVCDemo[1484:127089] count:4.000000


2016-04-20 16:45:54.697 KVCDemo[1484:127089] min:12.000000


2016-04-20 16:45:54.697 KVCDemo[1484:127089] max:111.000000



②对象运算符

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

​@distinctUnionOfObjects​

​@unionOfObjects​

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

用法如下:



 


 




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20




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


}


 


2016-04-20 16:47:34.490 KVCDemo[1522:128840] distinctUnionOfObjects


2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000


2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000


2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000


2016-04-20 16:47:34.490 KVCDemo[1522:128840] unionOfObjects


2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000


2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000


2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000


2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000



前者会将重复的价格去除后返回所有价格,后者直接返回所有的图书价格。(因为只返回价格,没有返回图书,感觉用处不大。)

③Array和Set操作符

这种情况更复杂了,说的是集合中包含集合的情况,我们执行了如下的一段代码:

@distinctUnionOfArrays

@unionOfArrays

@distinctUnionOfSets

​@distinctUnionOfArrays:​​该操作会返回一个数组,这个数组包含不同的对象,不同的对象是在从关键路径到操作器右边的被指定的属性里

​@unionOfArrays​​ 该操作会返回一个数组,这个数组包含的对象是在从关键路径到操作器右边的被指定的属性里和@distinctUnionOfArrays不一样,重复的对象不会被移除

​@distinctUnionOfSets​​ 和​​@distinctUnionOfArrays​​类似。因为Set本身就不支持重复。

============================================kvo=======kvo=============================================================================================================


KVO的是KeyValue Observe的缩写,中文是键值观察。这是一个典型的观察者模式,观察者在键值改变时会得到通知。iOS中有个Notification的机制,也可以获得通知,但这个机制需要有个Center,相比之下KVO更加简洁而直接。




Key-Value Observing (KVO) 建立在 KVC 之上,它能够观察一个对象的 KVC key path 值的变化。

  KVO的使用也很简单,就是简单的3步。


      1.注册需要观察的对象的属性addObserver:forKeyPath:options:context:
      2.实现observeValueForKeyPath:ofObject:change:context:方法,这个方法当观察的属性变化时会自动调用
      3.取消注册观察removeObserver:forKeyPath:context:



 


==================================应用举例=======================================================


 


 应用:修改textField的placeholder的字体颜色、大小


  1. textField.placeholder = @"username is in here!";  
  2. [textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];  
  3. [textField setValue:[UIFont boldSystemFontOfSize:16] forKeyPath:@"_placeholderLabel.font"];  

这里是使用了KVC的方式

 


=====================================更多内容参考我的其他博客===========================================================================================================================