【iOS】—— KVC的简单介绍

一.什么是KVC:

KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的。

二.KVC在iOS中的定义:

无论是Swift还是Objective-CKVC的定义都是对NSObject的扩展来实现的(Objective-C中有个显式的NSKeyValueCoding类别名,而Swift没有,也不需要)。所以对于所有继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用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转到字典。

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

三.KVC是怎么寻找Key的:

1.设值:

当调用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机制:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Dog : NSObject {
    NSString *toSetName;
    NSString *isName;
    //NSString *name;
    NSString *_name;
    NSString *_isName;
}

@end

NS_ASSUME_NONNULL_END

#import "Dog.h"

@implementation Dog

// - (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

#import <Foundation/Foundation.h>
#import "Dog.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog = [Dog new];
        [dog setValue:@"newName" forKey:@"name"];
        NSString *name = [dog valueForKey:@"toSetName"];
        NSLog(@"%@",name);
    }
    return 0;
}

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

这说明了重写+(BOOL)accessInstanceVariablesDirectly方法让其返回NO后,KVC找不到setName:方法后,不再去找name系列成员变量,而是直接调用setValue:forUndefinedKey:方法,所以开发者如果不想这样,就可以这么做:
下面那两个settergetter的注释取消掉,再把

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

XCode就可以正确地打印出正确的值了:
432

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

2.取值:

当调用valueForKey:@"name"的代码时,KVCkey的搜索方式不同于setValue:属性值 forKey:@"name",其搜索方式如下:

  • 首先按get<Key>,<key>,is<Key>的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
  • 如果上面的getter没有找到,KVC则会查找countOf<Key>,objectIn<Key>AtIndex<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex<Key>AtIndexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
  • 如果上面的方法没有找到,那么会同时查找countOf<Key>enumeratorOf<Key>,memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>enumeratorOf<Key>,memberOf<Key>组合的形式调用。

可能上面的两条查找方案可能不好理解,简单来说就是如果你在自己的类自定义了KVC的实现,并且实现了上面的方法,那么恭喜你,你可以将返回的对象当数组(NSArray)用了,详情见下面的示例代码。

  • 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:
  • 还没有找到的话,调用valueForUndefinedKey:
    下面再上代码测试:
@interface TwoTimesArray : NSObject
@property (nonatomic,readwrite,assign) NSUInteger count;
@property (nonatomic,copy) NSString *arrName;
- (void)incrementCount;
- (NSUInteger)countOfNumbers;
- (id)objectInNumbersAtIndex:(NSUInteger)index;
@end

@implementation TwoTimesArray
- (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


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(@"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);
        
    }
    return 0;
}

42342

从打印结果可以看出,它直接调用-(id)objectInNumbersAtIndex:(NSUInteger)index;方法,充分说明了KVC在调用ValueforKey:@"name"时搜索key的机制。

四.在KVC中使用keyPath:

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

下面是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);
    }
    return 0;
}

打印结果:
3242

上面的代码简单在展示了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

打印结果:
432

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值