(IOS)KVC

KVC 简介

KVC全称是Key Value Coding(键值编码),是一个基于NSKeyValueCoding非正式协议实现的机制,它可以直接通过key值对对象的属性进行存取操作,而不需通过调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定。

KVC提供了一种间接访问属性方法或成员变量的机制,可以通过字符串来访问对象的的属性方法或成员变量。

在实现了访问器方法的类中,使用点语法和KVC访问对象其实差别不大,二者可以任意混用。但是没有访问器方法的类中,点语法无法使用,这时KVC就有优势了。

KVC和KVO都是基于OC的动态特性和Runtime机制的。

KVC 通用的访问方法

1.通用的访问方法:

getter方法:valueForKey:

setter方法:setValue:forKey:

2.衍生的keyPath方法,用来进行深层访问(key使用点语法),也可单层访问:

keyPath的setter方法:setValue: forKeyPath:

keyPath的getter方法:valueForKeyPath:

示例:

Address.h:

#import

@interface Address : NSObject

@property (copy, nonatomic) NSString *city;

@property (copy, nonatomic) NSString *street;

@end

Person.h:

#import

#import "Address.h"

@interface Person : NSObject

@property (copy, nonatomic) NSString *name;

@property (assign, nonatomic) NSInteger *sex;

@property (strong, nonatomic) NSNumber *age;

@property (strong, nonatomic) Address *address;

@end

- (void)viewDidLoad {

    [super viewDidLoad];

    Person *myself = [[Person alloc] init];

    [myself setValue:@"xds" forKey:@"name"];

    NSLog(@"-------name = %@",myself.name);

    NSLog(@"-------name = %@",[myself valueForKey:@"name"]);

    /**

    keyPath的setter方法:setValue: forKeyPath:

    keyPath的getter方法:valueForKeyPath:

    keyPath为多级访问,使用点语法

    */

    //注意,这里要想使用keypath对adress的属性进行赋值,必须先给myself赋一个Address对象

    Address *myAddress = [[Address alloc] init];

    [myself setValue:myAddress forKey:@"address"];

    //KeyPath为多级访问

    [myself setValue:@"rizhao" forKeyPath:@"address.city"];

    NSLog(@"-------city = %@",myself.address.city);

    NSLog(@"-------city = %@",[myself valueForKeyPath:@"address.city"]);

}

keypath

除了对当前对象的属性进行赋值外,还可以对其更“深层”的对象进行访问。

keypath可以访问到array数组中所有存储的对象的属性,前提是对象类型是一样的。

例如下面例子中,通过valueForKeyPath:将数组中所有对象的name属性值取出,并放入一个数组中返回。

NSArray *names = [array valueForKeyPath:@"name"];

KVC 的多值操作

批量取值操作

KVC还有更强大的功能,可以根据给定的一组key,获取到一组value,并且以字典的形式返回。

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

批量赋值操作

同样,也可以通过KVC进行批量赋值。使用对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含key、value的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给User对象的属性赋值。

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

示例

示例如下:

//批量赋值NSDictionary *dic = @{

                      @"name":@"xiaoMing",

                      @"sex":@1,

                      @"age":@12,

                      @"address":myAddress

                      };

[myself setValuesForKeysWithDictionary:dic];

//批量取值NSArray *keys = @[@"name",@"age",@"sex",@"address"];

NSDictionary *values = [myself dictionaryWithValuesForKeys:keys];

NSLog(@"%@",values);

输出:

2018-08-25 15:45:57.316650+0800 KVC[1145:231458] {

    address = "";

    age = "";

    name = xds;

    sex = 0;

}

使用 KVC 进行字典转模型

可以使用setValuesForKeysWithDictionary: 进行字典转模型。

假如传过来的Jason数据如下所示,注意到age传过来的的key为Age,这样在字典转模型时会报错,因为这个Age在模型类里面并没有定义。

JSON数据:

{

    @"name":@"xiaoMing",

    @"sex":@1,

    @"Age":@12

}

我们可以在Person里重写 setValue:(id)value forUndefinedKey: 方法,这个方法是针对碰到未定义的key时怎么办的方法。

#import "Person.h"

@implementation Person

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

    if ([key isEqualToString:@"Age"]) {

        [self setValue:value forKey:@"age"];

    }

}

@end

注意点

Json里的字段数量和字段名字应该和model类所匹配,Json少了字段不会出现问题,但是多了或者是字段名不对,就会崩溃。

不需对基本数据类型做处理,例如int型转NSNumber,内部会自动作出处理

NSArray和NSDictionary等集合对象,value都不能是nil,否则会导致Crash

异常信息和异常处理

当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个NSUndefinedKeyException的异常,并且应用程序Crash。

我们可以重写下面两个方法,根据业务需求合理的处理KVC导致的异常:

在取值时,未有对应的key:

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

在赋值时,未有对应的key:

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

当通过 KVC 给某个非对象的属性赋值为 nil 时,例如使用 setValue:forkey 给 int 或 float 类型属性赋为 nil 时,会抛出 NSInvalidArgumentException 的异常并崩溃。

我们可以重写下面的方法,进行处理:

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

示例:

当我们为Person类的NSInteger类型的sex属性赋nil时,会报错

[myself setValue:nil forKey:@"sex"];

Person.m 重写 setNilValueForKey: 方法

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

    if ([key isEqualToString:@"sex"]) {

        [self setValue:@1111 forKey:@"sex"];

    }else{

        [super setNilValueForKey:key];

    }

}

集合属性操作

当我们要操作一个对象里的集合属性时(NSArray、NSSet等),我们可以通过KVC方法取到集合属性,然后通过集合属性操作集合中的元素。实际开发中最常用的方法,叫做间接操作。

直接操作要实现下面的方法,但通常不会用到:

有序集合对应方法如下:

-countOf//必须实现,对应于NSArray的基本方法count:2  -objectInAtIndex:

-AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get:range://不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:

-insertObject:inAtIndex:

-insert:atIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFromAtIndex:

-removeAtIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectInAtIndex:withObject:

-replaceAtIndexes:with://可选的,如果在此类操作上有性能问题,就需要考虑实现之

无序集合对应方法如下:

-countOf//必须实现,对应于NSArray的基本方法count:

-objectInAtIndex:

-AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get:range://不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:

-insertObject:inAtIndex:

-insert:atIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFromAtIndex:

-removeAtIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectInAtIndex:withObject:

-replaceAtIndexes:with://这两个都是可选的,如果在此类操作上有性能问题,就需要考虑实现之

使用 KVC 进行集合类的运算

KVC 提供的 valueForKeyPath: 方法非常强大,可以在keyPath中嵌套集合运算符对集合中的对象进行相关的运算,例如求一个数组中所有Person对象的age总和。集合对象主要指NSArray和NSSet,不包括NSDictionary。

集合运算符的格式

[email protected]

keyPathToCollection:Left key path,要操作的集合对象,若调用 valueForKeyPath: 方法的对象本来就是集合对象,则可以省略;

collentionOperator:Collection operator,集合操作符,一般以@开头;

keyPathToproperty:Right key path,要运算的属性。

//Address.h

@interface Address : NSObject

@property (copy, nonatomic) NSString *city;

@property (copy, nonatomic) NSString *street;

@property (strong, nonatomic) NSNumber *cityNumber;

@property (assign, nonatomic) NSInteger streetNumber;

@end

- (void)viewDidLoad {

    [super viewDidLoad];

    NSMutableArray *array = [NSMutableArray array];

    for ( int i = 0 ; i < 5 ; i++ ) {

        Address *address = [[Address alloc] init];

        NSString *cityStr = [NSString stringWithFormat:@"city-%d",i];

        //批量赋值

        NSDictionary *dic = @{

                              @"city":cityStr,

                              @"street":@"street",

                              @"cityNumber":@(i),

                              @"streetNumber":@101

                              };

        [address setValuesForKeysWithDictionary:dic];

        [array addObject:address];

    }

    //返回数组中保存的对象的属性,返回值为数组

    NSArray *cityArray = [array valueForKeyPath:@"city"];

    NSLog(@"%@",cityArray);

}

集合运算符的分类

集合运算符主要分为三类:

集合操作符:处理集合包含的对象,并根据操作符的不同返回不同的类型,返回值以NSNumber为主。

数组操作符:根据操作符的条件,将符合条件的对象包含在数组中返回。

嵌套操作符:处理集合对象中嵌套其他集合对象的情况,返回结果也是一个集合对象。

1. 集合操作符

集合操作符处理 NSArray和 NSSet 及其子类这样的集合对象,并根据不同的操作符返回不同类型的对象,返回值一般都是 NSNumber。

@avg 用来计算集合中 right keyPath 指定的属性的平均值。

//@avg:平均值

NSNumber *avg = [array valueForKeyPath:@"@avg.streetNumber"];

NSLog(@"%@",avg);

@count 用来计算集合中对象的数量。备注:@count 操作符比较特殊,它不需要写 right keyPath,即使写了也会被忽略。

//@count:集合里对象的数量

NSNumber *count = [array valueForKeyPath:@"@count"];

NSLog(@"%@",count);

@sum 用来计算集合中 right keyPath 指定的属性的总和。

//@sum:总和

NSNumber *sum = [array valueForKeyPath:@"@sum.streetNumber"];

NSLog(@"%@",sum);

@max 用来查找集合中 right keyPath 指定的属性的最大值。

//@max:最大值

NSNumber *max = [array valueForKeyPath:@"@max.cityNumber"];

NSLog(@"%@",max);

@min 用来查找集合中 right keyPath 指定的属性的最小值。

//@min:最小值

NSNumber *min = [array valueForKeyPath:@"@min.cityNumber"];

NSLog(@"%@",min);

备注:@max 和 @min 在进行判断时,都是通过调用 compare: 方法进行判断,所以可以通过重写该方法对判断过程进行控制。

2. 数组操作符(因为返回的是数组,所以叫数组操作符)

@unionOfObjects将集合中的所有对象的同一个属性放在数组中返回。这个和直接写属性好像没区别。

NSArray *city = [array valueForKeyPath:@"@unionOfObjects.city"];

NSLog(@"%@",city);

NSArray *cityArray = [array valueForKeyPath:@"city"];

NSLog(@"%@",cityArray);

@distinctUnionOfObjects将集合中对象的属性进行去重并返回。

NSArray *streetNumberArr = [array valueForKeyPath:@"@distinctUnionOfObjects.streetNumber"];

NSLog(@"%@",streetNumberArr);

3. 嵌套操作符

嵌套操作符是对集合里的集合进行操作,比如数组里面的数组。

NSMutableArray *array1 = [NSMutableArray array];

    for ( int i = 0 ; i < 5 ; i++ ) {

        Address *address = [[Address alloc] init];

        NSString *cityStr = [NSString stringWithFormat:@"city-%d",i];

        //批量赋值

        NSDictionary *dic = @{

                              @"city":cityStr,

                              @"street":@"street",

                              @"cityNumber":@(i),

                              @"streetNumber":@101

                              };

        [address setValuesForKeysWithDictionary:dic];

        [array1 addObject:address];

    }

    NSMutableArray *array2 = [NSMutableArray array];

    for ( int i = 3 ; i < 9 ; i++ ) {

        Address *address = [[Address alloc] init];

        NSString *cityStr = [NSString stringWithFormat:@"city-%d",i];

        //批量赋值

        NSDictionary *dic = @{

                              @"city":cityStr,

                              @"street":@"street",

                              @"cityNumber":@(i),

                              @"streetNumber":@101

                              };

        [address setValuesForKeysWithDictionary:dic];

        [array2 addObject:address];

    }

    NSArray *array = @[array1,array2];

@unionOfArrays 是用来操作集合内部的集合,将所有right keyPath对应的属性放在一个数组中返回。

NSArray *result = [array valueForKeyPath:@"@unionOfArrays.city"];

@distinctUnionOfArrays 是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个数组中,并进行排重。

    NSArray *result = [array valueForKeyPath:@"@distinctUnionOfArrays.city"];

@distinctUnionOfSets 是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个set中,并进行排重。

    NSSet *result = [array valueForKeyPath:@"@distinctUnionOfSets .city"];

4. 小技巧

如果你想对集合里装的对象直接进行操作,可以将 right keyPath 直接写为 self。比如集合里装的是NSNumber对象,如下:

    NSArray *numbers = @[@1,@1,@3,@5];

    NSNumber *sum = [numbers valueForKeyPath:@"@distinctUnionOfObjects.self"];

    NSNumber *avg = [numbers valueForKeyPath:@"@avg.self"];

KVC对数值和结构体型属性的支持

KVC可以自动的将数值或结构体型的数据打包或解包成NSNumber或NSValue对象,以达到适配的目的。

举个例子,Person类有个NSInteger类型的age属性,如下:

//  Person.m

#import "Person.h"

@interface Person ()

@property (nonatomic,assign) NSInteger age;

@end

@implementation Person

@end

//  Person.m

#import "Person.h"

@interface Person ()

@property (nonatomic,assign) NSInteger age;

@end

@implementation Person

@end

1. 修改值

我们通过KVC技术使用如下方式设置age属性的值:

[[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];

我们赋给age的是一个NSNumber对象,KVC会自动的将NSNumber对象转换成NSInteger对象,然后再调用相应的访问器方法设置age的值。

2. 获取值

同样,以如下方式获取age属性值:

NSNumber *age = [person valueForKey:@"age"];

这时,会以NSNumber的形式返回age的值。

3. 注意点

我们不能直接将基本数据类型通过KVC赋值,需要把数据转成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)insets;

NSValue主要用于处理结构体型的数据,任何结构体都是可以转化成NSValue对象的,包括其它自定义的结构体。

属性验证

在调用 KVC 前可以先进行验证 key(keyPath) 和 value 的正确性,验证通过下面两个方法进行。验证方法默认实现返回 YES,可以通过重写对应的方法修改验证逻辑。

注意:验证方法需要我们手动调用,并不会在进行 KVC 的过程中自动调用。

//key方法

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

//keyPath方法

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue

forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;

主动去调用验证方法:

Person *person = [[Person alloc] init];

NSError *error;

NSString *name = @"xds";

if (![person validateValue:&name forKey:@"name" error:&error]) {

    NSLog(@"%@", error);

}

单独验证

KVC还支持对单独属性做验证,可以通过定义validate:error:格式的方法,并在方法内部实现验证代码。在编写KVC验证代码的时候,应该先查找属性有没有自定义validate方法,然后再查找validateValue:方法,如果有则调用自己实现的方法,如果两个方法都没有实现则默认返回YES。

- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{

    if ((*ioValue ** nil) || ([(NSString *)*ioValue length] < 2)) {

        if (outError != NULL) {

            *outError = [NSError errorWithDomain:PersonErrorDomain

                                            code:PersonInvalidNameCode

                                        userInfo:@{ NSLocalizedDescriptionKey

                                                    : @"Name too short" }];

        }

        return NO;

    }

    return YES;

}

注意:这里 validateName 是 validate + 属性名称 组合而来的。

KVC 的搜索规则

在学习KVC的搜索规则前,要先弄明白一个属性的作用,这个属性在搜索过程中起到很重要的作用。这个属性表示是否允许读取实例变量的值,如果为YES则在KVC查找的过程中,从内存中读取属性实例变量的值。

@property (class, readonly) BOOL accessInstanceVariablesDirectly;

在KVC的实现中,依赖setter和getter的方法实现,所以方法命名应该符合苹果要求的规范,否则会导致KVC失败。

基础Getter搜索模式(valueForKey:/valueForKeyPath:)

1.首先按get、is的顺序查找getter方法,找到直接调用。

若方法的返回结果类型为是一个对象指针,则直接返回结果;

若类型为能够转化为NSNumber的基本数据类型,转换为NSNumber后返回;

否则,转换为NSValue返回。

2.上面的getter没有找到,查找countOf、objectInAtIndex:、AtIndexes格式的方法。

如果countOf和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的集合代理(collection proxy object)。发送给这个代理集合(collection proxy object)的NSArray消息方法,就会以countOf、objectInAtIndex:、AtIndexes这几个方法组合的形式调用。如果receiver的类实现了get:range:方法,给方法也会用于性能优化。

3.还没查到,那么查找countOf、enumeratorOf、memberOf:格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的集合代理(collection proxy object)。发送给这个代理集合(collection proxy object)的NSSet消息方法,就会以countOf、enumeratorOf、memberOf:组合的形式调用。

4.还是没查到,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_,_is,is(注意大小写)的顺序直接搜索实例变量。如果搜索到了,则返回receiver相应实例变量的值。返回结果的处理见步骤1。

5.再没查到,调用valueForUndefinedKey:方法,报出异常。

总结一下:

先找相应的 getter 方法(get, , is, 或者 _),找到了则返回(对象类型直接返回,其它类型进行转换);

没有找到,则寻找 NSArray 相应的方法;

没有找到,则寻找 NSSet 相应的方法;

如果还没有,且 accessInstanceVariablesDirectly 类属性返回的是 YES,则去搜索实例变量(_、_is、is)。如果发现了,则返回;

还没有,则转到 valueForUndefinedKey: 方法并抛出异常。

基础Setter搜索模式

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

查找set:或_set命名的setter,按照这个顺序,如果找到的话,调用这个方法并将值传进去(根据需要进行对象转换)。

如果没有发现一个简单的setter,但是 accessInstanceVariablesDirectly 类属性返回YES,则查找一个命名规则为_、_is、is的实例变量。根据这个顺序,如果发现则将value赋值给实例变量。

如果没有发现setter或实例变量,则调用setValue:forUndefinedKey:方法,并默认提出一个异常,但是一个NSObject的子类可以提出合适的行为。

总结:

先找 setter 方法(set:或_set);

没找到,如果则 accessInstanceVariablesDirectly 类属性返回的是 YES,则去查找实例变量(_、_is、is),若找到,则赋值;

没有找到 setter 方法和实例变量,则转到 setValue:forUndefinedKey: 方法,并抛出异常。

KVC性能

根据上面KVC的实现原理,我们可以看出KVC的性能并不如直接访问属性快,虽然这个性能消耗是微乎其微的。所以在使用KVC的时候,建议最好不要手动设置属性的setter、getter,这样会导致搜索步骤变长。

而且尽量不要用KVC进行集合操作,例如NSArray、NSSet之类的,集合操作的性能消耗更大,而且还会创建不必要的对象。

私有访问

根据上面的实现原理我们知道,KVC本质上是操作方法列表以及在内存中查找实例变量。我们可以利用这个特性访问类的私有变量,例如下面在.m中定义的私有成员变量和属性,都可以通过KVC的方式访问。

这个操作对readonly的属性,@protected的成员变量,都可以正常访问。如果不想让外界访问类的成员变量,则可以将accessInstanceVariablesDirectlygetter方法返回为NO。

Person.m文件

@interface Person(){

    NSString *str;

}

@property (strong, nonatomic) NSString *testStr;

@end

testStr是私有属性,str是私有成员变量。使用KVC都可以访问到这两个。如果不想让外界访问到str成员变量,可以这样做:

@implementation Person

+ (BOOL)accessInstanceVariablesDirectly{

    return NO;

}

@end

这样就访问不到私有的成员变量了。但是还是能访问到私有的属性,因为属性有getter和setter方法,KVC会先搜索访问方法,再去看accessInstanceVariablesDirectlygetter能不能访问实例变量。

KVC 的实践

KVC在实践中也有很多用处,例如UITabbar或UIPageControl这样的控件,系统已经为我们封装好了,但是对于一些样式的改变并没有提供足够的API,这种情况就需要我们用KVC进行操作了。可以自定义一个UITabbar对象,然后在内部创建自己想要的视图,并通过layoutSubviews方法在内部进行重新布局。然后通过KVC的方式,将UITabbarController的tabbar属性替换为自定义的类即可。

安全性检查

KVC存在一个问题在于,因为传入的key或keyPath是一个字符串,这样很容易写错或者属性自身修改后字符串忘记修改,这样会导致Crash。

可以利用iOS的反射机制来规避这个问题,通过@selector()获取到方法的SEL,然后通过NSStringFromSelector()将SEL反射为字符串。这样在@selector()中传入方法名的过程中,编译器会有合法性检查,如果方法不存在或未实现会报黄色警告。

你可能感兴趣的:((IOS)KVC)