iOS之武功秘籍⑪: KVC原理及自定义

iOS之武功秘籍 文章汇总

写在前面

平常开发中经常用到KVC赋值取值、字典转模型,但KVC的底层原理又是怎样的呢?本篇就来带你走进KVC..

本节可能用到的秘籍Demo

一、KVC初探

①.KVC定义及API

KVC(Key-Value Coding)是利用NSKeyValueCoding 非正式协议实现的一种机制,对象采用这种机制来提供对其属性的间接访问.

写下KVC代码并点击跟进setValue会发现NSKeyValueCoding是在Foundation框架下的

  • KVC是通过对NSObject的扩展来实现的 —— 只要继承了NSObject的类都可以使用KVC
  • NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet等也遵守KVC协议
  • 除少数类型(结构体)以外都可以使用KVC
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TCJPerson *person = [TCJPerson alloc];
        [person setValue:@"TCJ" forKey:@"name"];
        [person setValue:@"CJ" forKey:@"nickName"];
    }
    return 0;
}

KVC常用方法,这些也是我们在日常开发中经常用到的

// 通过 key 设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 通过 key 取值
- (nullable id)valueForKey:(NSString *)key;
// 通过 keyPath 设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 通过 keyPath 取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;

NSKeyValueCoding类别的其它方法

// 默认为YES。 如果返回为YES,如果没有找到 set 方法的话, 会按照_key, _isKey, key, isKey的顺序搜索成员变量, 返回NO则不会搜索
+ (BOOL)accessInstanceVariablesDirectly;
// 键值验证, 可以通过该方法检验键值的正确性, 然后做出相应的处理
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 如果key不存在, 并且没有搜索到和key有关的字段, 会调用此方法, 默认抛出异常。两个方法分别对应 get 和 set 的情况
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// setValue方法传 nil 时调用的方法
// 注意文档说明: 当且仅当 NSNumber 和 NSValue 类型时才会调用此方法 
- (void)setNilValueForKey:(NSString *)key;
// 一组 key对应的value, 将其转成字典返回, 可用于将 Model 转成字典
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

②.拓展——自动生成的setter和getter方法

试想一下编译器要为成千上万个属性分别生成settergetter方法那不得歇菜了嘛

于是乎苹果开发者们就运用通用原则给所有属性都提供了同一个入口——objc-accessors.mmsetter方法根据修饰符不同调用不同方法,最后统一调用reallySetProperty方法

来到reallySetProperty再根据内存偏移量取出属性,根据修饰符完成不同的操作

  • 在第一个属性name赋值时,此时的内存偏移量为8,刚好偏移isa所占内存(8字节)来到name
  • 在第二个属性nickName赋值时,此时的内存偏移量为16,刚好偏移isa、name所占内存(8+8)来到nickName

至于是哪里调用的objc_setProperty_nonatomic_copy

并不是在objc源码中,而在llvm源码中发现了它,根据它一层层找上去就能找到源头

二、KVC使用

相信大部分阅读本文的小伙伴们都对KVC的使用都比较了解了,但笔者建议还是看一下查漏补缺

typedef struct {
    float x, y, z;
} ThreeFloats;

@interface TCJPerson : NSObject{
    @public
    NSString *myName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, strong) NSMutableArray *mArray;
@property (nonatomic, assign) NSInteger  age;
@property (nonatomic) ThreeFloats threeFloats;
@property (nonatomic, strong) TCJStudent *student;
@end

@interface TCJStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) NSInteger length;
@property (nonatomic, strong) NSMutableArray *penArr;
@end

①.基本类型

注意一下NSInteger这类的属性赋值时要转成NSNumberNSString

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

// 1:Key-Value Coding (KVC) : 基本类型
[person setValue:@"TCJ" forKey:@"name"];
[person setValue:@19 forKey:@"age"];
[person setValue:@"CJ" forKey:@"myName"];
NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]);

打印结果:

 ① - KVC简介[10226:310799] TCJ - 19 - CJ

②.集合类型

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

// 2:KVC - 集合类型 -
person.array = @[@"1",@"2",@"3"];
// 由于不是可变数组 - 无法做到
// person.array[0] = @"100";
// 搞一个新的数组 - KVC 赋值就OK
NSArray *array = [person valueForKey:@"array"];
// 用 array 的值创建一个新的数组
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);
 
// 取出数组以可变数组形式保存,再修改 KVC 的方式
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"100";
NSLog(@"%@",[person valueForKey:@"array"]);

打印结果:

 ① - KVC简介[10226:310799] (
    100,
    2,
    3
)
 ① - KVC简介[10226:310799] (
    100,
    2,
    3
)

③.访问非对象类型——结构体

  • 对于非对象类型的赋值总是把它先转成NSValue类型再进行存储
  • 取值时转成对应类型后再使用
TCJPerson *person = [[TCJPerson alloc] init];

// 3:KVC - 访问非对象属性 - 面试可能问到
// 3.1 赋值
ThreeFloats floats = {1., 2., 3.};
NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslut = [person valueForKey:@"threeFloats"];
NSLog(@"非对象类型%@",reslut);
 
// 3.2 取值
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"非对象类型的值%f - %f - %f",th.x,th.y,th.z);

打印结果:

 ① - KVC简介[10226:310799] 非对象类型{length = 12, bytes = 0x0000803f0000004000004040}
 ① - KVC简介[10226:310799] 非对象类型的值1.000000 - 2.000000 - 3.000000

④.集合操作符

  • 聚合操作符
    • @avg: 返回操作对象指定属性的平均值
    • @count: 返回操作对象指定属性的个数
    • @max: 返回操作对象指定属性的最大值
    • @min: 返回操作对象指定属性的最小值
    • @sum: 返回操作对象指定属性值之和
  • 数组操作符
    • @distinctUnionOfObjects: 返回操作对象指定属性的集合--去重
    • @unionOfObjects: 返回操作对象指定属性的集合
  • 嵌套操作符
    • @distinctUnionOfArrays: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSArray
    • @unionOfArrays: 返回操作对象(集合)指定属性的集合
    • @distinctUnionOfSets: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSSet

集合操作符用得少之又少。下面举个

TCJPerson *person = [[TCJPerson alloc] init];
NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        TCJStudent *p = [TCJStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [p setValuesForKeysWithDictionary:dict];
        [personArray addObject:p];
    }
    NSLog(@"%@", [personArray valueForKey:@"length"]);
    
    /// 平均身高
    float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
    NSLog(@"%f", avg);
    
    int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
    NSLog(@"%d", count);
    
    int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
    NSLog(@"%d", sum);
    
    int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
    NSLog(@"%d", max);
    
    int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
    NSLog(@"%d", min);

打印结果:

 ① - KVC简介[10544:326204] (
    185,
    177,
    185,
    177,
    181,
    175
)
 ① - KVC简介[10544:326204] 180.000000
 ① - KVC简介[10544:326204] 6
 ① - KVC简介[10544:326204] 1080
 ① - KVC简介[10544:326204] 185
 ① - KVC简介[10544:326204] 175

⑤.层层嵌套

通过forKeyPath对实例变量(student)进行取值赋值

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

TCJStudent *student = [[TCJStudent alloc] init];
    student.subject    = @"iOS";
    person.student     = student;
    [person setValue:@"葵花宝典" forKeyPath:@"student.subject"];
    NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

打印结果:

 ① - KVC简介[10544:326204] 葵花宝典

三、KVC底层原理

由于NSKeyValueCoding的实现在Foundation框架中,但它又不开源,我们只能通过KVC官方文档来了解它

①.设值过程

官方文档上对Setter方法的过程进行了这样一段讲解


  • ①.按set:_set:顺序查找对象中是否有对应的方法
    • 找到了直接调用设值
    • 没有找到跳转第2步
  • ②.判断accessInstanceVariablesDirectly结果
    • 为YES时按照__isis的顺序查找成员变量,找到了就赋值;找不到就跳转第3步
    • 为NO时跳转第3步
  • ③.调用setValue:forUndefinedKey:。默认情况下会引发一个异常,但是继承于NSObject的子类可以重写该方法就可以避免崩溃并做出相应措施

②.取值过程

同样的官方文档上也给出了Getter方法的过程


  • ①.按照getis_顺序查找对象中是否有对应的方法

    • 如果有则调用getter,执行第5步
    • 如果没有找到,跳转到第2步
  • ②.查找是否有countOfobjectInAtIndex: 方法(对应于NSArray类定义的原始方法)以及AtIndexes: 方法(对应于NSArray方法objectsAtIndexes:)

    • 如果找到其中的第一个(countOf),再找到其他两个中的至少一个,则创建一个响应所有 NSArray方法的代理集合对象,并返回该对象(即要么是countOf + objectInAtIndex:,要么是countOf + AtIndexes:,要么是countOf + objectInAtIndex: + AtIndexes:)
    • 如果没有找到,跳转到第3步
  • ③.查找名为countOfenumeratorOfmemberOf这三个方法(对应于NSSet类定义的原始方法)

    • 如果找到这三个方法,则创建一个响应所有NSSet方法的代理集合对象,并返回该对象
    • 如果没有找到,跳转到第4步
  • ④.判断accessInstanceVariablesDirectly

    • 为YES时按照__isis的顺序查找成员变量,找到了就取值
    • 为NO时跳转第6步
  • ⑤.判断取出的属性值

    • 属性值是对象,直接返回
    • 属性值不是对象,但是可以转化为NSNumber类型,则将属性值转化为NSNumber 类型返回
    • 属性值不是对象,也不能转化为NSNumber类型,则将属性值转化为NSValue类型返回
  • ⑥.调用valueForUndefinedKey:.默认情况下会引发一个异常,但是继承于NSObject的子类可以重写该方法就可以避免崩溃并做出相应措施

四、自定义KVC

根据KVC的设值过程、取值过程,我们可以自定义KVCsetter方法和getter方法,但是这一切都是根据官方文档做出的规则,自定义KVC只能在一定程度上取代系统KVC,大致流程几乎一致:实现了 setValue:forUndefinedKey:valueForUndefinedKey: 的调用,且 accessInstanceVariablesDirectly 无论为truefalse,都能保持两次调用

新建一个NSObject+TCJKVC的分类,.h开放两个方法,.m引入

  • - (void)cj_setValue:(nullable id)value forKey:(NSString *)key;
  • - (nullable id)cj_valueForKey:(NSString *)key;

①.自定义setter方法

1.非空判断

if (key == nil || key.length == 0) return;

2.找到相关方法set_setsetIs,若存在就直接调用

NSString *Key = key.capitalizedString;
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];

if ([self cj_performSelectorWithMethodName:setKey value:value]) {
    NSLog(@"*********%@**********",setKey);
    return;
} else if ([self cj_performSelectorWithMethodName:_setKey value:value]) {
    NSLog(@"*********%@**********",_setKey);
    return;
} else if ([self cj_performSelectorWithMethodName:setIsKey value:value]) {
    NSLog(@"*********%@**********",setIsKey);
    return;
}

3.判断是否能够直接赋值实例变量,不能的情况下就调用setValue:forUndefinedKey:或抛出异常

NSString *undefinedMethodName = @"setValue:forUndefinedKey:";
IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));

if (![self.class accessInstanceVariablesDirectly]) {
    if (undefinedIMP) {
        [self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];
    } else {
        @throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
    }
    return;
}

4.找相关实例变量进行赋值

NSMutableArray *mArray = [self getIvarListName];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
   Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
   object_setIvar(self , ivar, value);
   return;
} else if ([mArray containsObject:_isKey]) {
   Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
   object_setIvar(self , ivar, value);
   return;
} else if ([mArray containsObject:key]) {
   Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
   object_setIvar(self , ivar, value);
   return;
} else if ([mArray containsObject:isKey]) {
   Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
   object_setIvar(self , ivar, value);
   return;
}

5.调用setValue:forUndefinedKey:或抛出异常

if (undefinedIMP) {
    [self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];
} else {
    @throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
}

在这里笔者存在一个疑问:没有实现setValue:forUndefinedKey:时,当前类可以响应respondsToSelector这个方法,但是直接performSelector会崩溃,所以改用了判断IMP是否为空

②.自定义getter方法

1.非空判断

if (key == nil  || key.length == 0) return nil;

2.找相关方法get,找到就返回(这里使用-Warc-performSelector-leaks消除警告)

NSString *Key = key.capitalizedString;
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
    return [self performSelector:NSSelectorFromString(getKey)];
} else if ([self respondsToSelector:NSSelectorFromString(key)]) {
    return [self performSelector:NSSelectorFromString(key)];
}
#pragma clang diagnostic pop

3.对NSArray进行操作:查找countOfobjectInAtIndex方法

NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(countOfKey)]) {
    if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
        int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
        NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
        for (int i = 0; i

4.判断是否能够直接赋值实例变量,不能的情况下就调用valueForUndefinedKey:或抛出异常

NSString *undefinedMethodName = @"valueForUndefinedKey:";
IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));

if (![self.class accessInstanceVariablesDirectly]) {
    
    if (undefinedIMP) {
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
#pragma clang diagnostic pop
    } else {
        @throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
    }
}

5.找相关实例变量,找到了就返回

NSMutableArray *mArray = [self getIvarListName];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
    Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
    return object_getIvar(self, ivar);;
} else if ([mArray containsObject:_isKey]) {
    Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
    return object_getIvar(self, ivar);;
} else if ([mArray containsObject:key]) {
    Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
    return object_getIvar(self, ivar);;
} else if ([mArray containsObject:isKey]) {
    Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
    return object_getIvar(self, ivar);;
}

6.调用valueForUndefinedKey:或抛出异常

if (undefinedIMP) {
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
#pragma clang diagnostic pop
} else {
    @throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
}

③.封装的方法

这里简单封装了几个用到的方法

  • cj_performSelectorWithMethodName:value:key:安全调用方法及传两个参数

    - (BOOL)cj_performSelectorWithMethodName:(NSString *)methodName value:(id)value key:(id)key {
     
        if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
            
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [self performSelector:NSSelectorFromString(methodName) withObject:value withObject:key];
    #pragma clang diagnostic pop
            return YES;
        }
        return NO;
    }
    
  • cj_performSelectorWithMethodName:key:安全调用方法及传参

    - (BOOL)cj_performSelectorWithMethodName:(NSString *)methodName key:(id)key {
     
        if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
            
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [self performSelector:NSSelectorFromString(methodName) withObject:key];
    #pragma clang diagnostic pop
            return YES;
        }
        return NO;
    }
    
    
  • getIvarListName取成员变量

    - (NSMutableArray *)getIvarListName {
        
        NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i

KVC中还有一些异常小技巧,在前文中已经提及过,这里再总结一下

五、KVC异常小技巧

①.技巧一 —— 自动转换类型

  • int类型赋值会自动转成__NSCFNumber

    [person setValue:@18 forKey:@"age"];
    [person setValue:@"20" forKey:@"age"];
    NSLog(@"%@-%@", [person valueForKey:@"age"], [[person valueForKey:@"age"] class]);
    
  • 用结构体类型赋值会自动转成NSConcreteValue

    ThreeFloats floats = {1.0, 2.0, 3.0};
    NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSLog(@"%@-%@", [person valueForKey:@"threeFloats"], [[person valueForKey:@"threeFloats"] class]);
    
    

②.技巧二 —— 设置空值

有时候在设值时设置空值,可以通过重写setNilValueForKey来监听,但是以下代码只有打印一次

// Int类型设置nil
[person setValue:nil forKey:@"age"];
// NSString类型设置nil
[person setValue:nil forKey:@"subject"];

@implementation TCJPerson

- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"设置 %@ 是空值", key);
}

@end

这是因为setNilValueForKey只对NSNumber类型有效

③.技巧三 —— 未定义的key

对于未定义的key我们可以通过重写setValue:forUndefinedKey:valueForUndefinedKey:来监听

@implementation TCJPerson

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"未定义的key——%@",key);
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"未定义的key——%@",key);
    return @"未定义的key";
}

@end

④.技巧四——键值验证

一个比较鸡肋的功能——键值验证,可以自行展开做重定向

NSError *error;
NSString *name = @"TCJ";
if (![person validateValue:&name forKey:@"names" error:&error]) {
    NSLog(@"%@",error);
}else{
    NSLog(@"%@", [person valueForKey:@"name"]);
}

@implementation TCJPerson

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError {
    if([inKey isEqualToString:@"name"]){
        [self setValue:[NSString stringWithFormat:@"里面修改一下: %@",*ioValue] forKey:inKey];
        return YES;
    }
    *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:10088 userInfo:nil];
    return NO;
}

@end

写在后面

和谐学习,不急不躁.我还是我,颜色不一样的烟火.

你可能感兴趣的:(iOS之武功秘籍⑪: KVC原理及自定义)