ios 底层探索———KVO

333 阅读21分钟

1.KVC简介

1.1 KVC概念(苹果文档概念的翻译)

KVC (Key-Value Coding), 是利用 NSKeyValueCoding 非正式协议实现的一种机制, 对象采用这种机制来提供对其属性的间接访问。当对象采用该协议时, 可以通过简洁统一的方法来访问其属性。简单来说, 就是我们在开发中可以通过key名直接访问对象的属性, 或者对属性进行赋值操作, 而不需要去调用明确的存取方法。这样就允许我们在运行时去动态地访问和修改对象的属性, 而不是在编译时决定

1.2 KVC在底层源码的展示

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

在LGPerson中设置(nonatomic, copy)name属性,重写name的setter 和 gertter方法。当使用点语法,对name赋值的时候,则会调用重写的setter方法,那么系统是如何从.name 调用到setter的重写方法中的呢?我们探索一下。 如果我们不去重写setter和getter方法,编译器就会帮我们自动生成属性所需的setter和getter方法。具体是通过怎样的流程来实现的呢? 我们在 libObjc 源码中搜索objc_setProperty,字面意思可以看出给属性赋值的,因为name对应的(nonatomic, copy),我们可以找到objc_setProperty_nonatomic_copy,在main方法中放上面的方法,注释调重写的setter和getter方法。然后在此处下断点,可以测试到当调用点name的时候,就会来到objc_setProperty_nonatomic_copy。

,我们可以看到内层又调用了reallySetProperty方法进行的属性的赋值。那么又是如何调用objc_setProperty这个地方的呢?我们可以尝试在 LLVM 的源码中进行搜索关键字 objc_setProperty:

在 clang 编译器前端的 RewriteModernObjC 命名空间下的 RewritePropertyImplDecl 方法中:

然后在 CodeGen 目录下的匿名命名空间下的 ObjcCommonTypesHelper 的 getOptimizedSetPropertyFn 处可以看到以下代码:

这里可以连接到上面提到的objc_setProperty了,setter方法的源码大概流程就这样了。

2.KVC的初探和基础使用方法

LGPerson点h文件,LGStudent.h文件中的源码如下:

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

@interface LGPerson : NSObject{
   @public
   NSString *myName;
}

@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) LGStudent         *student;

@end

@interface LGStudent : NSObject
@property (nonatomic, copy)   NSString          *name;
@property (nonatomic, copy)   NSString          *subject;
@property (nonatomic, copy)   NSString          *nick;
@property (nonatomic, assign) int               age;
@property (nonatomic, assign) int               length;
@property (nonatomic, strong) NSMutableArray    *penArr;
@end

2.1 1:Key-Value Coding (KVC) : 基本类型

[person setValue:@"KC" forKey:@"name"];
    [person setValue:@19 forKey:@"age"];
    [person setValue:@"酷C" forKey:@"myName"];
    NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]);

2.2:KVC - 集合类型

改变集合中的一个值,重新赋值。

person.array = @[@"1",@"2",@"3"];
    // 由于不是可变数组 - 无法做到
    // person.array[0] = @"100";
    NSArray *array = [person valueForKey:@"array"];
    // 用 array 的值创建一个新的数组
    array = @[@"100",@"2",@"3"];
    [person setValue:array forKey:@"array"];
    NSLog(@"%@",[person valueForKey:@"array"]);
    
    // KVC 的方式
    NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
    ma[0] = @"100";
    NSLog(@"%@",[person valueForKey:@"array"]);

2.3:KVC - 集合操作符

聚合操作符

@avg: 返回操作对象指定属性的平均值 @count: 返回操作对象指定属性的个数 @max: 返回操作对象指定属性的最大值 @min: 返回操作对象指定属性的最小值 @sum: 返回操作对象指定属性值之和

数组操作符

@distinctUnionOfObjects: 返回操作对象指定属性的集合--去重 @unionOfObjects: 返回操作对象指定属性的集合

嵌套操作符

@distinctUnionOfArrays: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSArray @unionOfArrays: 返回操作对象(集合)指定属性的集合 @distinctUnionOfSets: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSSet

#pragma mark - 聚合操作符
// @avg、@count、@max、@min、@sum
- (void)aggregationOperator{
    NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *p = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [p setValuesForKeysWithDictionary:dict];
        [personArray addObject:p];
    }
    NSLog(@"%@", [personArray valueForKey:@"length"]);
    
    /// 平均身高
    float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
    NSLog(@"%f", avg);
    
    int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
    NSLog(@"%d", count);
    
    int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
    NSLog(@"%d", sum);
    
    int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
    NSLog(@"%d", max);
    
    int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
    NSLog(@"%d", min);
}

// 数组操作符 @distinctUnionOfObjects @unionOfObjects
- (void)arrayOperator{
    NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *p = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [p setValuesForKeysWithDictionary:dict];
        [personArray addObject:p];
    }
    NSLog(@"%@", [personArray valueForKey:@"length"]);
    // 返回操作对象指定属性的集合
    NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.length"];
    NSLog(@"arr1 = %@", arr1);
    // 返回操作对象指定属性的集合 -- 去重
    NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.length"];
    NSLog(@"arr2 = %@", arr2);
    
}

// 嵌套集合(array&set)操作 @distinctUnionOfArrays @unionOfArrays @distinctUnionOfSets
- (void)arrayNesting{
    
    NSMutableArray *personArray1 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *student = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [student setValuesForKeysWithDictionary:dict];
        [personArray1 addObject:student];
    }
    
    NSMutableArray *personArray2 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGPerson *person = [LGPerson new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personArray2 addObject:person];
    }
    
    // 嵌套数组
    NSArray* nestArr = @[personArray1, personArray2];
    
    NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];
    NSLog(@"arr = %@", arr);
    
    NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];
    NSLog(@"arr1 = %@", arr1);
}

- (void)setNesting{
    
    NSMutableSet *personSet1 = [NSMutableSet set];
    for (int i = 0; i < 6; i++) {
        LGStudent *person = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personSet1 addObject:person];
    }
    NSLog(@"personSet1 = %@", [personSet1 valueForKey:@"length"]);
    
    NSMutableSet *personSet2 = [NSMutableSet set];
    for (int i = 0; i < 6; i++) {
        LGPerson *person = [LGPerson new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personSet2 addObject:person];
    }
    NSLog(@"personSet2 = %@", [personSet2 valueForKey:@"length"]);

    // 嵌套set
    NSSet* nestSet = [NSSet setWithObjects:personSet1, personSet2, nil];
    // 交集
    NSArray* arr1 = [nestSet valueForKeyPath:@"@distinctUnionOfSets.length"];
    NSLog(@"arr1 = %@", arr1);
}

2.4:KVC - 访问非对象属性

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

2.5:KVC - 层层访问

LGStudent *student = [[LGStudent alloc] init];
    student.subject    = @"iOS";
    person.student     = student;
    [person setValue:@"大师班" forKeyPath:@"student.subject"];
    NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

2.6属性验证

KVC 支持属性验证,而这一特性是通过validateValue:forKey:error: (或validateValue:forKeyPath:error:) 方法来实现的。这个验证方法的默认实现是去收到这个验证消息的对象(或keyPath中最后的对象)中根据 key 查找是否有对应的 validate:error: 方法实现,如果没有,验证默认成功,返回 YES。 而由于 validate:error: 方法通过引用接收值和错误参数,所以会有以下三种结果:

验证成功,返回 YES,对属性值不做任何改动。 验证失败,返回 NO,但对属性值不做改动,如果调用者提供了 NSError 的话,就把错误引用设置为指示错误原因的NSError对象。 验证失败,返回 YES,创建一个新的,有效的属性值作为替代。在返回之前,该方法将值引用修改为指向新值对象。 进行修改时,即使值对象是可变的,该方法也总是创建一个新对象,而不是修改旧对象。

Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
    NSLog(@"%@",error);
}

复制代码那么是否系统会自动进行属性验证呢? 通常,KVC 或其默认实现均未定义任何机制来自动的执行属性验证,也就是说需要在适合你的应用的时候自己提供属性验证方法。 某些其他 Cocoa 技术在某些情况下会自动执行验证。 例如,保存 managed object context 时,Core Data会自动执行验证。另外,在 macOS 中,Cocoa Binding允许你指定验证应自动进行。

3.KVC的设值和取值原理

3.1KVC的设值过程

当我们去调用 setValue:值 forKey:名字 设值方法时, 底层的执行机制大致如下:

    1. 程序会去优先调用, set: 或者 _set, setIs 方法, 如果存在这些命名规则的方法, 会直接调用该方法进行赋值。调用优先顺序按照上面书写的顺序。

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

    1. 如果没有找到步骤1的方法, 程序会回去判断 + (BOOL)accessInstanceVariablesDirectly; 方法的返回值, 如果该方法返回值为NO (默认为 YES, 在我们重写该方法时有可能返回NO, 一般不会返回NO), 则会执行 setValue: forUndefinedKey: 方法报错。
    1. 如果上一步方法的返回值为YES, 程序会去查找命名方式为 _, _, , 形式的实例变量, 加入存在该形式的实例变量, 则会直接将我们调用方法的值赋值给该实例变量。这里的查找优先顺序也会按照书写顺序去查找。
    1. 如果第三步没有查找到符合规则的实例变量, 程序就会去执行 setValue: forUndefinedKey: 方法进行报错。

下面是一段验证代码:

// 声明实例变量
@interface MCPerson : NSObject {
    @public
    NSString *_name;
    NSString *name;
    NSString *_isName;
    NSString *isName;
}
@end

// .m文件实现上面提到的方法进行监听
+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (void)setValue:(id)value forUndefinedKey:(nonnull NSString *)key {
    NSLog(@"设置出现异常!!!");
}

- (void)setName:(NSString *)value {
    NSLog(@"%s - %@", __func__, value);
}
- (void)setIsName:(NSString *)value {
    NSLog(@"%s - %@", __func__, value);
}
- (void)_setName:(NSString *)value {
    NSLog(@"%s - %@", __func__, value);
}

// 测试代码
    MCPerson *person = [[MCPerson alloc] init];
    [person setValue:@"person" forKey:@"name"];
  
    NSLog(@"%@ - %@ - %@ - %@", person->_name, person->_isName, person->name, person->isName);
    NSLog(@"%@ - %@ - %@", person->_isName, person->name, person->isName);
    NSLog(@"%@ - %@", person->name, person->isName);
    NSLog(@"%@", person->isName);

    1. 执行上面的代码, 查看打印信息如下:

可以看到程序执行了 setName 方法, 验证了上面步骤1的内容。而且我们发现三个方法中执行完第一个方法以后后面的就不会去执行了, 如果我们注释掉 setName 的话, 得到的结果是执行_setName , 由此就能得出方法的查找和执行优先顺序。

    1. 将上面的set方法注释掉, 然后 accessInstanceVariablesDirectly 方法的返回值改为 NO
      根据打印信息发现调用了 setValue: forUndefinedKey: 方法抛出了异常, 下面再把返回值改为 YES。
      可以发现我们的值被赋值给了 _name ,其余的三个实例变量仍然是空值, 然后试着把 _name注释掉就可以去一一验证实例变量的查找优先顺序了有兴趣的可以自己去验证一下。

3.2KVC的取值过程

  • 1.首先按照 get, , is, _ 的顺序查找方法, 如果找到方法, 执行找到的方法得到返回值, 返回值的判断跳到第5步; 如果没有查找到方法, 进行下一步

  • 2.如果没有找到上面的方法, KVC 就会去继续查找 countOf, objectInAtIndex: (对应NSArray的方法), AtIndexes: (对应NSArray 的 objectsAtIndexes: 方法) 格式的方法, 如果找到 countOf 和 另外两个方法中的一个, 就会返回一个可以响应所有NSArray方法的代理集合对象。当该代理集合对象接收到 NSArray 的方法调用时, 会去转换为对 countOf, objectInAtIndex 或 AtIndexes 这几个方法的调用 (此外还有一个可选方法格式为 get:range )。(注意: 该类为NSKeyValueArray , 是NSArray的子类)

  • 3.如果第2步仍然没有找到, 就会继续去查找 countOf, enumeratorOf, 和 memberOf: (对应NSSet的方法) 格式的方法, 如果这三种格式的方法都找到, 就会返回一个响应所有NSSet方法的代理集合对象, 反之则进行第4步。该集合对象会将接收到的NSSet方法调用转换为对 countOf, enumeratorOf, 和 memberOf: 方法的调用。

  • 4.如果没有找到任何符合要求的方法, 然后 accessInstanceVariablesDirectly 的返回值为YES, 会像上面的设值过程一样去查询实例变量 _, _, , , 如果查询到符合条件的实例变量, 会直接取出实例变量的值, 然后进行第5步。反之, 直接到第6步。

  • 5.如果第4步获取到的属性值是一个对象指针, 直接返回结果; 如果该值是 NSNumber 支持的标量类型, 将其存储为 NSNumber类型的实例然后返回; 如果该值不是 NSNumber 支持的标量类型, 将其转换为 NSValue对象然后返回。

  • 6.调用 valueForUndefinedKey: 方法进行报错。

下面是关于取值过程的代码验证:

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

@property (nonatomic,strong) NSMutableString *arrayM;
@property (nonatomic,strong) NSMutableSet *setM;
@property (nonatomic,strong) NSMutableOrderedSet *orderSetM;

//
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"取值出现异常!!!");
    return key;
}

//
- (NSString *)getName {
    return NSStringFromSelector(_cmd);
}
- (NSString *)name {
    return NSStringFromSelector(_cmd);
}
- (NSString *)isName {
    return NSStringFromSelector(_cmd);
}
- (NSString *)_name {
    return NSStringFromSelector(_cmd);
}

#pragma - NSArray -
- (NSUInteger)countOfPens {
    NSLog(@"- %s -", __func__);
    return [self.array count];
}
- (id)objectInPensAtIndex:(NSUInteger)index {
    NSLog(@"- %s -", __func__);
    return self.array[index];
}

#pragma - NSSet -
- (NSUInteger)countOfBooks {
    NSLog(@"- %s -", __func__);
    return [self.set count];
}
- (NSEnumerator *)enumeratorOfBooks {
    NSLog(@"- %s -", __func__);
    return [self.set objectEnumerator];
}
- (NSString *)memberOfBooks:(NSString *)object {
    NSLog(@"- %s -", __func__);
    return [self.set containsObject:object] ? object : nil;
}

// 验证代码
    person->_name = @"我是 _name";
    person->name = @"我是 name";
    person->isName = @"我是 isName";
    person->_isName = @"我是 _isName";
    
    NSLog(@"***** %@", [person valueForKey:@"name"]);
    
    person.array = @[@"pen0", @"pen1", @"pen2", @"pen3", @"pen4"];
    NSArray *array = [person valueForKey:@"pens"];
    NSLog(@"%@", [array objectAtIndex:1]);
    NSLog(@"数量 %ld", [array count]);
    NSLog(@"是否存在该值 %d", [array containsObject:@"pen2"]);

    person.set = [NSSet setWithArray:person.array];
    NSSet *set = [person valueForKey:@"books"];
    [set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop){
        NSLog(@"遍历set: %@", obj);
    }];
    NSLog(@"是否存在该值 %d", [set containsObject:@"pen2"]);


打印结果:

通过上面的代码可以去检索KVC在 valueForKey:@"name" 是的查询机制, 这里不再一一列出, 有兴趣的可以自己去一一验证。

4.KVC的自定义

4.1自定义设值

那么,我们如果要自定义 KVC 实现的话,也应该按照这种设计模式来操作。我们直接新建一个 NSObject 的分类,然后我们先着眼于 setValue:ForKey: 方法,为了避免与系统自带的 KVC 方法冲突,我们加一个前缀

// NSObject+JHKVC.h
@interface NSObject (JHKVC)
- (void)jh_setValue:(nullable id)value forKey:(NSString *)key;
@end

然后要实现这个方法,根据我们前面探索的 setValue:ForKey: 流程,我们判断一下传入的 key 是否为空:

    // 1.判断 key
    if (key == nil  || key.length == 0) return;

如果 key 为 nil 或者 key 长度为 0 ,直接退出。

接着我们要判断是否存在setKey,_setKey,这里有个小插曲,因为苹果官方文档上只说了这两种方法,但其实,iOS 底层还处理了 setIsKey,这是因为 key 可以被重写成 isKey 的形式,所以这里我们就再加上对 setIsKey 的判断。

    // 2.判断 setKey,_setKey,setIsKey 是否存在,如果存在,直接调用相应的方法来设置属性值
    NSString *Key = key.capitalizedString;
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self jh_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self jh_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self jh_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }

这里为了方便,先将 key 进行一下首字母大写化,然后拼接三个不同的 set 方法名,然后判断响应的方法能否实现,如果实现了就直接调用响应的方法来设置属性值

- (BOOL)jh_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;
}

这里如果按照系统的 KVC 设值流程,应该还有对 NSArray,NSSet 之类的处理,为了简化,就暂时忽略掉这些流程。我们直接往下面走,下一个流程应该就是判断类方法 accessInstanceVariablesDirectly 了:

    // 3.判断是否能直接读取成员变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"JHUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }

如果可以读取成员变量,那么就需要我们按照 _key,_isKey, key, isKey 的顺序去查找了:

    // 4.按照 _key,is_key,key,isKey 顺序查询实例变量
    NSMutableArray *mArray = [self getIvarListName];
    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;
    }

这里要先读取到当前对象上所有的实例变量,然后匹配四种情况

- (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 类型  
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    // 释放掉成员变量指针数组
    free(ivars);
    return mArray;
}

这里用到了 Runtime 的两个 api,class_copyIvarList 和 ivar_getName

Ivar  _Nonnull * class_copyIvarList(Class cls, unsigned int *outCount);

返回类结构中成员变量的指针数组,但是不包括父类中声明的成员变量。该数组包含 *outCount指针,后跟一个 NULL 终止符。使用完毕后您必须使用 free() 释放成员变量的指针数组。如果该类未声明任何实例变量,或者 cls 为Nil,则返回 NULL,并且 *outCount 为 0。

const char * ivar_getName(Ivar v);

返回成员变量的名称

    // 5.如果前面的流程都失败了,则抛出异常
    @throw [NSException exceptionWithName:@"JHUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: setValue:forUndefinedKey:%@.****",self,NSStringFromSelector(_cmd),key] userInfo:nil];

最后抛出 setValue:forUndefinedKey 的异常

至此,我们的 setValue:forKey: 流程就结束了,当然,整个内容和系统真正的 KVC 比起来还差得很远,包括线程安全、可变数组之类的都没涉及,不过这不是重点,我们只需要举一反三即可。

4.2 自定义取值

接着我们需要自定义的是 valueForKey:,我们声明如下的方法:

- (nullable id)jh_valueForKey:(NSString *)key;

然后同样的,根据我们前面探索的 valueForKey: 底层流程,还是要先判断 key:

    // 1.判断 key
    if (key == nil  || key.length == 0) {
        return nil;
    }

如果 key 为 nil 或者 key 长度为 0 ,直接退出。

然后就是判断是否有相应的 getter 方法,查找顺序是按照 getKey, key, isKey, _key:

    // 2.判断 getKey,key,isKey,_key 是否存在,如果存在,直接调用相应的方法来返回属性值
    NSString *Key = key.capitalizedString;
    NSString *getKey = [NSString stringWithFormat:@"get%@:",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@:",Key];
    NSString *_key = [NSString stringWithFormat:@"_%@:",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(isKey)]){
        return [self performSelector:NSSelectorFromString(isKey)];
    } else if ([self respondsToSelector:NSSelectorFromString(_key)]){
        return [self performSelector:NSSelectorFromString(_key)];
    }
#pragma clang diagnostic pop

如果这四种 getter 方法都没有找到,那么同样的就需要读取类方法:

    // 3.判断是否能直接读取成员变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"JHUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }

如果可以读取成员变量,那么就需要我们按照 _key,_isKey, key, isKey 的顺序去查找了:

    // 4.按照 _key,_iskey,key,isKey 顺序查询实例变量
    NSMutableArray *mArray = [self getIvarListName];
    _key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    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.抛出异常
    @throw [NSException exceptionWithName:@"JHUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: valueForUndefinedKey:%@.****",self,NSStringFromSelector(_cmd),key] userInfo:nil];

最后抛出 valueForUndefinedKey: 的异常

取值过程的自定义也结束了,其实这里也有不严谨的地方,比如取得属性值返回的时候需要根据属性值类型来判断是否要转换成 NSNumber 或 NSValue,以及对 NSArray 和 NSSet 类型的判断。

5.KVC的注意事项

5.1在KVC中使用 keyPath

在实际过程中, 一个类的成员变量有可能是自定义的类或者其他的复杂类型, 这时候如果想要使用KVC获取到自定义类的属性就会比较麻烦。这时KVC给我们提供了一个解决方案, 键路径 keyPath。方法如下:

- (nullable id)valueForKeyPath:(NSString *)keyPath;  //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值

// 
MCStudent *student = [[MCStudent alloc] init];
student.classNumber = @"num1";
student.number = @"007";
person.student = student;
NSLog(@"classNum: %@", [personvalueForKeyPath:@"student.classNumber"]);
// 打印结果
2020-02-16 13:27:08.482381+0800 002-KVC取值&赋值过程[96366:1435734] classNum: num1


上面展示keyPath的简单用法, 此时如果我们调用的方法是 valueForKey: 的话, 一般情况下系统会去调用undefinedKey方法, 因为没有找到这个属性及其相关的方法和实例变量。KVC在此方法中的搜索机制首先根据 " . " 来分割key, 然后在去按照上面的顺序去搜索下去。

5.2 KVC 的自动转换

我们进行设值或者取值的时候, 不是每次都是对象类型, 但是在 valueForKey: 和 setValue: forKey: 时总是需要或者返回一个id对象类型。看一下下面的代码:

typedef struct {
    float x, y, z;
} ThreeFloats;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, assign) int  age;
@property (nonatomic, assign) BOOL sex;
@property (nonatomic) ThreeFloats  threeFloats;
//
  [person setValue:@18 forKey:@"age"];
// 上面那个表达 大家应该都会! 但是下面这样操作可以?
  [person setValue:@"20" forKey:@"age"]; // int - string
  NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber
  [person setValue:@"20" forKey:@"sex"];
  NSLog(@"%@-%@",[person valueForKey:@"sex"],[[person valueForKey:@"sex"] class]);//__NSCFNumber

// 打印结果
2020-02-16 14:40:31.263662+0800 004-KVC异常小技巧[98873:1483589] 20-__NSCFNumber
2020-02-16 14:40:33.829650+0800 004-KVC异常小技巧[98873:1483589] 1-__NSCFBoolean

如果原本的变量类型是值类型或者布尔类型, 我们直接以字符串进行赋值以后, 得出的结果为 __NSCFNumber 和__NSCFBoolean 类型, 说明在我们进行setValue 和 getValue操作时, 系统帮我们进行了自动转换。那么当变量类型是结构体类型的时候呢, 此时我们是无法再去直接使用字符串赋值的, 需要我们将其转换为 NSValue类型:

    // 赋值操作
    ThreeFloats floats = {1., 2., 3.};
    NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSValue *reslut = [person valueForKey:@"threeFloats"];
    NSLog(@"%@",reslut);
    NSLog(@"%@-%@",[person valueForKey:@"threeFloats"],[[person valueForKey:@"threeFloats"] class]);//NSConcreteValue

    // 取值操作
    ThreeFloats th;
    [reslut getValue:&th] ;
    NSLog(@"%f - %f - %f",th.x,th.y,th.z);

    // 打印结果
    2020-02-16 15:05:00.494832+0800 004-KVC异常小技巧[99701:1501772] {length = 12, bytes = 0x0000803f0000004000004040}-NSConcreteValue

通过打印结果可以看出, 转换成 NSValue 类型以后赋值成功了, 然后如果我们需要使用 valueForKey 取值时, 同样取出的也是 NSValue 类型的值, 此时需要进行如上面样将其转换为结构体然后进行使用。所以当我们进行KVC设值和取值操作时, 因为我们传递进去和取出来的都是id类型的值, 有时候需要我们自己去保证类型的正确性。

5.3 KVC 异常处理

这一点需要结合上面的第2点进行说明, 我们知道KVC有时候会帮我们去自动转换我们所传的值, 但是当我们传 nil 的时候KVC是怎么处理的呢。下面看一段代码:

- (void)setNilValueForKey:(NSString *)key;

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"你傻不傻: 设置 %@ 是空值",key);
}
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, assign) int  age;
@property (nonatomic, assign) BOOL sex;
@property (nonatomic) ThreeFloats  threeFloats;

// 测试代码
  NSLog(@"******2: 设置空值******");
  [person setValue:nil forKey:@"age"]; 
  [person setValue:nil forKey:@"subject"];
  [person setValue:nil forKey:@"sex"];
  [person setValue:nil forKey:@"threeFloats"];
// 打印结果
2020-02-16 16:58:44.538117+0800 004-KVC异常小技巧[3703:1577245] ******2: 设置空值******
2020-02-16 16:58:44.538200+0800 004-KVC异常小技巧[3703:1577245] 你傻不傻: 设置 age 是空值
2020-02-16 16:58:44.538293+0800 004-KVC异常小技巧[3703:1577245] 你傻不傻: 设置 sex 是空值
2020-02-16 16:58:44.538487+0800 004-KVC异常小技巧[3703:1577245] 你傻不傻: 设置 threeFloats 是空值

可以看出给subject设置空值时, 不会走该方法, 其余三个进行 nil 赋值时都会调用该方法抛出异常。官方文档上面说明的是针对需要转换为 NSNumber 和 NSValue 类型的数据赋值 nil 时会抛出异常, 所以 当我们给 NSString 类型的subject进行赋值时并没有报错。

5.4 KVC 正确性验证

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

该方法默认返回YES, 如果类中实现了该方法, 那么就会去调用新实现的方法来返回。

- (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;
}

如上所示, 我们可以在该方法里面去验证某个key是否允许通过KVC去设定, 然后进行相关处理和返回。(注意: KVC是不会去主动验证的, 需要我们在该方法里面去实现验证。)