阅读 325

iOS KVC底层原理分析

1、前言

提起 KVC,大多数的第一反应是 setValue: forKey: 以及 setValue: forKeyPath:,这也就是我们的所说的键值编码(Key-value coding),键值编码是一种由 NSKeyValueCoding 非正式协议启用的机制,对象采用该协议来提供对其属性的间接访问。当对象符合键值编码时,可以通过简洁、统一的消息传递接口通过字符串参数对其属性进行寻址。详细解释可以进入官方文档查阅。接下来就一起跟我进入 KVC 的底层原理探索吧。

2、KVC 初探

1.KVC 的几种使用方式

创建一个 Person 类,在类中添加一些属性。

Person.h
typedef struct {
    float x, y, z;
} ThreeFloats;
@interface LGPerson : NSObject
@property (nonatomic, copy)   NSString          *name;
@property (nonatomic, strong) NSArray           *array;
@property (nonatomic, strong) NSMutableArray    *mArray;
@property (nonatomic, assign) int age;
@property (nonatomic)         ThreeFloats       threeFloats;
@property (nonatomic, strong) Student           *student;
@end
复制代码
1️⃣基本类型使用
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;

// 给person对象 name 属性赋值和取值
[person setValue:@"流年匆匆" forKey:@"name"];
[person valueForKey:@"name"];

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

// 嵌套属性访问
Student *student = [[Student alloc] init];
student.name    = @"xx";
person.student     = student;
[person setValue:@"学生" forKeyPath:@"student.name"];
NSLog(@"%@",[person valueForKeyPath:@"student.name"]);
复制代码
2️⃣集合类型使用
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// 修改不可变数组array的第一个值,
person.array = @[@"1",@"2",@"3"];
// 方法一,修改为1
NSArray *array = @[@"1",@"2",@"3"];
[person setValue:array forKey:@"array"];
// 方法二,kvc的方法,修改为10
NSMutableArray *arrayM = [person mutableArrayValueForKey:@"array"];
arrayM[0] = @"10";
复制代码
3️⃣字典使用
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

// 字典转模型
NSDictionary* dict = @{@"name":@"流年匆匆",@"age":@18};
[person setValuesForKeysWithDictionary:dict];
复制代码
4️⃣访问非对象属性
ThreeFloats floats = {1., 2., 3.};
// 非对象类型,需转换成相应的NSValue
NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslutValue = [person valueForKey:@"threeFloats"];
NSLog(@"value = %@",reslutValue);

// 创建一个同类型结构体用来接收reslutValue
ThreeFloats th;
[reslutValue getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);
复制代码

2. setValue:forKey: 底层原理探索

当我们调用 setValue:forKey: 的时候是怎么样将值赋值到我们的对象里去的呢?

官方文档解释
根据上面官方文档得知:

  • 1.第一步会先去对象里面查找是否有 set<Key>: 或者 _set<Key> 的访问器(即方法)。
  • 2.如果没有找到访问器并且类方法 accessInstanceVariablesDirectly 返回 YES,则会按顺序去查找名称为 _<key>_is< key><key><key> 的实例变量,如果找到直接设置变量并完成。
  • 3.如果方法和实例变量都没找到,则会调用 setValue:forUndefinedKey: 方法。

说明: 这里的 "key" 指成员变量名字, 书写格式需要符合 KVC 的命名规则。

1. setKey: 方法验证

创建 Person 类,并添加四个实例变量,以及添加 setName:_setName:accessInstanceVariablesDirectly 方法。(下面验证都是以这个对象为准,实例变量不变,方法变)

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

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

Person *person = [[Person alloc] init];
[person setValue:@"流年匆匆" forKey:@"name"];
复制代码
  • 结果:依次访问顺序
-[Person setName:] - 流年匆匆
-[Person _setName:] - 流年匆匆
复制代码

如果将所有 set 方法注释,accessInstanceVariablesDirectly 返回 NO,则会报 '[<Person 0x6000033a5860> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 崩溃。

2. accessInstanceVariablesDirectly 返回 YES 后的实例变量验证
@implementation Person
#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}
@end

Person *person = [[Person alloc] init];
[person setValue:@"流年匆匆" forKey:@"name"];
NSLog(@"_name:%@-_isName:%@-name:%@-isName%@",person->_name,person->_isName,person->name,person->isName);
NSLog(@"_isName:%@-name:%@-isName:%@",person->_isName,person->name,person->isName);
NSLog(@"name:%@-isName%@",person->name,person->isName);
NSLog(@"isName:%@",person->isName);
复制代码

accessInstanceVariablesDirectly 返回 YES,再将实例变量 _name_isNamenameisName 按顺序注释运行(NSLog也要依次注释哦),得到的结果会是以下输出。

1.KVC探索[4370:1716833] _name:流年匆匆-_isName:(null)-name:(null)-isName:(null)
2.KVC探索[4417:1720210] _isName:流年匆匆-name:(null)-isName:(null)
3.KVC探索[4445:1722057] name:流年匆匆-isName(null)
4.KVC探索[4468:1723450] isName:流年匆匆
复制代码

3. valueForKey: 底层原理探索

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.
// 中间是集合类型的,我们分析的是对象类型,所以跳过,有兴趣的可以自己看看
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.
复制代码

根据官方文档总体来说:

  • 1.第一步会先去按顺序查找 get<Key>, <key>, is<Key>_<key> 的访问器(即方法)。
  • 2.如果没有找到访问器并且类方法 accessInstanceVariablesDirectly 返回 YES,则按顺序搜索名为 _<key>_is<key><key>,或 is<Key> 的实例变量,如果找到,则直接获取实例变量的值并将值转换成相应类型返回。
  • 3.如果方法和实例变量都没有找到,则调用 valueForUndefinedKey: 方法。
1. getKey: 方法验证

将下面方法按照从上到下依次注释运行。

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

Person *person = [[Person alloc] init];
NSLog(@"取值:%@",[person valueForKey:@"name"]);
@end
复制代码
  • 结果:依次访问顺序
1.KVC探索[4725:1958586] 取值:getName
2.KVC探索[4749:1978501] 取值:name
3.KVC探索[4749:1978501] 取值:isName
4.KVC探索[4749:1978501] 取值:_name
复制代码

如果将所有 get 方法注释,accessInstanceVariablesDirectly 返回 NO,则会报 '[<Person 0x600001ce28b0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 崩溃。

2. accessInstanceVariablesDirectly 返回 YES 后实例变量验证
#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

Person *person = [[Person alloc] init];
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
复制代码

accessInstanceVariablesDirectly 返回 YES,再将实例变量 _name_isNamenameisName 按顺序注释运行(赋值代码也要依次注释哦),得到的结果会是以下输出。

1.KVC探索[4792:2019099] 取值:_name
2.KVC探索[4792:2019099] 取值:_isName
3.KVC探索[4792:2019099] 取值:name
4.KVC探索[4792:2019099] 取值:isName
复制代码

4. KVC 防崩溃处理

当我们在使用 setValue:forKey: 或者 valueForKey: 的时候,由于 key 需要自己手写且没有提示,所以很可能会不小心写错,然后就会报 setValue:forUndefinedKey: 或者 valueForUndefinedKey: 的崩溃,如何防止这种崩溃呢?直接在当前类实现 - (void)setValue:(id)value forUndefinedKey:(NSString *)key- (id)valueForUndefinedKey:(NSString *)key 即可。

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"来了");
}
- (id)valueForUndefinedKey:(NSString *)key {
    return nil;
}
//MARK: 空置防崩溃
- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"设置 %@ 是空值",key);
}
//MARK: - 键值验证 - 容错 - 派发 - 消息转发
- (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;
}
复制代码

5. 拓展

除了 KVC 能给对象属性赋值之外,其实我们经常用的是点语法,例: person.name = @"流年匆匆";,这种方法最终都会调用 reallySetProperty 函数对属性进行赋值,而又根据属性修饰符的不同,参数也是不一样的,看下面源码一目了然。

// atomic、copy 修饰的属性
void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, true, false);
}
// nonatomic、copy 修饰的属性
void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, false, true, false);
}
// 省略了一些其他属性修饰符的属性,大致和上面差不多

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        // 调用 changeIsa(Class newCls) 改变 isa 指向的 newValue
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        // 浅拷贝
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        // 深拷贝
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        // 引用计数加 1
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    // 释放旧值
    objc_release(oldValue);
}
复制代码

6. 总结

  • 1.使用 setValue:forKey: 的时候,会先去顺序查找对象是否有 set<Key>:_set<Key>setIs<Key>:(虽然setIs<Key>:这个方法官方文档上没有写,但确实是调用了的) 方法,有的话就调用进行赋值。
  • 2.如果没有找到,并且类方法 accessInstanceVariablesDirectly 返回 YES,则会按顺序去查找名称为 _<key>_is< key><key><key> 的实例变量,如果找到直接设置变量并完成。
  • 3.如果又没找到方法和实例变量,则会调用 setValue:forUndefinedKey: 方法,如果对象没有实现 setValue:forUndefinedKey: 则会报 '[<Person 0x60000346a760> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 的崩溃。
  • 4.在使用 valueForKey: 的时候,先去按顺序查找对象是否有 get<Key>, <key>, is<Key>_<key> 的方法,有的话就返回。
  • 5.如果没有找到,并且类方法 accessInstanceVariablesDirectly 返回 YES,则会按顺序搜索名为 _<key>_is<key><key>,或 is<Key> 的实例变量,如果找到,则直接获取实例变量的值并将值转换成相应类型返回。
  • 6.如果又没找到方法和实例变量,则调用 valueForUndefinedKey: 方法。如果对象没有实现 valueForUndefinedKey: 则会报 '[<Person 0x600001ce28b0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 的崩溃。