iOS – 探索KVC及其原理

时间:2021-7-21 作者:qvyue

KVC 全名:Key-value coding,中文简直编码。苹果对其定义如下:

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.
键-值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议来提供对其属性的间接访问。当对象符合键值编码时,可以通过简洁、统一的消息传递接口通过字符串参数对其属性进行寻址。这种间接访问机制补充了实例变量及其关联访问器方法提供的直接访问。

一、使用场景

KVC在我们的平常开发中应该是高频使用的,接下来我们先简单看一下常用的KVC使用场景:

1.基本类型

    LPPerson *person = [[LGPerson alloc] init];
    // 一般setter 方法
    person.name      = @"name"; // setter -- llvm
    person.age       = 18;
    person->myName   = @"name";
    NSLog(@"%@ - %d - %@",person.name,person.age,person->myName);

    // 非正式协议 - 间接访问
    [person setValue:@"KC" forKey:@"name"];

2.集合类型

    person.array = @[@"1",@"2",@"3"];
    // 修改数组
    // person.array[0] = @"100";
    // 第一种:搞一个新的数组 - KVC 赋值就OK
    NSArray *array = [person valueForKey:@"array"];
    array = @[@"100",@"2",@"3"];
    [person setValue:array forKey:@"array"];
    NSLog(@"%@",[person valueForKey:@"array"]);
    // 第二种
    NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
    mArray[0] = @"200";
    NSLog(@"%@",[person valueForKey:@"array"]);

3.访问非对象属性

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

    ThreeFloats floats = {1.,2.,3.};
    NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSValue *value1    = [person valueForKey:@"threeFloats"];
    NSLog(@"%@",value1);
    
    ThreeFloats th;
    [value1 getValue:&th];
    NSLog(@"%f-%f-%f",th.x,th.y,th.z);

4.key-path

    LPStudent *student = [LPStudent alloc];
    student.subject    = @"火箭班";
    person.student     = student;
    
    [person setValue:@"Swift" forKeyPath:@"student.subject"];
    NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

二、KVC原理

既然要了解KVC的原理,那么苹果官方的文档必定是不二之选,[传送门]。(https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/SearchImplementation.html#//apple_ref/doc/uid/20000955-CJBBBFFA)

我们先看下settergetter

1.setter

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: or _set, 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 _, _is, , or is, 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.
搜索基本Setter的模式
setValue:forKey:的默认实现,给定键和值参数作为输入,尝试在接收调用的对象内部设置一个名为key的属性为value(或者,对于非对象属性,为value的解包装版本,如描述的非对象值),使用以下过程:
1.按照顺序查找第一个访问器set:_set。如果找到,使用输入值(或根据需要打开包装值)和finish调用它。
2.如果没有找到简单的访问器,并且类方法accessinstancevariables直接返回YES,那么查找一个实例变量,其名称如下:__is,或is。如果找到,直接用输入值(或打开包装的值)和finish设置变量。
3.在没有找到访问器或实例变量时,调用setValue:forUndefinedKey:。这在默认情况下会引发一个异常,但是NSObject的一个子类可能提供特定于键的行为。

举个例子:比如我们要给LPPersonname赋值,首先会去找_name,如果没有没有找到,就会找_isName,还是没有找name,最后再找isName。如果找到了就直接设值,没有找到就会调用setValue:forUndefinedKey,来抛出异常。

2.getter

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, , is, or _, 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 and objectInAtIndex: (corresponding to the primitive methods defined by the NSArray class) and 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, objectInAtIndex:, and 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: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, enumeratorOf, and memberOf: (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, enumeratorOf, and memberOf: 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 _, _is, , or is, 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.
基本Getter的搜索模式
valueForKey:的默认实现,给定一个key参数作为输入,执行以下过程,从接收valueForKey:调用的类实例内部进行操作。
1.在实例中搜索找到的第一个访问器方法,其名称如下:getis,或_。如果找到了,就调用它,并在步骤5中处理结果。否则继续下一步。
2.如果没有找到简单的访问器方法,则在实例中搜索名称与模式countOfobjectInAtIndex:(对应于NSArray类定义的基本方法)和 AtIndexes:(对应于NSArray方法objectsAtIndexes:)匹配的方法。
如果找到第一个和至少两个中的一个,则创建一个集合代理对象,该对象响应所有NSArray方法并返回该方法。否则,继续执行步骤3。
代理对象随后将接收到的任何NSArray消息转换为countOfobjectInAtIndex: AtIndexes:的一些组合,这些组合将消息发送给创建它的符合键值编码的对象。如果原始对象还实现了一个名为get:range:的可选方法,代理对象也会在适当的时候使用它。实际上,代理对象与与键值编码兼容的对象一起工作,允许底层属性像NSArray一样工作,即使它不是NSArray
3.如果没有找到简单的访问方法或数组访问方法组,查找三个方法,分别为countOfenumeratorOfmemberOf:(对应于NSSet类定义的原语方法)。
如果这三个方法都找到了,创建一个集合代理对象,它响应所有NSSet方法并返回那个。否则,继续执行步骤4。
这个代理对象随后将它接收到的任何NSSet消息转换为countOfenumeratorOfmemberOf:消息的组合,并发送给创建它的对象。实际上,代理对象与遵循键值编码的对象一起工作,允许底层属性像NSSet一样运行,即使它不是NSSet
4.如果没有找到简单的访问方法或集合访问方法组,并且如果接收方的类方法accessinstancevariables直接返回YES,那么按照顺序搜索一个实例变量:__is,或is。如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤6。
5.如果检索到的属性值是一个对象指针,只需返回结果。
如果值是NSNumber支持的标量类型,将其存储在NSNumber实例中并返回。
如果结果是NSNumber不支持的标量类型,转换为NSValue对象并返回它。
6.如果所有其他方法都失败,则调用valueForUndefinedKey:。这在默认情况下会引发一个异常,但是NSObject的一个子类可能提供特定于键的行为。

setter类似,比如我们要取LPPersonname赋值,首先会去找getName,如果没有没有找到,就会找name,还是没有找isName,再找_name。如果找到了就直接设值,没有找到就会去实例中搜索名称与模式。

接下来我们通过一个demo演示一下:
新建LPPerson对象:

@interface LPPerson : NSObject{
    @public
        NSString *_isName;
        NSString *name;
        NSString *isName;
        NSString *_name;

}

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@end

@implementation LPPerson

#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

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


//MARK: - valueForKey 流程分析 - get, , is, or _,

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

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

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

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

@end

我们在LPPerson中创建了4个成员变量:_isName , name, isName, _name,是用来测试文档说的当accessinstancevariablesYES时,会访问成员变量。
Viewcontroller中实现如下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LPPerson *person = [[LPPerson alloc] init];
    
    // 1: KVC - 设置值的过程 setValue 分析调用过程
     [person setValue:@"peter" forKey:@"name"];

}

我们运行工程,查看打印结果:

2020-10-31 10:50:55.580747+0800 002-KVC取值&赋值过程[33506:1118311] -[LPPerson setName:] - peter

可以看到执行了setName方法,我们把setName注释掉,再次运行:

2020-10-31 10:52:44.333568+0800 002-KVC取值&赋值过程[33532:1120381] -[LPPerson _setName:] - peter

结果是执行了_setName方法,我们把_setName也注释掉,再运行:

2020-10-31 10:53:52.679457+0800 002-KVC取值&赋值过程[33548:1121542] -[LPPerson setIsName:] - peter

结果是执行了setIsName方法,我们把setIsName也注释掉,再运行:
这次没有执行_setIsName方法。
按照文档第二步,说如果没有找到简单的访问器,并且accessinstancevariablesYES时,回去查找实例变量,接下来我们来再次验证下,在viewDidLoad中代码修改为如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LPPerson *person = [[LPPerson alloc] init];
    
    // 1: KVC - 设置值的过程 setValue 分析调用过程
     [person setValue:@"peter" forKey:@"name"];

     NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
}

执行,观察运行结果:

2020-10-31 11:01:07.472392+0800 002-KVC取值&赋值过程[33622:1127379] peter-(null)-(null)-(null)

说明是set_name中的值,我们再修改一下代码:

@interface LPPerson : NSObject{
    @public
        NSString *_isName;
        NSString *name;
        NSString *isName;
//        NSString *_name;

}

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@end

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@end

//     NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
     NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);

再次运行:

2020-10-31 13:39:29.052301+0800 002-KVC取值&赋值过程[34763:1208784] peter-(null)-(null)

说明是set_isName中的值,我们再修改一下代码:

@interface LPPerson : NSObject{
    @public
//        NSString *_isName;
        NSString *name;
        NSString *isName;
//        NSString *_name;

}

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@end

//     NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
//     NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
     NSLog(@"%@-%@",person->name,person->isName);

运行,查看结果:

2020-10-31 13:40:58.689786+0800 002-KVC取值&赋值过程[34789:1210795] peter-(null)

说明是setname的值,我们再修改下:

@interface LPPerson : NSObject{
    @public
//        NSString *_isName;
//        NSString *name;
        NSString *isName;
//        NSString *_name;

}

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@end

//     NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
//     NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
//     NSLog(@"%@-%@",person->name,person->isName);
     NSLog(@"%@",person->isName);

再运行:

2020-10-31 13:42:36.330874+0800 002-KVC取值&赋值过程[34813:1212798] peter

可以看到,也是可以的。
这个时候,我们把LPPersonaccessInstanceVariablesDirectly置为NO,再运行下呢:

+ (BOOL)accessInstanceVariablesDirectly{
    return NO;
}
iOS - 探索KVC及其原理
image.png

可以看到,直接奔溃了。

上述的结果就可以验证到文档说的set过程。

接下来,我们验证下get的过程:
先将LPPerson的成员变量恢复:

@interface LPPerson : NSObject{
    @public
        NSString *_isName;
        NSString *name;
        NSString *isName;
        NSString *_name;

}

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@end

再在viewDidLoad中添加代码:


- (void)viewDidLoad {
    [super viewDidLoad];
    
    LPPerson *person = [[LPPerson alloc] init];
    
    // 1: KVC - 设置值的过程 setValue 分析调用过程
     [person setValue:@"peter" forKey:@"name"];

    // setter - getter  - KVC 设值 和 取值的流程
    // 2: KVC - 取值的过程
    NSLog(@"取值:%@",[person valueForKey:@"name"]);
}

运行查看结果:

2020-10-31 13:48:13.369199+0800 002-KVC取值&赋值过程[34884:1217744] 取值:getName

走到了getName方法,我们注释掉getName方法,再次运行:

2020-10-31 13:50:46.767696+0800 002-KVC取值&赋值过程[34911:1220210] 取值:name

可以看到执行了name方法,我们注释掉name方法,再次运行:

2020-10-31 13:51:21.653834+0800 002-KVC取值&赋值过程[34924:1221160] 取值:isName

执行了isName方法,我们再把isName注释掉,再运行:

2020-10-31 13:51:59.197860+0800 002-KVC取值&赋值过程[34937:1222157] 取值:_name

也执行了_name方法
同样的,我们把_name注释掉,并且设置accessinstancevariables返回YES,再次修改下ViewDidLoad方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LPPerson *person = [[LPPerson alloc] init];
    
    // 1: KVC - 设置值的过程 setValue 分析调用过程
     [person setValue:@"peter" forKey:@"name"];

    // setter - getter  - KVC 设值 和 取值的流程
    // 2: KVC - 取值的过程
     person->_name = @"_name = peter";
     NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
   

再次运行:

2020-10-31 13:55:49.859847+0800 002-KVC取值&赋值过程[34997:1226759] 取值:_name = peter

再修改成_isName:

@interface LPPerson : NSObject{
    @public
        NSString *_isName;
        NSString *name;
        NSString *isName;
//        NSString *_name;

}

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@end
- (void)viewDidLoad {
    [super viewDidLoad];
    
    LPPerson *person = [[LPPerson alloc] init];
    
    // 1: KVC - 设置值的过程 setValue 分析调用过程
     [person setValue:@"peter" forKey:@"name"];

    // setter - getter  - KVC 设值 和 取值的流程
    // 2: KVC - 取值的过程
     //    person->_name = @"_name = peter";
      person->_isName = @"_isName = peter";
     NSLog(@"取值:%@",[person valueForKey:@"name"]);
}

再运行:

2020-10-31 13:57:15.787149+0800 002-KVC取值&赋值过程[35027:1229056] 取值:_isName = peter

在修改为name

@interface LPPerson : NSObject{
    @public
//        NSString *_isName;
        NSString *name;
        NSString *isName;
//        NSString *_name;

}

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LPPerson *person = [[LPPerson alloc] init];
    
    // 1: KVC - 设置值的过程 setValue 分析调用过程
     [person setValue:@"peter" forKey:@"name"];

    // setter - getter  - KVC 设值 和 取值的流程
    // 2: KVC - 取值的过程
//    person->_name = @"_name = peter";
//    person->_isName = @"_isName = peter";
     person->name = @"name = peter";
     NSLog(@"取值:%@",[person valueForKey:@"name"]);
}

运行:

2020-10-31 13:58:42.396675+0800 002-KVC取值&赋值过程[35052:1231112] 取值:name = peter

最后,我们再修改为isName:

@interface LPPerson : NSObject{
    @public
//        NSString *_isName;
//        NSString *name;
        NSString *isName;
//        NSString *_name;

}

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LPPerson *person = [[LPPerson alloc] init];
    
    // 1: KVC - 设置值的过程 setValue 分析调用过程
     [person setValue:@"peter" forKey:@"name"];

    // setter - getter  - KVC 设值 和 取值的流程
    // 2: KVC - 取值的过程
//    person->_name = @"_name = peter";
//    person->_isName = @"_isName = peter";
//    person->name = @"name = peter";
      person->isName = @"isName = peter";
     NSLog(@"取值:%@",[person valueForKey:@"name"]);
}

运行:

2020-10-31 13:59:52.374174+0800 002-KVC取值&赋值过程[35077:1232750] 取值:isName = peter

我们再把accessinstancevariables设置为NO试试:

+ (BOOL)accessInstanceVariablesDirectly{
    return NO;
}
iOS - 探索KVC及其原理
image.png

同样奔溃了,上述结果都验证了文档的过程。
所以可见文档的重要性,我们一定要要成良好的查阅文档的习惯,可以帮助我们正确的理解。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。