iOS探索 KVC原理及自定义

4,765 阅读14分钟

欢迎阅读iOS探索系列(按序阅读食用效果更加)

写在前面

平常开发中经常用到KVC赋值取值、字典转模型,但KVC的底层原理又是怎样的呢?

Demo

一、KVC初探

1.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 {
        FXPerson *person = [FXPerson new];
        [person setValue:@"Felix" forKey:@"name"];
        [person setValue:@"Felix" 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> 方法的话, 会按照_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<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

2.拓展——自动生成的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 FXPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSArray  *family;
@property (nonatomic) ThreeFloats threeFloats;
@property (nonatomic, strong) FXFriend *friends;
@end

@interface FXFriend : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

1.基本类型

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

FXPerson *person = [FXPerson new];

[person setValue:@"Felix" forKey:@"name"];
[person setValue:@(18) forKey:@"age"];
NSLog(@"名字%@ 年龄%@", [person valueForKey:@"name"], [person valueForKey:@"age"]);

打印结果:

2020-03-08 14:06:20.913692+0800 FXDemo[2998:151140] 名字Felix 年龄18

2.集合类型

两种方法对数组进行赋值,更推荐使用第二种方法

FXPerson *person = [FXPerson new];
person.family = @[@"FXPerson", @"FXFather"];

// 直接用新的数组赋值
NSArray *temp = @[@"FXPerson", @"FXFather", @"FXMother"];
[person setValue:temp forKey:@"family"];
NSLog(@"第一次改变%@", [person valueForKey:@"family"]);

// 取出数组以可变数组形式保存,再修改
NSMutableArray *mTemp = [person mutableArrayValueForKeyPath:@"family"];
[mTemp addObject:@"FXChild"];
NSLog(@"第二次改变%@", [person valueForKey:@"family"]);

打印结果:

2020-03-08 14:06:20.913794+0800 FXDemo[2998:151140] 第一次改变(
    FXPerson,
    FXFather,
    FXMother
)
2020-03-08 14:06:20.913945+0800 FXDemo[2998:151140] 第二次改变(
    FXPerson,
    FXFather,
    FXMother,
    FXChild
)

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

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

// 赋值
ThreeFloats floats = {180.0, 180.0, 18.0};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSLog(@"非对象类型%@", [person valueForKey:@"threeFloats"]);

// 取值
ThreeFloats th;
NSValue *currentValue = [person valueForKey:@"threeFloats"];
[currentValue getValue:&th];
NSLog(@"非对象类型的值%f-%f-%f", th.x, th.y, th.z);

打印结果:

2020-03-08 14:06:20.914088+0800 FXDemo[2998:151140] 非对象类型{length = 12, bytes = 0x000034430000344300009041}
2020-03-08 14:06:20.914182+0800 FXDemo[2998:151140] 非对象类型的值180.000000-180.000000-18.000000
2020-03-08 14:06:20.914333+0800 FXDemo[2998:151140] (
    18,
    19,
    20,
    21,
    22,
    23
)

4.集合操作符

  • 聚合操作符

    • @avg: 返回操作对象指定属性的平均值
    • @count: 返回操作对象指定属性的个数
    • @max: 返回操作对象指定属性的最大值
    • @min: 返回操作对象指定属性的最小值
    • @sum: 返回操作对象指定属性值之和
  • 数组操作符

    • @distinctUnionOfObjects: 返回操作对象指定属性的集合--去重
    • @unionOfObjects: 返回操作对象指定属性的集合
  • 嵌套操作符

    • @distinctUnionOfArrays: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSArray
    • @unionOfArrays: 返回操作对象(集合)指定属性的集合
    • @distinctUnionOfSets: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSSet

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

FXPerson *person = [FXPerson new];

NSMutableArray *friendArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
    FXFriend *f = [FXFriend new];
    NSDictionary* dict = @{
                           @"name":@"Felix",
                           @"age":@(18+i),
                           };
    [f setValuesForKeysWithDictionary:dict];
    [friendArray addObject:f];
}
NSLog(@"%@", [friendArray valueForKey:@"age"]);

float avg = [[friendArray valueForKeyPath:@"@avg.age"] floatValue];
NSLog(@"平均年龄%f", avg);

int count = [[friendArray valueForKeyPath:@"@count.age"] intValue];
NSLog(@"调查人口%d", count);

int sum = [[friendArray valueForKeyPath:@"@sum.age"] intValue];
NSLog(@"年龄总和%d", sum);

int max = [[friendArray valueForKeyPath:@"@max.age"] intValue];
NSLog(@"最大年龄%d", max);

int min = [[friendArray valueForKeyPath:@"@min.age"] intValue];
NSLog(@"最小年龄%d", min);

打印结果:

2020-03-08 14:06:20.914503+0800 FXDemo[2998:151140] 平均年龄20.500000
2020-03-08 14:06:20.914577+0800 FXDemo[2998:151140] 调查人口6
2020-03-08 14:06:20.914652+0800 FXDemo[2998:151140] 年龄总和123
2020-03-08 14:06:20.914739+0800 FXDemo[2998:151140] 最大年龄23
2020-03-08 14:06:20.914832+0800 FXDemo[2998:151140] 最小年龄18

5.层层嵌套

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

FXPerson *person = [FXPerson new];

FXFriend *f = [[FXFriend alloc] init];
f.name = @"Felix的朋友";
f.age = 18;
person.friends = f;
[person setValue:@"Feng" forKeyPath:@"friends.name"];
NSLog(@"%@", [person valueForKeyPath:@"friends.name"]);

打印结果:

2020-03-08 14:06:20.914927+0800 FXDemo[2998:151140] Feng

三、KVC底层原理

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

1.设值过程

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

  1. set<Key>:_set<Key>:顺序查找对象中是否有对应的方法

    • 找到了直接调用设值
    • 没有找到跳转第2步
  2. 判断accessInstanceVariablesDirectly结果

    • 为YES时按照_<key>_is<Key><key>is<Key>的顺序查找成员变量,找到了就赋值;找不到就跳转第3步
    • 为NO时跳转第3步
  3. 调用setValue:forUndefinedKey:。默认情况下会引发一个异常,但是继承于NSObject的子类可以重写该方法就可以避免崩溃并做出相应措施

2.取值过程

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

  1. 按照get<Key><key>is<Key>_<key>顺序查找对象中是否有对应的方法

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

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

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

    • 为YES时按照_<key>_is<Key><key>is<Key>的顺序查找成员变量,找到了就取值
    • 为NO时跳转第6步
  5. 判断取出的属性值

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

四、自定义KVC

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

新建一个NSObject+FXKVC的分类,.h开放两个方法,.m引入<objc/runtime.h>

  • - (void)fx_setValue:(nullable id)value forKey:(NSString *)key;
  • - (nullable id)fx_valueForKey:(NSString *)key;

1.自定义setter方法

  1. 非空判断
if (key == nil || key.length == 0) return;
  1. 找到相关方法set<Key>_set<Key>setIs<Key>,若存在就直接调用
NSString *Key = key.capitalizedString;
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];

if ([self fx_performSelectorWithMethodName:setKey value:value]) {
    NSLog(@"*********%@**********",setKey);
    return;
} else if ([self fx_performSelectorWithMethodName:_setKey value:value]) {
    NSLog(@"*********%@**********",_setKey);
    return;
} else if ([self fx_performSelectorWithMethodName:setIsKey value:value]) {
    NSLog(@"*********%@**********",setIsKey);
    return;
}
  1. 判断是否能够直接赋值实例变量,不能的情况下就调用setValue:forUndefinedKey:或抛出异常
NSString *undefinedMethodName = @"setValue:forUndefinedKey:";
IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));

if (![self.class accessInstanceVariablesDirectly]) {
    if (undefinedIMP) {
        [self fx_performSelectorWithMethodName:undefinedMethodName value:value key:key];
    } else {
        @throw [NSException exceptionWithName:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
    }
    return;
}
  1. 找相关实例变量进行赋值
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;
}
  1. 调用setValue:forUndefinedKey:或抛出异常
if (undefinedIMP) {
    [self fx_performSelectorWithMethodName:undefinedMethodName value:value key:key];
} else {
    @throw [NSException exceptionWithName:@"FXUnknownKeyException" 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是否为空

2.自定义getter方法

  1. 非空判断
if (key == nil  || key.length == 0) return nil;
  1. 找相关方法get<Key><key>,找到就返回(这里使用-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
  1. NSArray进行操作:查找countOf<Key>objectIn<Key>AtIndex方法
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<num-1; i++) {
            num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
        }
        for (int j = 0; j<num; j++) {
            id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
            [mArray addObject:objc];
        }
        return mArray;
    }
}
#pragma clang diagnostic pop
  1. 判断是否能够直接赋值实例变量,不能的情况下就调用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:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
    }
}
  1. 找相关实例变量,找到了就返回
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);;
}
  1. 调用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:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
}

3.封装的方法

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

  • fx_performSelectorWithMethodName:value:key:安全调用方法及传两个参数
- (BOOL)fx_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;
}
  • fx_performSelectorWithMethodName:key:安全调用方法及传参
- (BOOL)fx_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<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}

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

五、KVC异常小技巧

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

  • 用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]);

2.技巧二——设置空值

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

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

@implementation FXPerson

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

@end

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

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

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

@implementation FXPerson

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

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

@end

4.技巧四——键值验证

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

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

@implementation FXPerson

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

写在后面

我们平时开发中经常用到KVC,理解KVC的使用和原理对我们会有很大帮助,具体可以下载Demo操作一下