19-KVC分析

前言

KVC(Key-Value Coding)键值编码:是一种由NSKeyValueCoding非正式协议启用的机制,对象采用该机制来提供对其属性的间接访问。
KVC是一个基本概念,它是许多其他 Cocoa 技术的基础,例如键值观察、Cocoa 绑定、核心数据和 AppleScript 能力。在某些情况下,键值编码还有助于简化您的代码。

官方文档:Key-Value Coding Programming Guide
通过官方文档你可以完全了解到什么是KVC并且如何去使用。

KVC基础

获取属性值

获取值的通用方法(valueForKey:) 还有两个相关方法 valueForKeyPath:dictionaryWithValuesForKeys:;分别返回对应的数据结构。

按照流程:比如这里需要搜索一个[person valueForKey:@"name"];

1: 先按照下面的格式 getName, name,isName, _name去搜索结果。如果找到
2: 如果没找到,那么执行 NSArray 的相关方法 countOfNameobjectInNameAtIndex:(对应于NSArray类定义的原始方法)和 nameAtIndexes:(对应于NSArray 方法 objectsAtIndexes:)。
3: 如果还是没有找到,寻找NSSet类的方法,方法名为 countOfNameenumeratorOfNamememberOfName:(对应于由所定义的原始的方法NSSet类)。
4: 如果发现收集的访问方法没有简单的存取方法或者组,如果接收器的类方法accessInstanceVariablesDirectly 返回 YES,搜索名为实例变量_<key>_is<Key><key>,或者is<Key>,按照这个顺序。如果找到,直接获取实例变量的值并进行步骤5,否则进行步骤6。
5: 如果检索到的属性值是一个对象指针,只需返回结果即可;如果该值是 支持的变量类型NSNumber,则将其存储在一个NSNumber实例中并返回该实例;如果结果是 NSNumber 不支持的变量类型,则转换为NSValue对象并返回
6: 如果所有其他方法都失败,请调用 valueForUndefinedKey:,默认情况下,这会引发异常,但 NSObject 的子类可能会提供特定于键的行为。

1、直接搜索格式如下代码所示

- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}
- (NSString *)name{
    return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}
 
    LGPerson *person = [[LGPerson alloc] init];
    //1、获取值
    id v = [person valueForKey:@"name"];
    NSLog(@"name = '%@'", v);

2、NSArray 集合类的方法

// 个数
- (NSUInteger)countOfPens{
    NSLog(@"%s",__func__);
    return [self.arr count];
}
// 获取值
- (id)objectInPensAtIndex:(NSUInteger)index {
    NSLog(@"%s",__func__);
    return [NSString stringWithFormat:@"pens %lu", index];
} 
// 或者这个
- (id)pensAtIndexes:(NSIndexSet *)indexs {
    NSLog(@"%s",__func__);
    return [self.arr objectsAtIndexes:indexs];
}

LGPerson *person = [[LGPerson alloc] init];
    // 3: KVC - 集合类型
    person.arr = @[@"pen0", @"pen1", @"pen2", @"pen3"];
    NSArray *array = [person valueForKey:@"pens"];
    NSLog(@"%@",[array objectAtIndex:1]);
    NSLog(@"%@",[array objectsAtIndexes:[NSIndexSet indexSetWithIndex:2]]);
    NSLog(@"%d",[array containsObject:@"pen1"]);

3、NSSet 集合类型的方法

// 个数
- (NSUInteger)countOfBooks{
    NSLog(@"%s",__func__);
    return [self.set count];
}
// 是否包含这个成员对象
- (id)memberOfBooks:(id)object {
    NSLog(@"%s",__func__);
    return [self.set containsObject:object] ? object : nil;
}
// 迭代器
- (id)enumeratorOfBooks {
    // objectEnumerator
    NSLog(@"来了 迭代编译");
    return [self.arr reverseObjectEnumerator];
}

person.set = [NSSet setWithArray:person.arr];
    NSSet *set = [person valueForKey:@"books"];
    [set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
        NSLog(@"set遍历 %@",obj);
    }];

4、+ accessInstanceVariablesDirectly 方法返回YES。

@interface LGPerson : NSObject{
    @public
    NSString *_name;        //1
    NSString *_isName;      //2
    NSString *name;         //3
    NSString *isName;       //4 
}

LGPerson *person = [[LGPerson alloc] init];
    person->_isName = @"CD_isName";
    person->name = @"CDname";
    person->isName = @"CDisName";
    person->_name = @"CD_name";
    
    //1、获取值
    id v = [person valueForKey:@"name"]; 
    NSLog(@"name = '%@'", v);

5、如果是基本数据类型必须包装成number,如果结构体就转换成 NSValue对象。
这也符合面向对象的概念,需要转换为对象。

///基本数据类型
@property (nonatomic, assign) NSInteger mun;
@property (nonatomic, assign) BOOL      sex;
@property (nonatomic, copy) NSNumber   *hei;

///结构体
@property (nonatomic, assign) CGPoint   point;

person.mun = 1234;
    person.sex = true;
    person.hei = @(188.8);
    person.point = CGPointMake(10, 20);

//    id num = [person valueForKey:@"num"];
    id sex = [person valueForKey:@"sex"];
    id hei = [person valueForKey:@"hei"];
    id point = [person valueForKey:@"point"];
    
//    NSLog(@"num = %@", num);
    NSLog(@"sex = %@", sex);
    NSLog(@"hei = %@", hei);
    NSLog(@"point = %@", point);

6、如果所有其他方法都失败,请调用 valueForUndefinedKey: 默认情况下,这会引发异常。当然可以重写 valueForUndefinedKey: 实现。

- (id)valueForUndefinedKey:(NSString *)key {
    //重写了方法,就可以返回默认值。如果这里设置了,那么上面的 num 就可以获到值了。
    return @(-1);
}
getter流程图

设置属性值

同样 setter 也有一些规则。设置值的默认实现方法为 setValue: forKey: 和扩展 setValue: forKeyPath: 这两个方法。

还是以name 为例说明,其中<Key> == Name, <key> = name

1 .按顺序 查找名为set<Key>:,_set<Key>:,setIs<Key>的方法。
2 .如果查找失败,且accessInstanceVariablesDirectly 返回 true。那么去找实例变量如下: _<key>_is<Key><key>is<Key>
3 .如果依然找不到,那么执行 setValue:forUndefinedKey:。默认实现爆出异常。

1、按照上诉方法去寻找对应的setter。在测试两个方法 setIs<Key>:_setIs<Key>: 这两个方法。

//MARK: - setKey. 的流程分析
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
- (void)_setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

[person setValue:@"6666" forKey:@"name"];

通过上面的测试。记过发现 setIsName: 执行了,但是 _setIsName: 没有执行。所以实现了三个方法 set<Key>: ,_set<Key>:setIs<Key>:

2 如果 accessInstanceVariablesDirectly 返回 true 。那么执行成员变量查找。如下代码调试。

@interface LGPerson : NSObject{
    @public
    NSString *_name;        //1
    NSString *_isName;      //2
    NSString *name;         //3
    NSString *isName;       //4
 }
//按照如下代码分别调试
     NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
//     NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
//     NSLog(@"%@-%@",person->name,person->isName);
//     NSLog(@"%@",person->isName);

通过上面的代码调试发现,会依次对成员变量 _name; _isName; name; isName; 赋值。

3 如果上面的步骤都失败,那么会查看是否实现了 -setValue: forUndefinedKey: 否则报异常。

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"%s", __func__);
}
setter 流程

使用和自定义KVC

上面既然有了 setValue: forKey:valueForKey: 的流程,那么我们来尝试一下自定义KVC。

头文件, 因为 NSKeyValueCoding 也是分类,所以呀参考分类来写。

@interface NSObject (CDKVC)

///这是两个默认kvc 方法
- (void)cd_setValue:(id)value forKey:(NSString *)key;
- (id)cd_valueForKey:(NSString *)key;

/// 这个方法默认返回true
+ (BOOL)cd_accessInstanceVariablesDirectly;

/// 这两个方法默认实现抛出异常
- (void)cd_setValue:(id)value forUndefinedKey:(NSString *)key;
- (id)cd_valueForUndefinedKey:(NSString *)key;
    
@end

实现文件


- (void)cd_setValue:(id)value forKey:(NSString *)key {
    
    // KVC 自定义
    // 1: 判断什么 key,必须是string,且长度大于0
    if (![key isKindOfClass:[NSString class]]) {
        return;
    }
    if (key == nil || key.length == 0) {
        return;
    }
    
    
    // 2: 查找 set<Key>: ,_set<Key>: 和 setIs<Key>:
    // 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 cd_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self cd_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self cd_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    // 3:判断是否能够直接赋值实例变量
    if (![self.class cd_accessInstanceVariablesDirectly] ) {
        
        //3.2 调用默认的 setValue: forUndefinedKey: 这个方法
        [self cd_setValue:value forUndefinedKey:key];
    }
    
    // 4: 间接变量
    // 获取 ivar -> 遍历 containsObjct -
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        // 4.2 获取相应的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 对相应的 ivar 设置值
       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:如果找不到相关实例
    [self cd_setValue:value forUndefinedKey:key];
    
}

- (id)cd_valueForKey:(NSString *)key {
    
    // 1:刷选key 判断非空
    if (![key isKindOfClass:[NSString class]]) {
        return nil;
    }
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    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(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }else 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
    
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        
        // 3.2 判断是否实现了 valueForUndefinedKey:
        return [self cd_valueForUndefinedKey:key];
    }
    
    // 4.找相关实例变量进行赋值
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // _name -> _isName -> name -> isName
    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);;
    }
    
    // 5 最后也没有查找到。
    return [self cd_valueForUndefinedKey:key];

}

+ (BOOL)cd_accessInstanceVariablesDirectly {
    return true;
}

- (void)cd_setValue:(id)value forUndefinedKey:(NSString *)key {
#if DEBUG
    NSLog(@"CDKVC - %s", __func__);
#endif
    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

- (id)cd_valueForUndefinedKey:(NSString *)key {
#if DEBUG
    NSLog(@"CDKVC - %s", __func__);
#endif
    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}


#pragma mark - 相关方法
- (BOOL)cd_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
 
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (id)performSelectorWithMethodName:(NSString *)methodName{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}

- (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]; 
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}

使用

    CDPerson *person = [[CDPerson alloc] init]; 
    [person cd_setValue:@"6666" forKey:@"name"];

应用场景

KVC 的使用场景还是与很多地方的,下面是笔者用到的一些地方。当然还有其他的地方。

1 通过KVC 可以修改一些系统类的内部函数。
2 通过KVC 可以实现字典模型转换的自动实现。
3 一些简单数据结构,没必要采用模型化,直接KVC赋、取值。
4 系统的一些功能,比如相机拍照的参数,音视频的编解码参数等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容