iOS KVC探索及自定义KVC

在日常的编码中我们会经常用到KVC,今天从以下3个方面来探索下经常使用的KVC。

  • 1.kvc赋值
  • 2.kvc取值
  • 3.自定义kvc

1.KVC赋值

首先来看这样一个例子

@interface MGPerson : NSObject {
    @public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end

#import "MGPerson.h"

@implementation MGPerson

//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);
}

@end

这里我们创建了一个类MGPerson并在其中写入了4个成员变量:_name,_isName,name,isName,以及如代码所示这样一些方法;

然后我们在ViewController中去调用我们写好的类,并通过setValue:<#(nullable id)#> forKey:<#(nonnull NSString *)#>去给我们声明的成员变量赋值,最后来看下打印结果

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MGPerson *person = [[MGPerson alloc] init];
    [person setValue:@"MG" forKey:@"name"];
    
    NSLog(@"p->name == %@",person->name);
    NSLog(@"p->_name == %@",person->_name);
    NSLog(@"p->_isName == %@",person->_isName);
    NSLog(@"p->isName == %@",person->isName);
}

打印结果

 -[MGPerson setName:] - MG
 p->name == (null)
 p->_name == (null)
 p->_isName == (null)
 p->isName == (null)

从打印结果中我们看到这里只调用了setName方法,符合我们的预期,那么假如注释掉setName方法呢

@implementation MGPerson

//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);
}
@end

MGPerson.m代码如上

打印结果

 -[MGPerson _setName:] - MG
 p->name == (null)
 p->_name == (null)
 p->_isName == (null)
 p->isName == (null)

这次却走了_setName,和我们的预期有些出入,接下来我们依次把.m文件的方法注释掉看下会发生什么

注释掉_setName的打印结果

 -[MGPerson setIsName:] - MG
 p->name == (null)
 p->_name == (null)
 p->_isName == (null)
 p->isName == (null)

这次却走了setIsName方法,再注释掉setIsName看看打印结果

 p->name == (null)
 p->_name == MG
 p->_isName == (null)
 p->isName == (null)

注释掉_name的打印结果

@interface MGPerson : NSObject {
    @public
//    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    MGPerson *person = [[MGPerson alloc] init];
    [person setValue:@"MG" forKey:@"name"];
    
    NSLog(@"p->name == %@",person->name);
//    NSLog(@"p->_name == %@",person->_name);
    NSLog(@"p->_isName == %@",person->_isName);
    NSLog(@"p->isName == %@",person->isName);
}

打印结果
 p->name == (null)
 p->_isName == MG
 p->isName == (null)

通过打印结果,我们发现首先会赋值给_name,如果没有_name则会赋值给_isName,如果没有_isName呢?接下来采用同样的方式把_isName也给注释掉

打印结果

 p->name == MG
 p->isName == (null)

这次就赋值给了name了,再把name给注释掉
打印结果

 p->isName == MG

这次就赋值给了isName了。

总结:
通过KVC赋值成员变量,有两个步骤
Step1: set<key> -> _set<key> -> setIs<key>如果这些都没实现那么会走到Step2
Step2:_<key> -> _is<key> -> <key> -> is<key>

这是为什么呢,我们去查看下官方文档是怎么解释的

苹果KVC官方文档
在文档中发现这样一段描述
Search Pattern for the Basic Setter
The default implementation of setValue:forKey:, given key and value parameters as input, attempts to set a property named key to value (or, for non-object properties, the unwrapped version of value, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:
1.Look for the first accessor named set<Key>: or _set<Key>, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.
2.If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.
3.Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

大致意思就是:
1.按顺序查找名为set<Key>_set<Key>的第一个访问器。如果找到,使用输入值(或根据需要展开的值)调用它并完成。
2.如果找不到简单访问器,并且类方法accessInstanceVariablesDirectly返回YES,请按顺序查找名为_<key>_is<key><key>is<key>的实例变量。如果找到,直接用输入值(或展开的值)设置变量并完成。
3.如果找不到访问器或实例变量,请调用setValue:forUndefinedKey:。默认情况下,这会引发异常,但NSObject的子类可能会提供特定于键的行为。

从官方文档中我们得到了解释,不过对比我们的实例,官方文档在第一条中漏掉了setIs<key>.

通过官方文档以及上面的代码示例我们就能得到这样一张流程图

KVC赋值流程

2.KVC取值

苹果的官方文档是这样说的

Search Pattern for the Basic Getter

The default implementation of valueForKey:, given a key parameter as input, carries out the following procedure, operating from within the class instance receiving the valueForKey: call.

  1. Search the instance for the first accessor method found with a name like get<Key>, <key>, is<Key>, or _<key>, in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.
  2. If no simple accessor method is found, search the instance for methods whose names match the patterns countOf<Key> and objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSArray class) and <key>AtIndexes: (corresponding to the NSArray method objectsAtIndexes:).
    If the first of these and at least one of the other two is found, create a collection proxy object that responds to all NSArray methods and return that. Otherwise, proceed to step 3.The proxy object subsequently converts any NSArray messages it receives to some combination of countOf<Key>, objectIn<Key>AtIndex:, and <key>AtIndexes: messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name like get<Key>:range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSArray, even if it is not.
  3. If no simple accessor method or group of array access methods is found, look for a triple of methods named countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class).
    If all three methods are found, create a collection proxy object that responds to all NSSet methods and return that. Otherwise, proceed to step 4.
    This proxy object subsequently converts any NSSet message it receives into some combination of countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSSet, even if it is not.
  4. If no simple accessor method or group of collection access methods is found, and if the receiver's class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.
  5. If the retrieved property value is an object pointer, simply return the result.
    If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that.
    If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.
  6. If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

大致,首先通过get<Key> -> <key> -> is<Key> -> _<key>这个几个方法去获取值,如果获取不到
再在 _<key> -> _is<Key> -> <key> -> is<Key> 成员变量中去获取,如果还是获取不到
valueForUndefinedKey抛出异常

@interface MGPerson : NSObject {
    @public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end

@implementation MGPerson
//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,

- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];    
    MGPerson *person = [[MGPerson alloc] init];    
    person->_name = @"_name";
    person->_isName = @"_isName";
    person->name = @"name";
    person->isName = @"isName";

    NSLog(@"取值:%@",[person valueForKey:@"name"]);
}

通过这样一个实例来验证,查看下打印结果

 取值:getName

这个打印结果说明,调用了getName方法

我们注释掉getName看看会不会调用name方法

取值:name

当注释掉getName方法时,结果与预想一致调用了name方法

接下来依次注释掉name,isName,_name,看看下结果

//注释掉 -(NSString *)name
取值:isName
//符合预期
//注释掉 -(NSString *) isName
取值:_name
//符合预期
//注释掉 -(NSString *) _name
取值:_name
//符合预期

方法的查找流程已经验证完毕,接下来验证下成员变量的流程,依次注释掉_name,_isName,name,isName;

//注释掉 _name成员变量
取值:_isName
//符合预期
//注释掉 _isName成员变量
取值: name
//符合预期
//注释掉 name成员变量
取值: isName
//符合预期
//注释掉 isName成员变量
程序崩掉
//符合预期
KVC取值流程

3.自定义KVC

自定义KVC其实就是实现-(void)setValue:(nullable id)value forKey:(NSString *)key-(id)valueForKey:(NSString *)key 方法

3.1实现set方法

  • 1.对传入的key进行非空判断
  • 2.构建setter即set<key>或者 _set<key>
  • 3.判断是否能够直接赋值给实例变量 ---- accessInstanceVariablesDirectly 返回yes
  • 4.设置ivar的值
  • 5.如果找不到相关实例则抛出异常
- (void)mg_setValue:(nullable id)value forKey:(NSString *)key {
    //自定义KVC
    //1.判断key
    if (key.length == 0 || key == nil) return;
        
    //2.构建setter set<key> or _set<key>
    //key的首字母大写
    NSString *newKey = key.capitalizedString;
    //拼接出方法名
    NSString *setKey = [NSString stringWithFormat:@"set%@:",newKey];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",newKey];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",newKey];
    //调用方法
    if ([self mg_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"------- %@ ---------",setKey);
        return;
    }else if ([self mg_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"------- %@ ---------",_setKey);
        return;
    }else if ([self mg_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"------- %@ ---------",setIsKey);
        return;
    }
    
    //3. 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    if ([self.class accessInstanceVariablesDirectly] == NO) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    //4.变量赋值
    //获取成员变量名
    NSMutableArray *nArray = [self getIvarListNames];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",newKey];
    NSString *isKey = [NSString stringWithFormat:@"is%@",newKey];
    
    if ([nArray containsObject:_key]) {
        //获取相应的ivar
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        //对相应的 ivar 设置值
        object_setIvar(self, ivar, value);
        return;
    } else if ([nArray containsObject:_isKey]) {
        //获取相应的ivar
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        //对相应的 ivar 设置值
        object_setIvar(self, ivar, value);
        return;
    } else if ([nArray containsObject:newKey]) {
        //获取相应的ivar
        Ivar ivar = class_getInstanceVariable([self class], newKey.UTF8String);
        //对相应的 ivar 设置值
        object_setIvar(self, ivar, value);
        return;
    } else if ([nArray containsObject:isKey]) {
        //获取相应的ivar
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        //对相应的 ivar 设置值
        object_setIvar(self, ivar, value);
        return;
    }
    
    //5.如果找不到相关实例
    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

- (BOOL)mg_performSelectorWithMethodName:(NSString *)methodName value:(id)value {
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
        return YES;
    }
    return NO;
}

//获取成员变量名列表
- (NSMutableArray *)getIvarListNames {
    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;
}

3.2实现get方法

  • 1.对传入的key进行非空判断
  • 2.找到相关方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex
  • 3:判断是否能够直接赋值实例变量
  • 4.找相关实例变量进行赋值
- (id)mg_valueForKey:(NSString *)key {
    //1:key判空
    if (key == nil || key.length == 0) return nil;
    
    //2:找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    //key 首字母大写处理
    NSString *newKey = key.capitalizedString;
    NSString *getKey = [NSString stringWithFormat:@"get%@",newKey];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",newKey];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",newKey];
    
    //调用方法
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(newKey)]) {
        return [self performSelector:NSSelectorFromString(newKey)];
    }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;
        }
    }
    
    //3.判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    //4.找相关实例变量进行赋值
    // 定义一个收集实例变量的可变数组
    NSMutableArray *nArray = [self getIvarListNames];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",newKey];
    NSString *isKey = [NSString stringWithFormat:@"is%@",newKey];
    if ([nArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([nArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([nArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([nArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }
    
    return @"";
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容