阅读 251

iOS 底层原理:KVC 底层探索

准备

一、 KVC 简介

什么是 KVC

  • KVC的全称是Key-Value Coding(键值编码),是由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问,可以通过字符串来访问一个对象的成员变量或其关联的存取方法(getter or setter)。
  • 通常,我们可以直接通过存取方法或变量名来访问对象的属性。我们也可以使用KVC间接访问对象的属性,并且KVC还可以访问私有变量。某些情况下,KVC还可以帮助简化代码。

访问对象属性

1. 常用 API

- (nullable id)valueForKey:(NSString *)key;         // 通过 key 来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通过 keyPath 来取值

- (void)setValue:(nullable id)value forKey:(NSString *)key;         // 通过 key 来赋值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通过 keyPath 来赋值
复制代码

2. 基础操作

SSLPersonSSLPackage的声明:

@interface SSLPerson : NSObject
@property (nonatomic, copy)   NSString   *name;
@property (nonatomic, strong) SSLPackage *package;
@end

@interface SSLPackage : NSObject
@property (nonatomic, copy) NSNumber *money;
@end
复制代码

可以通过setter方法为name属性赋值:

person.name = @"SSL";
复制代码

也可以通过KVC的方式为name属性赋值:

[person setValue:@"SSL" forKey:@"name"];
复制代码

3. KeyPath

KVC还支持多级访问,KeyPath用法跟点语法相同。例如:我们想对personpackage属性的money属性赋值,其KeyPathpackage.money

[person setValue:@1000000 forKeyPath:@"package.money"];
复制代码

4. 多值操作

给定一组Key,获得一组value,以字典的形式返回。该方法为数组中的每个Key调用valueForKey:方法。

- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
复制代码

将指定字典中的值设置到消息接收者的属性中,使用字典的Key标识属性。默认实现是为每个键值对调用setValue:forKey:方法 ,会根据需要用nil替换NSNull对象。

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;
复制代码

二、 苹果官方文档解释 KVC

苹果官方文档 关于KVC的解释:

About 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.

You typically use accessor methods to gain access to an object’s properties. A get accessor (or getter) returns the value of a property. A set accessor (or setter) sets the value of a property. In Objective-C, you can also directly access a property’s underlying instance variable. Accessing an object property in any of these ways is straightforward, but requires calling on a property-specific method or variable name. As the list of properties grows or changes, so also must the code which accesses these properties. In contrast, a key-value coding compliant object provides a simple messaging interface that is consistent across all of its properties.

Key-value coding is a fundamental concept that underlies many other Cocoa technologies, such as key-value observing, Cocoa bindings, Core Data, and AppleScript-ability. Key-value coding can also help to simplify your code in some cases.

三、 KVC 设值和取值过程

KVC 设值过程

打开 苹果官方文档 查看KVC的设值原理:

Search Pattern for the Basic Setter

The default implementation of setValue:forKey:, given key and valueparameters 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.
  • 查看文档可以看到,KVC的设值过程有三步,下面进行分析。

1. set<Key>: / _set<Key>

按照set<Key>:_set<Key>:顺序查找方法。
如果找到就调用并将value传进去,否则执行下一步,接下来用示例进行验证。

创建SSLPerson类,实现setName:_setName:方法:

@interface SSLPerson : NSObject
@end
@implementation SSLPerson
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name {
    NSLog(@"%s - %@",__func__,name);
}
@end
复制代码

KVC设值代码调用:

SSLPerson *person = [[SSLPerson alloc] init];
[person setValue:@"SSL" forKey:@"name"];
复制代码

查看结果:

-[SSLPerson setName:] - SSL
复制代码
  • setName:被成功调用,_setName:没有被调用,说明这两个方法是不会重复调用的。
  • 注意:如果声明了name属性,即使setName:没有实现,_setName也是不会被调用的。

去掉setName:方法:

@interface SSLPerson : NSObject
@end
@implementation SSLPerson
- (void)_setName:(NSString *)name {
    NSLog(@"%s - %@",__func__,name);
}
@end
复制代码

再次运行程序,可以看到_setName:方法被调用了:

-[SSLPerson _setName:] - SSL
复制代码

注:测试的时候发现set<IsKey>:也是可以被调用的。

2. _<key>_is<Key><key>, or is<Key>

查看消息接收者类的+accessInstanceVariablesDirectly方法的返回值(默认返回YES)。如果返回YES,就按照_<key>_is<Key><key>is<Key>顺序查找成员变量。如果找到就将value赋值给它,否则执行下一步。如果+accessInstanceVariablesDirectly方法返回NO也执行下一步。

创建测试代码:

@interface SSLPerson : NSObject{
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end
@implementation SSLPerson
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    SSLPerson *person = [[SSLPerson alloc] init];
    [person setValue:@"SSL" forKey:@"name"];
    
    NSLog(@"_name = %@",person->_name);
    NSLog(@"_isName = %@",person->_isName);
    NSLog(@"name = %@",person->name);
    NSLog(@"isName = %@",person->isName);
}
复制代码

查看打印结果:

_name = SSL
_isName = (null)
name = (null)
isName = (null)
复制代码
  • 只有_name被赋值,没有问题。

测试代码去掉_name

@interface SSLPerson : NSObject{
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end
@implementation SSLPerson
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    SSLPerson *person = [[SSLPerson alloc] init];
    [person setValue:@"SSL" forKey:@"name"];
    
    NSLog(@"_isName = %@",person->_isName);
    NSLog(@"name = %@",person->name);
    NSLog(@"isName = %@",person->isName);
}
复制代码

查看打印结果:

_isName = SSL
name = (null)
isName = (null)
复制代码
  • 只有_isName被赋值,没有问题,剩下的两种情况经过测试也是没问题的。

3. setValue:forUndefinedKey:

调用setValue:forUndefinedKey:方法,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定key做一些特殊处理。

KVC 取值过程

打开 苹果官方文档 查看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 accessInstanceVariablesDirectlyreturns 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 NSNumberinstance and return that.
    If the result is a scalar type not supported by NSNumber, convert to an NSValueobject 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.]

1. get<Key><key>is<Key>, or _<key>

按照get<Key><key>is<Key>_<key>顺序查找方法。
如果找到就调用取值并执行5,否则执行下一步;

2. countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:

查找countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:命名的方法。
如果找到第一个和后面两个中的至少一个,则创建一个能够响应所有NSArray的方法的集合代理对象(类型为NSKeyValueArray,继承自NSArray),并返回该对象。否则执行3

  • 代理对象随后将其接收到的任何NSArray消息转换为countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes:消息的组合,并将其发送给KVC调用方。如果原始对象还实现了一个名为get<Key>:range:的可选方法,则代理对象也会在适当时使用该方法。
  • KVC调用方与代理对象一起工作时,允许底层属性的行为如同NSArray一样,即使它不是NSArray

3. countOf<Key>enumeratorOf<Key>memberOf<Key>:

查找countOf<Key>enumeratorOf<Key>memberOf<Key>:命名的方法。
如果三个方法都找到,则创建一个能够响应所有NSSet的方法的集合代理对象(类型为NSKeyValueSet,继承自NSSet),并返回该对象。否则执行4

  • 代理对象随后将其接收到的任何NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>:消息的组合,并将其发送给KVC调用方。
  • KVC调用方与代理对象一起工作时,允许底层属性的行为如同NSSet一样,即使它不是NSSet

4. +accessInstanceVariablesDirectly

查看消息接收者类的+accessInstanceVariablesDirectly方法的返回值(默认返回YES)。如果返回YES,就按照_<key>_is<Key><key>is<Key>顺序查找成员变量。如果找到就直接取值并执行5,否则执行6。如果+accessInstanceVariablesDirectly方法返回NO也执行6

5. 根据需要进行数据类型转换

如果取到的值是一个对象指针,即获取的是对象,则直接将对象返回。
如果取到的值是一个NSNumber支持的数据类型,则将其存储在NSNumber实例并返回。
如果取到的值不是一个NSNumber支持的数据类型,则转换为NSValue对象, 然后返回。

6. valueForUndefinedKey:

调用valueForUndefinedKey:方法,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定key做一些特殊处理。

四、 KVC 自定义实现

自定义设值

- (void)ssl_setValue:(nullable id)value forKey:(NSString *)key {
    // 1: key 判空
    if (key == nil || key.length == 0) {
        return;
    }
    
    // 2: setter set<Key>: or _set<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 ssl_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self ssl_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self ssl_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    // 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: 间接变量
    // 获取 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:如果找不到相关实例
    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
复制代码

自定义取值

- (nullable id)ssl_valueForKey:(NSString *)key {
    
    // 1:刷选key 判断非空
    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] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 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);;
    }

    return @"";
}
复制代码

相关方法

#pragma mark - 相关方法
- (BOOL)ssl_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];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}
复制代码

拓展

文章分类
iOS
文章标签