KVC的简单使用

2017-07-11  本文已影响0人  CALayer_Sai

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

KVC在iOS中的定义

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

- (nullableid)valueForKey:(NSString*)key;//直接通过Key来取值

- (void)setValue:(nullableid)value forKey:(NSString*)key;//通过Key来设值

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

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

当然NSKeyValueCoding类别中还有其他的一些方法,下面列举一些

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

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

KVC是怎么寻找Key的

KVC是怎么使用的,我相信绝大多数的开发者都很清楚,我在这里就不再写简单的使用KVC来设值和取值的代码了,首页我们来探讨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机制

首先我们先重写accessInstanceVariablesDirectly方法让其返回NO,再运行代码(注意上面注释的部分),XCode直接打印出

这说明了重写+(BOOL)accessInstanceVariablesDirectly方法让其返回NO后,KVC找不到SetName:方法后,不再去找name系列成员变量,而是直接调用forUndefinedKey方法

所以开发者如果不想让自己的类实现KVC,就可以这么做。

下面那两个setter和gettr的注释取消掉,再把

NSString*name= [dog valueForKey:@"toSetName"]; 换成 NSString*name= [dog valueForKey:@"name"];

下面再注释到accessInstanceVariablesDirectly方法,就能测试其他的key查找顺序了,为了节省篇幅,剩下的的KVC对于key寻找机制就不在这里展示了,有兴趣的读者可以写代码去验证。

当调用ValueforKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下

首先按get,,is的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者int等值类型, 会做NSNumber转换

如果上面的getter没有找到,KVC则会查找countOf,objectInAtIndex,AtIndex格式的方法。如果countOf和另外两个方法中的要个被找到,那么就会返回一个可以响应NSArray所的方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送NSArray的方法,就会以countOf,objectInAtIndex,AtIndex这几个方法组合的形式调用。还有一个可选的get:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。

如果上面的方法没有找到,那么会查找countOf,enumeratorOf,memberOf格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,以送给这个代理集合消息方法,就会以countOf,enumeratorOf,memberOf组合的形式调用。

如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_,_is,,is的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:

还没有找到的话,调用valueForUndefinedKey:

下面再上代码测试

很明显,上面的代码充分说明了说明了KVC在调用ValueforKey:@”name“时搜索key的机制。不过还有些功能没有全部列出,有兴趣的读者可以写代码去验证。

在KVC中使用KeyPath

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

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

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

//打印结果

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:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。

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

不是每一个方法都返回对象,但是valueForKey:总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber或者NSValue对象。这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开以者需要手动转换成原来的类型。尽管valueForKey:会自动将值类型封装成对象,但是setValue:forKey:却不行。你必须手动将值类型转换成NSNumber或者NSValue类型,才能传递过去。

对于自定义对象,KVC也会正确以设值和取值。因为传递进去和取出来的都是id类型,所以需要开发者自己担保类型的正确性,运行时Objective-C在发送消息的会检查类型,如果错误会直接抛出异常。

KVC与容器类

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

不可变的有序容器属性(NSArray)和无序容器属性(NSSet)一般可以使用valueForKey:来获取。比如有一个叫items的NSArray属性,你可以用valurForKey:@"items"来获取这个属性。前面valueForKey:的key搜索模式中,我们发现其实KVC使用了一种更灵活的方式来管理容器类。苹果的官方文档也推荐我们实现这些这些特殊的访问器。

而当对象的属性是可变的容器时,对于有序的容器,可以用下面的方法:

- (NSMutableArray*)mutableArrayValueForKey:(NSString *)key;

该方法返回一个可变有序数组,如果调用该方法,KVC的搜索顺序如下

搜索insertObject:inAtIndex:,removeObjectFromAtIndex:或者insertAdIndexes,removeAtIndexes格式的方法

如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableArray所有方法代理集合(类名是NSKeyValueFastMutableArray2),那么给这个代理集合发送NSMutableArray的方法,以insertObject:inAtIndex:,removeObjectFromAtIndex:或者insertAdIndexes,removeAtIndexes组合的形式调用。还有两个可选实现的接口:replaceOnjectAtIndex:withObject: , replaceAtIndexes:with: 。

如果上步的方法没有找到,则搜索set:格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set:方法。 也就是说,mutableArrayValueForKey:取出的代理集合修改后,用·set:· 重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。

如果上一步的方法还还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_,,的顺序搜索成员变量名,如果找到,那么发送的NSMutableArray消息方法直接交给这个成员变量处理。

如果还是找不到,调用valueForUndefinedKey:

关于mutableArrayValueForKey:的适用场景,我在网上找了很多,发现其一般是用在对NSMutableArray添加Observer上。

如果对象属性是个NSMutableAArray、NSMutableSet、NSMutableDictionary等集合类型时,你给它添加KVO时,你会发现当你添加或者移除元素时并不能接收到变化。因为KVO的本质是系统监测到某个属性的内存地址或常量改变时,会添加上- (void)willChangeValueForKey:(NSString *)key

和- (void)didChangeValueForKey:(NSString *)key方法来发送通知,所以一种解决方法是手动调用者两个方法,但是并不推荐,你永远无法像系统一样真正知道这个元素什么时候被改变。另一种便是利用使用mutableArrayValueForKey:了。

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

而对于无序的容器,可以用下面的方法:

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

该方法返回一个可变的无序数组如果调用该方法,KVC的搜索顺序如下

搜索addObjectObject:,removeObject:或者add,remove格式的方法

如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableSet所有方法代理集合(类名是NSKeyValueFastMutableSet2),那么给这个代理集合发送NSMutableSet的方法,以addObjectObject:,removeObject:或者add,remove组合的形式调用。还有两个可选实现的接口:intersect , set:。

如果reciever是ManagedObject,那么就不会继续搜索。

如果上步的方法没有找到,则搜索set: 格式的方法,如果找到,那么发送给代理集合的NSMutableSet最终都会调用set:方法。 也就是说,mutableSetValueForKey取出的代理集合修改后,用set:重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。

如果上一步的方法还还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_,,的顺序搜索成员变量名,如果找到,那么发送的NSMutableSet消息方法直接交给这个成员变量处理。

如果还是找不到,调用valueForUndefinedKey:

可见,除了检查reciever是ManagedObject以外,其搜索顺序和mutableArrayValueForKey基本一至,

同样,它们也有对应的keyPath版本

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

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

KVC和字典

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

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

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

- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

dictionaryWithValuesForKeys:是指输入一组key,返回这组key对应的属性,再组成一个字典。

setValuesForKeysWithDictionary是用来修改Model中对应key的属性。下面直接用代码会更直观一点

KVC的内部实现机制

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

KVC的使用

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

动态地取值和设值

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

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

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

Model和字典转换

Model就是MVC和MVVM最前面的M,显然Model的重要性不言而喻。只有在将网络&数据库获取的数据正确转化成Model后,才能更好地服务ViewController和View。通常--Model是应用逻辑层的对象,如 Account、Order 等等。这些对象是你开发的应用程序中的一些核心对象,负责应用的逻辑计算和诸多与业务相关的方法和操作。首先Model将未处理的数据转化成Model后,再传给ViewController,再传给ViewController再将处理好的Model数据显示到View上去。相反View产生的数据可也可以转化为Model,通过ViewConroller传到Model层处理后再保存&更新。在iOS开发中,Model还可以分为胖Model和瘦Model。当然,这些东西都不在本文的讨论范围之内。本文讨论的是如何增强Model的一些功能,这些功能并不是业务逻辑上的功能,而是让Model可以自动实现一些代码层面的功能。可以降低我们的代码量,大量减少重复的代码。

-(NSString)descprition{    

return 你对自定义类的各个变量的描述,从而可以打印出来

}

修改一些控件的内部属性

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

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

参考:http://www.jianshu.com/p/45cbd324ea65

上一篇下一篇

猜你喜欢

热点阅读