iOS KVC 底层原理3

108 阅读5分钟

「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」。

三、KVC 使用场景

估计都会使用就不举例了

  • 利用KVC动态的取值和设值

    常用的可以通过setValue:forKey: 和 valueForKey:

    也可以通过路由的方式setValue:forKeyPath: 和 valueForKeyPath:

  • Model和字典转换

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

    根据上面的实现原理我们知道,KVC本质上是操作方法列表以及在内存中查找实例变量。我们可以利用这个特性访问类的私有变量,例如下面在.m中定义的私有成员变量和属性,都可以通过KVC的方式访问。【对于KVC而言,一个对象没有自己的隐私,所以可以通过KVC修改和访问任何私有属性

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

  • 修改一些控件的内部属性

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

  • 用KVC实现高阶消息传递

    在对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合 来返回 另一个集合

//KVC实现高阶消息传递
- (void)transmitMsg{
    NSArray *arrStr = @[@"english", @"franch", @"chinese"];
    NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];
    
    for (NSString *str in arrCapStr) {
        NSLog(@"%@", str);
    }
    
    NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"];
    for (NSNumber *length in arrCapStrLength) {
        NSLog(@"%ld", (long)length.integerValue);
    }
}

//********打印结果********
11:33:43.377672+0800 CJLCustom[60035:6380757] English
11:33:43.377773+0800 CJLCustom[60035:6380757] Franch
11:33:43.377860+0800 CJLCustom[60035:6380757] Chinese
11:33:43.378233+0800 CJLCustom[60035:6380757] 7
11:33:43.378327+0800 CJLCustom[60035:6380757] 6
11:33:43.378417+0800 CJLCustom[60035:6380757] 7

四、拓展

KVC属性验证

KVC提供了属性值,用来验证key对应的Value是否可用的方法

  • 在调用KVC时可以先进行验证,验证通过下面两个方法进行,支持keykeyPath两种方式。验证方法默认实现返回YES,可以通过重写对应的方法修改验证逻辑。

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

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath 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);

//打印结果 
KVCDemo[867:58871] 键值不匹配
KVCDemo[867:58871] country:China

KVC处理非对象

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

  • 可以调用initWithBool:方法对基础数据类型进行包装
@property (nonatomic, assign, readonly) BOOL boolValue;
- (NSNumber *)initWithBool:(BOOL)value 

KVC异常处理

    1. key或者keyPath发生错误

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

//获取了不存在的key 只需要实现如下方法
- (nullable id)valueForUndefinedKey:(NSString *)key;
//设置了不存在的key 造成崩溃 只需要实现如下方法
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
    1. 传参为nil

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

- (void)setNilValueForKey:(NSString *)key{
   NSLog(@"-----设置了nil");
}