Objective-C 的 KVC(一):基本使用 && 底层原理

KVC 简介

  • 相关文档

    Key-Value Coding Programming Guide

    NSKeyValueCoding.h 代码注释

  • KVC 的概念

    KVC(Key-Value Coding)翻译成中文叫:键值编码,是由 NSObject 的非正式协议(即 NSObject 的分类)NSKeyValueCoding 启用的一种机制,用于间接地访问对象的属性与成员变量(即,通过字符串来访问对象的属性与成员变量)。遵守了 NSKeyValueCoding 非正式协议的对象会提供对其属性与成员变量的间接访问(即,继承自 NSObject 的对象都拥有 KVC 机制,都能调用 KVC 的相关方法)。KVC 的这种间接访问机制,补充了对象的属性与成员变量所提供的直接访问机制

    KVC 是 iOS 开发中的黑魔法之一,通过 KVC 可以在程序运行时动态地获取和设置对象的属性与成员变量,很多高级的 iOS 开发技巧都是基于 KVC 实现的。同时,KVC 也是许多其他 Cocoa 技术的基础,比如 KVO、Cocoa bindings、Core Data、AppleScript-ability 等等

  • KVC 的相关方法

    KVC 所有方法的默认实现都在 NSObject 的分类 NSKeyValueCoding 中,子类可以重写相关方法,提供自定义的实现

    // KVC 的相关方法都定义在该头文件下
    #import <Foundation/NSKeyValueCoding.h>
    
    
    #pragma mark - 获取属性或者成员变量的值
    // 获取方法调用者中给定 key 所标识的属性或者成员变量的值
    -(nullable id)valueForKey:(NSString *)key;
    // 获取方法调用者中给定 keyPath 所标识的属性或者成员变量的值
    -(nullable id)valueForKeyPath:(NSString *)keyPath;
    // 在通过 KVC 取值时,如果没有搜索到任何跟 key 或者 keyPath 有关的属性与成员变量,则会调用该方法。该方法默认会抛出 NSUnknownKeyException 异常
    // 开发者可以通过重写该方法,以更优雅的方式处理 KVC 在取值时 key 或者 keyPath 未搜索到的情况
    -(nullable id)valueForUndefinedKey:(NSString *)key;
    
    
    #pragma mark - 设置属性或者成员变量的值
    // 将方法调用者中给定 key 所标识的属性或者成员变量的值设置为给定的 value
    -(void)setValue:(nullable id)value forKey:(NSString *)key;
    // 将方法调用者中给定 keyPath 所标识的属性或者成员变量的值设置为给定的 value
    -(void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
    // 在通过 KVC 赋值时,如果没有搜索到任何跟 key 或者 keyPath 有关的属性与成员变量,则会调用该方法。该方法默认会抛出 NSUnknownKeyException 异常
    // 开发者可以通过重写该方法,以更优雅的方式处理 KVC 在赋值时 key 或者 keyPath 未搜索到的情况
    -(void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
    // 在通过 KVC 赋值时,如果向非对象指针类型的属性或者成员变量传 nil,则会调用该方法。该方法默认会抛出 NSInvalidArgumentException 异常
    // 开发者可以通过重写该方法,以更优雅的方式处理 KVC 在赋值时向非对象指针类型的属性或者成员变量传 nil 的情况
    -(void)setNilValueForKey:(NSString *)key;
    
    
    #pragma mark - KVC 访问权限控制
    // 用于标识:在通过 KVC 取值或者赋值时,如果没有搜索到相应的 getter 或者 setter,是否可以直接访问对象的成员变量。默认返回 YES
    +(BOOL)accessInstanceVariablesDirectly;
    
    
    #pragma mark - 进行字典与模型的相互转换
    // 用于字典转模型:输入一个字典,获取字典中的 key-value,并设置模型中该 key 对应的 value
    // 如果字典中 key 对应的 value 为 NSNull 对象,则会先将获取到的 NSNull 对象拆箱成 nil,然后再赋值给对应的 value
    -(void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
    // 用于模型转字典:输入一组 key,获取模型中该组 key 对应的 value,并将获取到的 key-value 封装成字典返回
    // 如果获取到的 value 是 nil,则会先将获取到的 nil 值装箱成 NSNull 对象,然后再添加到要返回的字典中
    -(NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    
    
    #pragma mark - 获取集合类型的属性或者成员变量
    // 获取方法调用者中给定 key 所标识的 NSMutableArray 类型的属性或者成员变量(返回的集合代理对象表现为一个 NSMutableArray 对象)
    -(NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
    // 获取方法调用者中给定 keyPath 所标识的 NSMutableArray 类型的属性或者成员变量(返回的集合代理对象表现为一个 NSMutableArray 对象)
    -(NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
    // 获取方法调用者中给定 key 所标识的 NSMutableOrderedSet 类型的属性或者成员变量(返回的集合代理对象表现为一个 NSMutableOrderedSet 对象)
    -(NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
    // 获取方法调用者中给定 keyPath 所标识的 NSMutableOrderedSet 类型的属性或者成员变量(返回的集合代理对象表现为一个 NSMutableOrderedSet 对象)
    -(NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath;
    // 获取方法调用者中给定 key 所标识的 NSMutableSet 类型的属性或者成员变量(返回的集合代理对象表现为一个 NSMutableSet 对象)
    -(NSMutableSet *)mutableSetValueForKey:(NSString *)key;
    // 获取方法调用者中给定 keyPath 所标识的 NSMutableSet 类型的属性或者成员变量(返回的集合代理对象表现为一个 NSMutableSet 对象)
    -(NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
    
    
    #pragma mark - 验证属性或者成员变量的值的合法性
    // 验证要设置给属性或者成员变的值的合法性
    -(BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
    // 验证要设置给属性或者成员变的值的合法性
    -(BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
    

KVC 的基本使用

Dog 类和 Person 类如下所示:

Dog
Person

  • 通过 key 获取和设置实例对象的属性或者成员变量

    -(void)kvcDemo {
        Person* aPerson = [[Person alloc] init];
        // 赋值
        [aPerson setValue:@"hcg" forKey:@"name"];
        // 取值
        NSString* aName = [aPerson valueForKey:@"name"];
        // 输出结果
        NSLog(@"aName = %@", aName);
        // aName = hcg
    }
    
  • 通过 keyPath 获取和设置实例对象的属性或者成员变量

    keyPath(键路径 or 路由 )用于支持多级访问,其用法跟点语法相同

    -(void)kvcDemo {
        Person* aPerson = [[Person alloc] init];
        Dog* aDog = [[Dog alloc] init];
        aPerson.petAnimal = aDog;
        // 赋值
        [aPerson setValue:@"tom" forKeyPath:@"petAnimal.nickname"];
        // 取值
        NSString* aNickname = [aPerson valueForKeyPath:@"petAnimal.nickname"];
        // 输出结果
        NSLog(@"petAnimal.nickname = %@", aNickname);
        // petAnimal.nickname = tom
    }
    

KVC 对(非对象指针类型的值)的处理

仔细观察 KVC 取值和赋值的接口方法,我们会发现值 value 都被定义为对象类型 id。那么如何通过 KVC 获取和设置 基本数据类型或者结构体类型 的属性与成员变量呢?答案是使用拆箱操作和装箱操作:

  1. 在使用 KVC 进行取值时,如果获取的是非对象类型的值,则 KVC 会使用该值初始化一个 NSNumber 对象(用于基本数据类型)或者 NSValue 对象(用于结构体类型),然后返回该对象。调用者需要调用拆箱操作,以提取对象里面存储的真实数值
  2. 在使用 KVC 进行赋值时,如果设置的是非对象类型的值,则调用者需要使用该值初始化一个 NSNumber 对象(用于基本数据类型)或者 NSValue 对象(用于结构体类型),然后传递该对象。KVC 内部会调用拆箱操作,以提取对象里面存储的真实数值
  • KVC 对(基本数据类型的值)的处理

    下表是 KVC 对于基本数据类型和 NSNumber 对象之间的转换:

    基本数据类型装箱操作拆箱操作
    BOOLnumberWithBool:boolValue (in iOS) / charValue (in macOS)
    charnumberWithChar:charValue
    doublenumberWithDouble:doubleValue
    floatnumberWithFloat:floatValue
    intnumberWithInt:intValue
    longnumberWithLong:longValue
    long longnumberWithLongLong:longLongValue
    shortnumberWithShort:shortValue
    unsigned charnumberWithUnsignedChar:unsignedChar
    unsigned intnumberWithUnsignedInt:unsignedInt
    unsigned longnumberWithUnsignedLong:unsignedLong
    unsigned long longnumberWithUnsignedLongLong:unsignedLongLong
    unsigned shortnumberWithUnsignedShort:unsignedShort

    代码示例:

    Person

    -(void)kvcDemo {
        Person* aPerson = [[Person alloc] init];
        // 赋值
        NSNumber* num0 = [NSNumber numberWithInt:20];
        [aPerson setValue:num0 forKey:@"age"];
        // 取值
        NSNumber* num1 = [aPerson valueForKey:@"age"];
        int anAge = [num1 intValue];
        // 输出结果
        NSLog(@"anAge = %d", anAge);
        // anAge = 20
    }
    
  • KVC 对(结构体类型的值)的处理

    下表是 KVC 对于结构体类型和 NSValue 对象之间的转换:

    基本数据类型装箱操作拆箱操作
    CGPointvalueWithCGPoint:CGPointValue
    CGRectvalueWithCGRect:CGRectValue
    CGSizevalueWithCGSize:CGSizeValue
    NSRangevalueWithRange:rangeValue

    除了以上 CGPointCGRectCGSizeNSRange 类型的结构体可以和 NSValue 对象之间进行相互转换,开发者自定义的结构体也可以装箱成 NSValue 对象,示例如下:

    Person

    -(void)kvcDemo {
        Person* aPerson = [[Person alloc] init];
        // 赋值
        ThreeFloats threeDimensional0 = {100.0f, 100.0f, 100.0f};
        NSValue* value0 = [NSValue valueWithBytes:&threeDimensional0 objCType:@encode(ThreeFloats)];
        [aPerson setValue:value0 forKey:@"threeDimensional"];
        // 取值
        NSValue* value1 = [aPerson valueForKey:@"threeDimensional"];
        ThreeFloats threeDimensional1;
        [value1 getValue:&threeDimensional1];
        // 输出结果
        NSLog(@"threeDimensional1 = {%f, %f, %f}", threeDimensional1.x, threeDimensional1.y, threeDimensional1.z);
        // threeDimensional1 = {100.000000, 100.000000, 100.000000}
    }
    
  • 注意

    ① 因为 Swift 中的所有属性都是对象,所以这里的拆箱操作和装箱操作仅适用于 Objective-C 属性

    ② 当使用 KVC 进行赋值时(setValue:forKey:setValue:forKeyPath:),如果 key 对应的属性或者成员变量的数据类型不是对象指针类型,则 value 就禁止传 nil。否则会调用异常处理方法 setNilValueForKey:,该方法的默认实现为抛出异常 NSInvalidArgumentException,并导致程序 Crash

KVC 的搜索模式

  • 基本的 Getter 搜索模式

    valueForKey: 用于获取方法调用者中给定 key 所标识的属性或者成员变量的值,其默认实现会在方法调用者所属的类中执行以下操作:

    1. 按照 get<Key><key>is<Key>、(_get<Key>_<key>)的顺序在方法调用者所属的类中查找 getter 方法
      如果找到相应的 getter 方法,则调用之
      如果 getter 方法的返回值类型是对象指针类型,则直接返回结果
      如果 getter 方法的返回值类型是 NSNumber 支持的标量类型之一,则将返回值装箱成 NSNumber 类型的对象并返回
      如果 getter 方法的返回值类型是 NSValue 支持的结构体类型之一,则将返回值装箱成 NSValue 类型的对象并返回(在 MacOS 10.5 中:任意类型的结构体都将转换为 NSValues,而不仅仅是 NSPointNRangeNSRectNSSize

    2. (在 MacOS 10.7 中引入)(没有找到简单的访问器方法)
      在方法调用者所属的类中查找
      -countOf<Key>
      -indexIn<Key>OfObject:(对应于 -[NSOrderedSet indexOfObject:]
      -objectIn<Key>AtIndex:(对应于 -[NSOrderedSet objectAtIndex:]
      -<key>AtIndexes:(对应于 -[NSOrderedSet objectsAtIndexes:]
      如果找到一个 count 方法和一个 indexOf 方法,以及另外两个可能的方法中的至少一个,则返回响应所有 NSOrderedSet 方法的集合代理对象(集合代理对象 NSKeyValueOrderedSetNSOrderedSet 的子类)
      发送到集合代理对象的每个 NSOrdereredSet 消息将会被转换成方法调用者所属的类中以下方法的某些组合
      -countOf<Key>-indexIn<Key>OfObject:-objectIn<Key>AtIndex:-<key>AtIndexes:
      如果在方法调用者所属的类中还实现了一个名称为 -get<Key>:range: 的可选方法,则该可选方法将在适当的时候被调用以获得最佳性能

    3. (如果没有找到简单的访问器方法,没有找到 NSOrderedSet 的相关访问方法)
      在方法调用者所属的类中查找
      -countOf<Key>
      -objectIn<Key>AtIndex:(对应于 -[NSArray objectAtIndex:]
      -<key>AtIndexes:(对应于 -[NSArray objectsAtIndexes:])(在 MacOS 10.4 中引入)
      如果找到一个 count 方法和另外两个可能方法中的一个,则返回响应所有 NSArray 方法的集合代理对象(集合代理对象 NSKeyValueArrayNSArray 的子类)
      发送到集合代理对象的每个 NSArray 消息将会被转换成方法调用者所属的类中以下方法的某些组合:
      -countOf<Key>-objectIn<Key>AtIndex:-<key>AtIndexes:
      如果在方法调用者所属的类中还实现了一个名称为 -get<Key>:range: 的可选方法,则该可选方法将在适当的时候被调用以获得最佳性能

    4. (在 MacOS 10.4 中引入)(没有找到简单的访问器方法,没有找到 NSOrderedSet 的相关访问方法,没有找到 NSArray 的相关访问方法)
      在方法调用者所属的类中查找
      -countOf<Key>
      -enumeratorOf<Key>
      -memberOf<Key>:(对应于 -[NSSet member]
      如果找到所有这三个方法,则将返回响应所有 NSSet 方法的集合代理对象(集合代理对象 NSKeyValueSetNSSet 的子类)
      发送到集合代理对象的每个 NSSet 消息将会被转换成方法调用者所属的类中以下方法的某些组合:
      -countOf<Key>-enumeratorOf<Key>-memberOf<Key>:

    5. (没有找到简单的访问器方法,没有找到 NSOrderedSet 的相关访问方法,没有找到 NSArray 的相关访问方法,没有找到 NSSet 的相关访问方法)
      如果方法调用者的类属性 +accessInstanceVariablesDirectly 返回 YES
      则按照 _<key>_is<Key><key>is<Key> 的顺序在方法调用者中查找成员变量
      如果找到这样的成员变量,则返回方法调用者中该成员变量的值,并且与步骤 1 一样,查看是否需要转换成 NSNumberNSValue 类型的对象

    6. (没有找到简单的访问器方法,没有找到 NSOrderedSet 的相关访问方法,没有找到 NSArray 的相关访问方法,没有找到 NSSet 的相关访问方法,没有找到相关的成员变量)
      调用 -valueForUndefinedKey: 并返回调用结果。-valueForUndefinedKey: 的默认实现是抛出异常 NSUnknownKeyException,并导致程序 Crash。开发者可以重写该方法根据特定的 key 做一些特殊处理

    代码举例如下:

    // Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @end
    
    // Person.m
    #import "Person.h"
    
    @interface Person ()
    {
    @private NSString* _name;
    @private NSString* _isName;
    @private NSString* name;
    @private NSString* isName;
    }
    @end
    
    @implementation Person
    
    #pragma mark - ① 查找 getter 方法
    -(NSString *)getName {
        return @"getter method: getName";
    }
    
    -(NSString *)name {
        return @"getter method: name";
    }
    
    -(NSString *)isName {
        return @"getter method: isName";
    }
    
    -(NSString *)_getName {
        return @"getter method: _getName";
    }
    
    -(NSString *)_name {
        return @"getter method: _name";
    }
    
    #pragma mark - ② 查找 NSOrderedSet 的相关方法
    -(NSInteger)countOfName {
        return 5;
    }
    
    -(NSUInteger)indexInNameOfObject:(id)object {
        return 3;
    }
    
    -(id)objectInNameAtIndex:(NSUInteger)idx {
        return @"tom";
    }
    
    -(NSArray<id> *)nameAtIndexes:(NSIndexSet *)indexes {
        NSMutableArray* mArr = [NSMutableArray array];
        for (int i = 0; i < indexes.count; i++) {
            [mArr addObject:@"jack"];
        }
        return mArr;
    }
    
    #pragma mark - ③ 查找 NSArray 的相关方法
    -(NSInteger)countOfName {
        return 4;
    }
    
    -(id)objectInNameAtIndex:(NSUInteger)idx {
        return @"kang";
    }
    
    -(NSArray<id> *)nameAtIndexes:(NSIndexSet *)indexes {
        NSMutableArray* mArr = [NSMutableArray array];
        for (int i = 0; i < indexes.count; i++) {
            [mArr addObject:@"jack"];
        }
        return mArr;
    }
    
    #pragma mark - ④ 查找 NSSet 的相关方法
    -(NSInteger)countOfName {
        return 3;
    }
    
    -(NSEnumerator *)enumeratorOfName {
        NSSet* set = [NSSet setWithObjects:@"1", @"2", @"3", nil];
        NSEnumerator* enumerator = [set objectEnumerator];
        return enumerator;
    }
    
    -(nullable id)memberOfName:(id)object {
        return @"michael";
    }
    
    #pragma mark - ⑤ 查找成员变量
    +(BOOL)accessInstanceVariablesDirectly {
        return YES;
    }
    
    -(instancetype)init {
        if (self = [super init]) {
            _name = @"variable: _name";
            _isName = @"variable: _isName";
            name = @"variable: name";
            isName = @"variable: isName";
        }
        return self;
    }
    
    #pragma mark - ⑥ 处理 key 对应的属性或者成员变量查找不到的情况
    -(id)valueForUndefinedKey:(NSString *)key {
        NSLog(@"method name = %s, key = %@", __func__, key);
        return @"hcg";
    }
    
    @end
    
    // 调用示例
    -(void)kvcDemo {
        Person* aPerson = [[Person alloc] init];
        id value = [aPerson valueForKey:@"name"];
        Class valueCls = [value class];
        Class valueSuperCls = [value superclass];
        NSLog(@"value.class = %@", valueCls);
        NSLog(@"value.superClass = %@", valueSuperCls);
        NSLog(@"value = %@", value);
        // 如果通过 KVC 获取到的是 NSOrderedSet 的集合代理对象 NSKeyValueOrderedSet
        if ([NSStringFromClass(valueCls) isEqualToString:@"NSKeyValueOrderedSet"]) {
            NSOrderedSet* orderedSet = (NSOrderedSet *)value;
            NSLog(@"orderedSet[0] = %@", orderedSet[0]);
            
            NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:0];
            NSLog(@"orderedSet.objectsAtIndexes = %@", [orderedSet objectsAtIndexes:indexSet]);
            
            NSInteger index = [orderedSet indexOfObject:@"unknow"];
            NSLog(@"indexOfObject.unknow = %ld", index);
        }
        // 如果通过 KVC 获取到的是 NSArray 的集合代理对象 NSKeyValueArray
        if ([NSStringFromClass(valueCls) isEqualToString:@"NSKeyValueArray"]) {
            NSArray* array = (NSArray *)value;
            NSLog(@"array[0] = %@", array[0]);
            
            NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:0];
            NSLog(@"array.objectsAtIndexes = %@", [array objectsAtIndexes:indexSet]);
            
            NSInteger index = [array indexOfObject:@"unknow"];
            NSLog(@"indexOfObject.unknow = %ld", index);
        }
        // 如果通过 KVC 获取到的是 NSSet 的集合代理对象 NSKeyValueSet
        if ([NSStringFromClass(valueCls) isEqualToString:@"NSKeyValueSet"]) {
            NSSet* set = (NSSet *)value;
            [set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
                        NSLog(@"obj = %@", obj);
            }];
        }
    }
    
  • 基本的 Setter 搜索模式

    setValue:forKey: 用于将方法调用者中给定 key 所标识的属性或者成员变量的值设置为给定的 value,其默认实现会在方法调用者所属的类中执行以下操作:

    1. 按照 set<Key>:_set<Key>:setIs<Key>)的顺序在方法调用者所属的类中查找 setter 方法
      如果找到相应的 setter 方法,则检查该 setter 方法的参数类型
      如果该 setter 方法的参数类型不是对象指针类型,但是给定的 value 却为 nil,则调用方法调用者的 -setNilValueForKey:-setNilValueForKey: 的默认实现是抛出异常 NSInvalidArgumentException,并导致程序 Crash。开发者可以重写该方法根据特定的 key-value 做一些特殊处理
      如果该 setter 方法的参数类型是对象指针类型,则用给定的 value 作为参数简单地调用该 setter 方法
      如果该 setter 方法的参数类型是基础数据类型或者结构体类型,则在调用该 setter 方法之前先对给定的 value 执行 NSNumber/NSValue 的拆箱操作

    2. (没有找到简单的访问器方法)
      如果方法调用者的类属性 +accessInstanceVariablesDirectly 返回 YES
      则按照 _<key>_is<Key><key>is<Key> 的顺序在方法调用者中查找成员变量
      如果找到了这样的成员变量,并且该成员变量是对象指针类型,则首先 release 该成员变量,其次 retain 给定的 value,最后将给定的 value 赋值给该成员变量
      如果找到了这样的成员变量,并且该成员变量是基本数据类型或者结构体类型,则首先对给定的 value 执行与步骤 1 相同的 NSNumber/NSValue 的拆箱操作,然后再把从给定的 value 中提取到的数据赋值给该成员变量

    3. (没有找到简单的访问器方法,没有找到相关的成员变量)
      调用 -setValue:forUndefinedKey:-setValue:forUndefinedKey: 的默认实现是抛出异常 NSUndefinedKeyException,并导致程序 Crash
      开发者可以重写该方法根据特定的 key-value 做一些特殊处理

    代码举例如下:

    // Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @end
    
    // Person.m
    #import "Person.h"
    
    @interface Person ()
    {
    @private NSString* _name;
    @private NSString* _isName;
    @private NSString* name;
    @private NSString* isName;
    }
    @end
    
    @implementation Person
    
    #pragma mark - ① 查找 setter 方法
    -(void)setName:(NSString *)name {
        NSLog(@"%s, name = %@", __func__, name);
    }
    
    -(void)_setName:(NSString *)name {
        NSLog(@"%s, name = %@", __func__, name);
    }
    
    -(void)setIsName:(NSString *)name {
        NSLog(@"%s, name = %@", __func__, name);
    }
    
    #pragma mark - ② 查找成员变量
    +(BOOL)accessInstanceVariablesDirectly {
        return YES;
    }
    
    -(void)printInstanceVariable {
        NSLog(@"_name = %@", _name);
        NSLog(@"_isName = %@", _isName);
        NSLog(@"name = %@", name);
        NSLog(@"isName = %@", isName);
        return nil;
    }
    
    #pragma mark - ③ 处理 key 对应的属性或者成员变量查找不到的情况
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key {
        NSLog(@"method name = %s, key = %@, value = %@", __func__, key, value);
    }
    
    @end
    
    // 调用示例
    -(void)kvcDemo {
        Person* aPerson = [[Person alloc] init];
        [aPerson setValue:@"hcg" forKey:@"name"];
        [aPerson printInstanceVariable];
    }
    
  • NSMutableArray 的搜索模式

    mutableArrayValueForKey: 用于获取方法调用者中给定 key 所标识的 NSMutableArray 类型的属性或者成员变量,此方法的默认实现可以识别与 -valueForKey: 相同的简单访问器方法和数组访问器方法,并与 -valueForKey: 遵守相同的直接访问成员变量的策略。但是此方法总是会返回一个可变的集合代理对象,而不是像 -valueForKey: 那样返回不可变的集合对象。此方法还会在方法调用者所属的类中执行以下操作:

    1. 在方法调用者所属的类中查找
      -insertObject:in<Key>AtIndex:(对应于 -[NSMutableArray insertObject:atIndex:]
      -removeObjectFrom<Key>AtIndex:(对应于 -[NSMutableArray removeObjectAtIndex:]
      -insert<Key>:atIndexes:(在 MacOS 10.4 中引入)(对应于 -[NSMutableArray insertObjects:atIndexes:]
      -remove<Key>AtIndexes:(在 MacOS 10.4 中引入)(对应于 -[NSMutableArray removeObjectsAtIndexes:]
      如果至少找到一个 insert 方法和至少找到一个 remove 方法,则返回能响应所有 NSMutableArray 方法的集合代理对象(集合代理对象为 NSMutableArray 的子类),发送到集合代理对象的每个 NSMutableArray 消息都将会产生对方法调用者所属的类中以下方法组合的调用
      -insertObject:in<Key>AtIndex:-removeObjectFrom<Key>AtIndex:
      -insert<Key>:atIndexes:-remove<Key>AtIndexes:
      如果在方法调用者所属的类中还实现了一个名称为 -replaceObjectIn<Key>AtIndex:withObject: 或者 -replace<Key>AtIndexes:with<Key>:(在 MacOS 10.4 中引入)的可选方法,则该可选方法将在适当的时候被调用以获得最佳性能

    2. (没有找到与 NSMutableArray 相关的方法)
      在方法调用者所属的类中查找 setter 方法 -set<Key>:,如果找到了此 setter 方法,则发送到集合代理对象的每个 NSMutableArray 消息都将会产生对方法调用者所属的类中以下方法的调用 -set<Key>:。也就是说,集合代理对象被修改之后,会调用 -set<Key>: 重新赋值回去

    3. (没有找到与 NSMutableArray 相关的方法,没有找到简单的 setter 方法)
      如果方法调用者的类属性 +accessInstanceVariablesDirectly 返回 YES
      则按照 _<key><key> 的顺序在方法调用者所属的类中查找成员变量
      如果找到了相应的成员变量,则发送到集合代理对象的每个 NSMutableArray 消息将转发给相应的成员变量
      因此,该成员变量的类型通常必须是 NSMutableArray 或者 NSMutableArray 的子类

    4. (没有找到与 NSMutableArray 相关的方法,没有找到简单的 setter 方法,没有找到相关的成员变量)
      仍将返回一个可变的集合代理对象。发送到集合代理对象的每个 NSMutableArray 消息将会产生对方法调用者所属的类中以下方法的调用 -setValue:forUndefinedKey:
      -setValue:forUndefinedKey: 的默认实现是抛出异常 NSUnknownKeyException,并导致程序 Crash。开发者可以重写该方法根据特定的 key-value 做一些特殊处理

    代码举例如下:

    // Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @end
    
    // Person.m
    #import "Person.h"
    
    @interface Person ()
    {
    @private NSMutableArray* tempArr;
    @private NSMutableArray* _nameArr;
    @private NSMutableArray* nameArr;
    }
    @end
    
    @implementation Person
    
    #pragma mark - ① 查找 getter 方法
    // 代码同 基本的 Getter 搜索模式
    #pragma mark - ② 查找 NSOrderedSet 的相关方法
    // 代码同 基本的 Getter 搜索模式
    #pragma mark - ③ 查找 NSArray 的相关方法
    // 代码同 基本的 Getter 搜索模式
    #pragma mark - ④ 查找 NSSet 的相关方法
    // 代码同 基本的 Getter 搜索模式
    #pragma mark - ⑤ 查找成员变量
    // 代码同 基本的 Getter 搜索模式
    
    #pragma mark - 提供一个 getter 方法返回可变数组
    -(NSMutableArray *)nameArr {
        if (!tempArr) {
            tempArr = [NSMutableArray array];
        }
        return tempArr;
    }
    
    #pragma mark - ① 查找 NSMutableArray 相关的方法
    -(void)insertObject:(id)anObject inNameArrAtIndex:(NSUInteger)index {
        [self.nameArr insertObject:anObject atIndex:index];
    }
    
    -(void)removeObjectFromNameArrAtIndex:(NSUInteger)index {
        [self.nameArr removeObjectAtIndex:index];
    }
    
    -(void)insertNameArr:(NSArray<id> *)objects atIndexes:(NSIndexSet *)indexes {
        [self.nameArr insertObjects:objects atIndexes:indexes];
    }
    
    -(void)removeNameArrAtIndexes:(NSIndexSet *)indexes {
        [self.nameArr removeObjectsAtIndexes:indexes];
    }
    
    #pragma mark - ② 查找 setter 方法
    -(void)setNameArr:(NSMutableArray *)aNameArr {
        tempArr = aNameArr;
    }
    
    #pragma mark - ③ 查找成员变量
    +(BOOL)accessInstanceVariablesDirectly {
        return YES;
    }
    
    -(instancetype)init {
        if (self = [super init]) {
            _nameArr = [NSMutableArray array];
            [_nameArr addObject:@"First"];
            
            nameArr = [NSMutableArray array];
            [nameArr addObject:@"Second"];
        }
        return self;
    }
    
    #pragma mark - ④ 处理 key 对应的属性或者成员变量查找不到的情况
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key {
        NSLog(@"method name = %s, key = %@, value = %@", __func__, key, value);
    }
    
    @end
    
    // 调用示例
    -(void)kvcDemo {
        Person* aPerson = [[Person alloc] init];
        NSMutableArray* mArr = [aPerson mutableArrayValueForKey:@"nameArr"];
        Class mArrCls = [mArr class];
        Class mArrSuperCls = [mArr superclass];
        NSLog(@"mArr.class = %@", mArrCls);
        NSLog(@"mArr.superClass = %@", mArrSuperCls);
        NSLog(@"mArr = %@", mArr);
        
        [mArr addObject:@"hcg"];
        NSLog(@"-[mArr addObject:@\"hcg\"], mArr = %@", mArr);
        
        [mArr removeObject:@"hcg"];
        NSLog(@"-[mArr removeObject:@\"hcg\"], mArr = %@", mArr);
        
        NSArray* objects = @[@"tom", @"jack", @"kang"];
        NSIndexSet* indexes = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, objects.count)];
        [mArr insertObjects:objects atIndexes:indexes];
        NSLog(@"-[mArr insertObjects:objects atIndexes:indexes], mArr = %@", mArr);
        
        [mArr removeObjectsAtIndexes:indexes];
        NSLog(@"-[mArr removeObjectsAtIndexes:indexes], mArr = %@", mArr);
    }
    
  • 其他的搜索模式

    除了以上三种搜索模式,KVC 还有 NSMutableSetNSMutableOrderedSet 两种搜索模式

    NSMutableSetNSMutableOrderedSet 的搜索模式和 NSMutableArray 的搜索模式相同,只是查找和调用的方法有所不同。具体可以查看 KVC 的官方文档 Accessor Search Patterns

通过 KVC 进行字典与模型的相互转换

  • 通过 KVC 进行字典转模型

    // Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property (nonatomic, strong) NSString* name;   // 姓名
    @property (nonatomic, strong) NSString* uid;    // 学号
    @property (nonatomic, assign) int age;          // 年龄
    @property (nonatomic, assign) int score;        // 成绩
    @property (nonatomic, strong) NSString* addr;   // 住址
    
    @end
    
    // Person.m
    #import "Person.h"
    
    @implementation Person
    
    // 重写以下方法:处理在通过 KVC 赋值时未搜索到任何跟 key 有关的属性或者成员变量的情况
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key {
        NSLog(@"%s, key = %@, value = %@", __func__, key, value);
        if ([key isEqualToString:@"id"]) {
            self.uid = value;
            return;
        }
        [super setValue:value forUndefinedKey:key];
    }
    
    // 重写以下方法:处理在通过 KVC 赋值时对基本数据类型或者结构体类型的变量赋值 nil 的情况
    -(void)setNilValueForKey:(NSString *)key {
        NSLog(@"%s, key = %@", __func__, key);
        if ([key isEqualToString:@"score"]) {
            self.score = -1;
            return;
        }
        [super setNilValueForKey:key];
    }
    
    -(NSString *)description {
        NSString* desc = [NSString stringWithFormat:@"name = %@, id = %@, age = %d, score = %d, addr = %@",
                          self.name, self.uid, self.age, self.score, self.addr];
        return desc;
    }
    
    @end
    
    // 调用示例
    -(void)kvcDemo {
        // 创建 JSON 字典
        NSDictionary* jsonDict = @{
            @"name" : @"hcg",
            @"id" : @"12345678",
            @"age" : @20,
            @"score" : [NSNull null],
            @"addr" : @"XiaMen"
        };
        // 创建 Person 对象
        Person* aPerson = [[Person alloc] init];
        NSLog(@"before : %@", aPerson);
        // 通过 KVC 进行字典转模型
        [aPerson setValuesForKeysWithDictionary:jsonDict];
        NSLog(@"after : %@", aPerson);
    }
    
    // 输出结果
    // before : name = (null), id = (null), age = 0, score = 0, addr = (null)
    // -[Person setValue:forUndefinedKey:], key = id, value = 12345678
    // -[Person setNilValueForKey:], key = score
    // after : name = hcg, id = 12345678, age = 20, score = -1, addr = XiaMen
    
  • 通过 KVC 进行模型转字典

    // Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property (nonatomic, strong) NSString* name;   // 姓名
    @property (nonatomic, strong) NSString* uid;    // 学号
    @property (nonatomic, assign) int age;          // 年龄
    @property (nonatomic, assign) int score;        // 成绩
    @property (nonatomic, strong) NSString* addr;   // 住址
    
    @end
    
    // Person.m
    #import "Person.h"
    
    @implementation Person
    
    // 重写以下方法:处理在通过 KVC 取值时未搜索到任何跟 key 有关的属性或者成员变量的情况
    -(id)valueForUndefinedKey:(NSString *)key {
        NSLog(@"%s, key = %@", __func__, key);
        if ([key isEqualToString:@"id"]) {
            return self.uid;
        }
        if ([key isEqualToString:@"height"]) {
            return [NSNumber numberWithFloat:170.0f];
        }
        return [super valueForUndefinedKey:key];
    }
    
    -(NSString *)description {
        NSString* desc = [NSString stringWithFormat:@"name = %@, id = %@, age = %d, score = %d, addr = %@",
                          self.name, self.uid, self.age, self.score, self.addr];
        return desc;
    }
    
    @end
    
    // 调用示例
    -(void)kvcDemo {
        // 创建 Person 对象
        Person* aPerson = [[Person alloc] init];
        aPerson.name = @"hcg";
        aPerson.uid = @"12345678";
        aPerson.age = 20;
        aPerson.score = 100;
        aPerson.addr = nil;
        NSLog(@"before : %@", aPerson);
        // 创建 key 数组
        NSArray<NSString *>* keys = @[@"name", @"id", @"age", @"score", @"addr", @"height"];
        // 通过 KVC 进行模型转字典
        NSDictionary<NSString*, id>* jsonDict = [aPerson dictionaryWithValuesForKeys:keys];
        NSLog(@"jsonDict = %@", jsonDict);
    }
    
    // 输出结果
    // before : name = hcg, id = 12345678, age = 20, score = 100, addr = (null)
    // -[Person valueForUndefinedKey:], key = id
    // -[Person valueForUndefinedKey:], key = height
    // jsonDict = {
    //     name = hcg;
    //     id = 12345678;
    //     age = 20;
    //     score = 100;
    //     addr = "<null>";
    //     height = 170;
    // }
    

KVC 与集合类型

  • 通过 KVC 获取集合类型的属性,并实现对不可变数组的修改
    Person

    // 调用示例
    -(void)kvcDemo {
        // 创建 Person 对象
        Person* aPerson = [[Person alloc] init];
        aPerson.hobbies = @[@"singing", @"dancing", @"drawing"];
        
        NSLog(@"hobbies.class = %@", [aPerson.hobbies class]);
        // hobbies.class = __NSArrayI
        
        // 通过 KVC 对不可变数组进行修改
        NSMutableArray* mArr = [aPerson mutableArrayValueForKey:@"hobbies"];
        mArr[0] = @"coding";
        mArr[1] = @"gaming";
        
        NSLog(@"hobbies.class = %@", [aPerson.hobbies class]);
        // hobbies.class = __NSArrayM
        
        NSLog(@"aPerson.hobbies = %@", aPerson.hobbies);
        // aPerson.hobbies = (
        //      coding,
        //      gaming,
        //      drawing
        // )
    }
    
  • 通过 KVC 在集合类型的对象中实现高阶的消息传递

    通过 Foundation/NSKeyValueCoding.h 中方法的声明和注释可知:NSArray(NSKeyValueCoding) 提供了有别于 NSObject(NSKeyValueCoding) 的通过 KVC 取值和赋值的方法实现

    NSObject 及其子类中,通过 KVC 取值和赋值的方法直接作用于当前的实例对象
    NSArray 及其子类中,通过 KVC 取值和赋值的方法不作用于数组对象本身,而是作用于数组对象所包含的每个元素
    Person

    // 调用示例
    -(void)kvcDemo {
        // 创建 Person 数组
        Person* person0 = [Person personWithName:@"hcg" age:20 addr:@"XiaMen"];
        Person* person1 = [Person personWithName:@"hzp" age:21 addr:@"ZhangZhou"];
        Person* person2 = [Person personWithName:@"lxy" age:22 addr:@"QuanZhou"];
        NSMutableArray* mArr = [NSMutableArray arrayWithObjects:person0, person1, person2, nil];
        
        // 通过 KVC 取值
        NSArray<NSNumber *>* ages = [mArr valueForKey:@"age"];
        NSLog(@"ages = %@", ages);
        // ages = (
        //     20,
        //     21,
        //     22
        // )
        
        // 通过 KVC 赋值
        [mArr setValue:@"FuJian" forKey:@"addr"];
        for (int i = 0; i < mArr.count; i++) {
            NSLog(@"person%d : %@", i, mArr[i]);
        }
        // person0 : name = hcg, age = 20, addr = FuJian
        // person1 : name = hzp, age = 21, addr = FuJian
        // person2 : name = lxy, age = 22, addr = FuJian
    }
    

KVC 的集合运算符

KVC 的 -valueForKeyPath: 方法不仅可以获取方法调用者中给定 keyPath 所标识的属性或者成员变量的值,还可以在参数 keyPath 中嵌套集合运算符,用来对集合对象中的元素进行操作

下图是 keyPath 中使用集合运算符时的格式:
keyPath
由上图可知,keyPath 中使用集合运算符时,主要分为 3 个部分:

  1. 左键路径(Left key path),用于标识要操作的集合对象。如果方法的调用者就是集合对象,则可以省略左键路径(Left key path)
  2. 集合运算符(Collection operator),用于标识要进行的集合操作
  3. 右键路径(Right key path),用于标识要进行操作的集合元素的属性

-valueForKeyPath: 方法中可以使用的集合运算符主要分为以下 3 类:

  1. 聚合运算符(@avg@count@sum@max@min
  2. 数组运算符(@unionOfObjects@distinctUnionOfObjects
  3. 嵌套运算符(@unionOfArrays@distinctUnionOfArrays@distinctUnionOfSets

为了更好地讲解各个集合运算符的功能与用法,先定义如下 2 个类:
Book
Student
接下来我们开始讲解各个集合运算符的功能与用法:

  • ① 聚合运算符

    以指定的方式聚合(集合中每个元素的右键路径所指定的属性或者成员变量的值)
    聚合运算符的操作结果通常是一个 NSNumber 对象。即,聚合运算符的返回值通常是一个 NSNumber 对象

    -valueForKeyPath: 方法中可以使用的聚合运算符有:

    1. @avg,读取集合中每个元素的右键路径所指定的属性或者成员变量的值,然后将其转换为 double 类型的数据 (nil0 替代),并计算这些数据的算术平均值,然后将计算结果装箱成 NSNumber 类型的对象返回
    2. @count,计算集合中元素的个数,然后将计算结果装箱成 NSNumber 类型的对象返回
    3. @sum,读取集合中每个元素的右键路径所指定的属性或者成员变量的值,然后将其转换为 double 类型的数据 (nil0 替代),并计算这些数据的总和,然后将计算结果装箱成 NSNumber 类型的对象返回
    4. @max,读取集合中每个元素的右键路径所指定的属性或者成员变量的值,然后将其转换为 double 类型的数据 (忽略属性或者成员变量为 nil 的集合元素),并计算这些数据的最大值,然后将计算结果装箱成 NSNumber 类型的对象返回
    5. @min,读取集合中每个元素的右键路径所指定的属性或者成员变量的值,然后将其转换为 double 类型的数据 (忽略属性或者成员变量为 nil 的集合元素),并计算这些数据的最小值,然后将计算结果装箱成 NSNumber 类型的对象返回

    注意:

    1. @count 比较特别,它不需要写右键路径,即使写了右键路径也会被忽略
    2. @max@min 是根据右键路径指定的属性在集合中进行搜索的,搜索时使用右键路径指定的属性中的 compare: 方法比较集合元素的大小,许多基础类(如 NSNumber 类)中都有 compare: 方法的定义。因此,在使用 @max@min 时,右键路径指定的属性必须能响应 compare: 方法。可以通过重写集合元素的 compare: 方法对搜索过程进行控制

    代码示例:

    // 调用示例
    -(void)kvcDemo {
        // 创建 Book 数组
        Book* book0 = [Book bookWithName:@"Chinese" price:10.0f];
        Book* book1 = [Book bookWithName:@"English" price:20.0f];
        Book* book2 = [Book bookWithName:@"Math" price:30.0f];
        Book* book3 = [Book bookWithName:@"Technology" price:40.0f];
        NSMutableArray* books = [NSMutableArray arrayWithObjects:book0, book1, book2, book3, nil];
        // 创建 Student 对象
        Student* aStudent = [Student studentWithName:@"jack" age:18 books:books];
        
        // 1.计算课本的平均价格
        NSNumber* avgOfPrice0 = [aStudent.books valueForKeyPath:@"@avg.price"];
        NSNumber* avgOfPrice1 = [aStudent valueForKeyPath:@"books.@avg.price"];
        NSLog(@"avgOfPrice0 = %@, avgOfPrice1 = %@", avgOfPrice0, avgOfPrice1);
        // avgOfPrice0 = 25, avgOfPrice1 = 25
        
        // 2.计算课本的总数量
        NSNumber* countOfBooks0 = [aStudent.books valueForKeyPath:@"@count"];
        NSNumber* countOfBooks1 = [aStudent valueForKeyPath:@"books.@count"];
        NSLog(@"countOfBooks0 = %@, countOfBooks1 = %@", countOfBooks0, countOfBooks1);
        // countOfBooks0 = 4, countOfBooks1 = 4
        
        // 3.计算课本的总价格
        NSNumber* sumOfPrice0 = [aStudent.books valueForKeyPath:@"@sum.price"];
        NSNumber* sumOfPrice1 = [aStudent valueForKeyPath:@"books.@sum.price"];
        NSLog(@"sumOfPrice0 = %@, sumOfPrice1 = %@", sumOfPrice0, sumOfPrice1);
        // sumOfPrice0 = 100, sumOfPrice1 = 100
        
        // 4.计算课本价格的最大值
        NSNumber* maxOfPrice0 = [aStudent.books valueForKeyPath:@"@max.price"];
        NSNumber* maxOfPrice1 = [aStudent valueForKeyPath:@"books.@max.price"];
        NSLog(@"maxOfPrice0 = %@, maxOfPrice1 = %@", maxOfPrice0, maxOfPrice1);
        // maxOfPrice0 = 40, maxOfPrice1 = 40
        
        // 5.计算课本价格的最小值
        NSNumber* minOfPrice0 = [aStudent.books valueForKeyPath:@"@min.price"];
        NSNumber* minOfPrice1 = [aStudent valueForKeyPath:@"books.@min.price"];
        NSLog(@"minOfPrice0 = %@, minOfPrice1 = %@", minOfPrice0, minOfPrice1);
        // minOfPrice0 = 10, minOfPrice1 = 10
    }
    
  • ② 数组运算符

    根据运算符所指定的条件遍历集合中每个元素的右键路径所指定的属性或者成员变量,将所有符合条件的集合元素的右键路径所指定的属性或者成员变量组成一个 NSArray 对象并返回

    -valueForKeyPath: 方法中可以使用的数组运算符有:

    1. @unionOfObjects,读取集合中每个元素的右键路径所指定的属性或者成员变量的值,组成一个 NSArray 对象并返回
    2. @distinctUnionOfObjects,读取集合中每个元素的右键路径所指定的属性或者成员变量的值,成一个 NSArray 对象,将 NSArray 对象去重后返回

    注意:

    1. 在使用数组运算符时,因为数组中不能添加 nil,所以会忽略属性或者成员变量为 nil 的集合元素

    代码示例:

    // 调用示例
    -(void)kvcDemo {
        // 创建 Book 数组
        Book* book0 = [Book bookWithName:@"Chinese" price:10.0f];
        Book* book1 = [Book bookWithName:@"English" price:20.0f];
        Book* book2 = [Book bookWithName:@"Math" price:30.0f];
        Book* book3 = [Book bookWithName:@"Technology" price:40.0f];
        Book* book4 = [Book bookWithName:@"Technology" price:40.0f];
        NSMutableArray* books = [NSMutableArray arrayWithObjects:book0, book1, book2, book3, book4, nil];
        // 创建 Student 对象
        Student* aStudent = [Student studentWithName:@"jack" age:18 books:books];
        
        // 1.获取所有的书名 - 不去重
        NSArray<NSString *>* bookNames0 = [aStudent.books valueForKeyPath:@"@unionOfObjects.name"];
        // NSArray<NSString *>* bookNames1 = [aStudent valueForKeyPath:@"books.@unionOfObjects.name"];
        NSLog(@"bookNames0 = %@", bookNames0);
        // bookNames0 = (
        //     Chinese,
        //     English,
        //     Math,
        //     Technology,
        //     Technology
        // )
        
        // 2.获取所有书名 - 去重
        NSArray<NSString *>* bookNamesDistinct0 = [aStudent.books valueForKeyPath:@"@distinctUnionOfObjects.name"];
        // NSArray<NSString *>* bookNamesDistinct1 = [aStudent valueForKeyPath:@"books.@distinctUnionOfObjects.name"];
        NSLog(@"bookNamesDistinct0 = %@", bookNamesDistinct0);
        // bookNamesDistinct0 = (
        //     English,
        //     Chinese,
        //     Math,
        //     Technology
        // )
    }
    
  • ③ 嵌套运算符

    处理集合对象中嵌套其他集合对象的情况,根据运算符所指定的条件遍历所有子集合中每个元素的右键路径所指定的属性或者成员变量,将所有符合条件的子集合元素的右键路径所指定的属性或者成员变量组成一个 NSArray 或者 NSSet 对象并返回

    -valueForKeyPath: 方法中可以使用的嵌套运算符有:

    1. @unionOfArrays,读取所有子集合中每个元素的右键路径所指定的属性或者成员变量的值,组成一个 NSArray 对象并返回(子集合的类型必需是 NSArray 或者 NSMutableArray
    2. @distinctUnionOfArrays,读取所有子集合中每个元素的右键路径所指定的属性或者成员变量的值,组成一个 NSArray 对象,将 NSArray 对象去重后返回(子集合的类型必需是 NSArray 或者 NSMutableArray
    3. @distinctUnionOfSets,读取所有子集合中每个元素的右键路径所指定的属性或者成员变量的值,组成一个 NSSet 对象,将 NSSet 对象去重后返回(子集合的类型必需是 NSSet 或者 NSMutableSet

    注意:

    1. 在使用嵌套运算符时,-valueForKeyPath: 方法内部首先会根据运算符创建一个 NSMutableArray 或者 NSMutableSet 对象,然后将子集合中的元素添加到 NSMutableArray 或者 NSMutableSet 对象中,最后再进行操作。如果集合中含有非集合元素(即集合中含有非子集合的元素),则 -valueForKeyPath: 方法将会引发异常
    2. 在使用嵌套运算符时,因为数组中不能添加 nil,所以会忽略属性或者成员变量为 nil 的子集合的元素

    代码示例:

    // 调用示例
    -(void)kvcDemo {
        // 创建 Book 数组
        Book* book0 = [Book bookWithName:@"Chinese" price:10.0f];
        Book* book1 = [Book bookWithName:@"English" price:20.0f];
        Book* book2 = [Book bookWithName:@"Technology" price:30.0f];
        NSMutableArray* books0 = [NSMutableArray arrayWithObjects:book0, book1, book2, nil];
        Book* book3 = [Book bookWithName:@"Chinese" price:10.0f];
        Book* book4 = [Book bookWithName:@"English" price:20.0f];
        Book* book5 = [Book bookWithName:@"Math" price:30.0f];
        NSMutableArray* books1 = [NSMutableArray arrayWithObjects:book3, book4, book5, nil];
        // 创建 Book 数组列表
        NSMutableArray* booksList0 = [NSMutableArray arrayWithObjects:books0, books1, nil];
        
        // 1.获取所有的书名 - 返回 NSArray,不去重
        NSArray<NSString *>* bookNamesArr = [booksList0 valueForKeyPath:@"@unionOfArrays.name"];
        NSLog(@"bookNamesArr = %@", bookNamesArr);
        // bookNamesArr = (
        //     Chinese,
        //     English,
        //     Technology,
        //     Chinese,
        //     English,
        //     Math
        // )
        
        // 2.获取所有的书名 - 返回 NSArray,去重
        NSArray<NSString *>* bookNamesDistinctArr = [booksList0 valueForKeyPath:@"@distinctUnionOfArrays.name"];
        NSLog(@"bookNamesDistinctArr = %@", bookNamesDistinctArr);
        // bookNamesDistinctArr = (
        //     English,
        //     Chinese,
        //     Technology,
        //     Math
        // )
        
        // 3.获取所有的书名 - 返回 NSSet,去重
        NSMutableSet* booksSet0 = [NSMutableSet setWithObjects:book0, book1, book2, nil];
        NSMutableSet* booksSet1 = [NSMutableSet setWithObjects:book3, book4, book5, nil];
        NSMutableArray* booksList1 = [NSMutableArray arrayWithObjects:booksSet0, booksSet1, nil];
        NSSet<NSString *>* bookNamesDistinctSet = [booksList1 valueForKeyPath:@"@distinctUnionOfSets.name"];
        NSLog(@"bookNamesDistinctSet = %@", bookNamesDistinctSet);
        // bookNamesDistinctSet = (
        //     English,
        //     Chinese,
        //     Technology,
        //     Math
        // )
    }
    
  • 拓展:自定义集合运算符

    上面介绍了 KVC 为我们提供的集合运算符,那么我们能不能自定义集合运算符呢?

    我们使用 RunTime 打印出 NSArray 的方法列表

    -(void)printNSArrayMethodList {
        unsigned int count = 0;
        Method* methodList = class_copyMethodList([NSArray class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Method method = methodList[i];
            NSString* methodName = NSStringFromSelector(method_getName(method));
            NSString* methodTypes = @(method_getTypeEncoding(method));
            NSLog(@"%03d, method name = %@, method types = %@", i, methodName, methodTypes);
        }
        free(methodList);
    }
    
    // 可以看到 NSArray 中的方法有很多,我们搜索关键字 avg、count、sum 等
    // 发现 KVC 为我们提供的集合运算符都有对应的 _<operatorKey>ForKeyPath: 方法
    // ...
    // 299, method name = _sumForKeyPath:,						method types = @24@0:8@16
    // 300, method name = _unionOfArraysForKeyPath:,			method types = @24@0:8@16
    // 301, method name = _unionOfObjectsForKeyPath:, 			method types = @24@0:8@16
    // 302, method name = _avgForKeyPath:, 						method types = @24@0:8@16
    // 303, method name = _countForKeyPath:, 					method types = @24@0:8@16
    // 304, method name = _maxForKeyPath:, 						method types = @24@0:8@16
    // 305, method name = _minForKeyPath:, 						method types = @24@0:8@16
    // 306, method name = _unionOfSetsForKeyPath:, 				method types = @24@0:8@16
    // 307, method name = _distinctUnionOfArraysForKeyPath:, 	method types = @24@0:8@16
    // 308, method name = _distinctUnionOfObjectsForKeyPath:, 	method types = @24@0:8@16
    // 309, method name = _distinctUnionOfSetsForKeyPath:, 		method types = @24@0:8@16
    // ...
    

    我们尝试为 NSArray 添加一个分类,并定义一个 _medianForKeyPath: 方法,用来获取数组中每个元素的右键路径所指定的属性或者成员变量的值的中位数

    // NSArray+CollectionOperator.h
    #import <Foundation/Foundation.h>
    
    @interface NSArray (CollectionOperator)
    
    @end
    
    // NSArray+CollectionOperator.m
    #import "NSArray+CollectionOperator.h"
    
    @implementation NSArray (CollectionOperator)
    
    // 在当前情况下 keyPath 的值为 @"price"
    -(NSNumber *)_medianForKeyPath:(NSString *)keyPath {
        // 使用数组元素默认的 compare: 方法对当前数组进行排序
        NSArray* sortedArray = [self sortedArrayUsingSelector:@selector(compare:)];
        // 计算数组元素的中位数
        double median = 0.0;
        if (0 == sortedArray.count) {}
        else if (1 == sortedArray.count) {
            median = [[sortedArray[0] valueForKey:keyPath] doubleValue];
        }
        else if (0 == sortedArray.count % 2) {
            unsigned long indexBefore = sortedArray.count / 2 - 1;
            unsigned long indexAfter = sortedArray.count / 2;
            double valueBefore = [[sortedArray[indexBefore] valueForKey:keyPath] doubleValue];
            double valueAfter = [[sortedArray[indexAfter] valueForKey:keyPath] doubleValue];
            median = (valueBefore + valueAfter) / 2;
        }
        else {
            unsigned long index = (sortedArray.count - 1) / 2;
            median = [[sortedArray[index] valueForKey:keyPath] doubleValue];
        }
        return [NSNumber numberWithDouble:median];
    }
     
    @end
    

    测试代码和测试结果如下:

    // 调用示例
    -(void)kvcDemo {
        // 创建 Book 数组
        Book* book0 = [Book bookWithName:@"Chinese" price:10.0f];
        Book* book1 = [Book bookWithName:@"English" price:20.0f];
        Book* book2 = [Book bookWithName:@"Math" price:30.0f];
        Book* book3 = [Book bookWithName:@"Technology" price:40.0f];
        NSMutableArray* books = [NSMutableArray arrayWithObjects:book0, book1, book2, book3, nil];
        // 创建 Student 对象
        Student* aStudent = [Student studentWithName:@"jack" age:18 books:books];
        // 计算课本价格的中位数
        NSNumber* medianOfPrice0 = [aStudent.books valueForKeyPath:@"@median.price"];
        NSNumber* medianOfPrice1 = [aStudent valueForKeyPath:@"books.@median.price"];
        NSLog(@"medianOfPrice0 = %@, medianOfPrice1 = %@", medianOfPrice0, medianOfPrice1);
        // medianOfPrice0 = 25, medianOfPrice1 = 25
    }
    

KVC 对异常的处理

  • 处理 NSUnknownKeyException 异常

    根据 KVC 的搜索规则,当没有搜索到跟 key 或者 keyPath 相关的属性与成员变量时,会在方法调用者中调用对应的异常处理方法
    -valueForUndefinedKey: 或者 -setValue:forUndefinedKey:
    这两个异常处理方法的默认实现都是抛出异常 NSUnknownKeyException,并导致程序 Crash
    我们可以通过重写这两个方法来处理异常:

    // 重写以下方法:处理在通过 KVC 取值时未搜索到任何跟 key 有关的属性或者成员变量的情况
    -(id)valueForUndefinedKey:(NSString *)key {
        NSLog(@"%s, key = %@", __func__, key);
        if ([key isEqualToString:@"id"]) {
            return self.uid;
        }
        if ([key isEqualToString:@"height"]) {
            return [NSNumber numberWithFloat:170.0f];
        }
        return [super valueForUndefinedKey:key];
    }
    
    // 重写以下方法:处理在通过 KVC 赋值时未搜索到任何跟 key 有关的属性或者成员变量的情况
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key {
        NSLog(@"%s, key = %@, value = %@", __func__, key, value);
        if ([key isEqualToString:@"id"]) {
            self.uid = value;
            return;
        }
        [super setValue:value forUndefinedKey:key];
    }
    
  • 处理 NSInvalidArgumentException 异常

    当使用 KVC 进行赋值时(setValue:forKey:setValue:forKeyPath:),如果 key 对应的属性或者成员变量的数据类型不是对象指针类型,则 value 就禁止传 nil。否则会在方法调用者中调用对应的异常处理方法 -setNilValueForKey:,该方法的默认实现为抛出异常 NSInvalidArgumentException,并导致程序 Crash。我们可以通过重写这个方法来处理异常:

    // 重写以下方法:处理在通过 KVC 赋值时对基本数据类型或者结构体类型的变量赋值 nil 的情况
    -(void)setNilValueForKey:(NSString *)key {
        NSLog(@"%s, key = %@", __func__, key);
        if ([key isEqualToString:@"score"]) {
            self.score = -1;
            return;
        }
        [super setNilValueForKey:key];
    }
    

KVC 的其他细节

  • 通过 KVC 验证属性或者成员变量的值的合法性

    /* 
    验证要设置给属性或者成员变的值的合法性
    @param.ioValue 指向要设置给属性或者成员变的值的指针,当此方法调用完成之后,会得到一个适合于后续 -setValue:forKey: 使用的值
    @param.inKey 用于标识要验证的属性或者成员变量的 key
    @param.outError 一个指向 NSError 类型的对象的指针
    @return 如果不需要进行验证,则直接返回 YES。注意:不要修改 *ioValue 和 *outError
    		如果需要进行验证并且可以执行验证,则将(作为原始值的已验证版本的对象)赋值给 *ioValue,然后返回 YES。注意:不要修改 *outError
    		如果需要进行验证但又无法执行验证,则将无法执行验证的原因封装在一个 NSError 对象中并赋值给 *outError,然后返回 NO。注意:不要修改 *ioValue
    @note 此方法的调用者不会负责释放 ioValue 和 outError
    @note 此方法的默认实现会在方法调用者所属的类中搜索名称为 -validate<Key>:error: 的验证方法
    	  如果找到相应的验证方法,则调用该验证方法并返回该验证方法的执行结果
    	  如果没有找到相应的验证方法,则返回 YES
    @note 此方法需要开发者手动调用,KVC 在进行赋值时不会主动调动此方法验证属性值的合法性
    	  即使开发者在方法调用者所属的类中重写了此方法,但是因为 KVC 不会主动调用此方法验证属性值的合法性,所以即使是非法值也还是能赋值成功
    */
    -(BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
    

    代码示例:

    // Person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property (nonatomic, strong) NSString* name;       // 姓名
    @property (nonatomic, assign) int age;              // 年龄
    @property (nonatomic, strong) NSString* country;    // 国籍
    
    @end
    
    // Person.m
    #import "Person.h"
    
    @implementation Person
    
    //-(BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue
    //              forKey:(NSString *)inKey
    //               error:(out NSError *__autoreleasing  _Nullable *)outError {
    //    return [super validateValue:ioValue forKey:inKey error:outError];
    //}
    
    //-(BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue
    //          forKeyPath:(NSString *)inKeyPath
    //               error:(out NSError *__autoreleasing  _Nullable *)outError {
    //    return [super validateValue:ioValue forKeyPath:inKeyPath error:outError];
    //}
    
    // 验证要设置给属性 country 的值的合法性
    -(BOOL)validateCountry:(inout id  _Nullable __autoreleasing *)ioCountry
                     error:(out NSError *__autoreleasing  _Nullable *)outError {
        NSString* country = *ioCountry;
        country = [country lowercaseString];
        if ([country isEqualToString:@"america"]) {
            *ioCountry = @"China";
            if (outError) {
                NSErrorDomain errorDomain = @"HCGErrorDomain";
                NSInteger code = 0;
                NSDictionary<NSErrorUserInfoKey, id>* userInfo = @{
                    @"kInvalidCountry" : @"America",
                    @"kSuggestedCountry" : @"China"
                };
                *outError = [NSError errorWithDomain:errorDomain code:code userInfo:userInfo];
            }
            return NO;
        }
        return YES;
    }
    
    // 验证要设置给属性 age 的值的合法性
    -(BOOL)validateAge:(inout id  _Nullable __autoreleasing *)ioAge
                     error:(out NSError *__autoreleasing  _Nullable *)outError {
        NSNumber* age = *ioAge;
        if ([age integerValue] < 0) {
            *ioAge = @(0);
            if (outError) {
                NSErrorDomain errorDomain = @"HCGErrorDomain";
                NSInteger code = 0;
                NSDictionary<NSErrorUserInfoKey, id>* userInfo = @{
                    @"kInvalidAge" : @"Less than 0",
                    @"kSuggestedAge" : @"Greater than or equal to 0"
                };
                *outError = [NSError errorWithDomain:errorDomain code:code userInfo:userInfo];
            }
            return NO;
        }
        return YES;
    }
    
    @end
    
    // 调用示例
    -(void)kvcDemo {
        Person* aPerson = [[Person alloc] init];
        // 验证要设置给属性 country 的值的合法性
        NSString* countryValue = @"America";
        NSString* countryKey = @"country";
        NSError* countryError = nil;
        bool countryResult = [aPerson validateValue:&countryValue forKey:countryKey error:&countryError];
        if (!countryResult) {
            NSLog(@"countryValue = %@", countryValue);
            NSLog(@"%@", countryError);
            // countryValue = China
            // Error Domain=HCGErrorDomain Code=0 "(null)" UserInfo={kSuggestedCountry=China, kInvalidCountry=America}
        }
        // 验证要设置给属性 age 的值的合法性
        NSNumber* ageValue = @(-10);
        NSString* ageKey = @"age";
        NSError* ageError = nil;
        bool ageResult = [aPerson validateValue:&ageValue forKey:ageKey error:&ageError];
        if (!ageResult) {
            NSLog(@"ageValue = %@", ageValue);
            NSLog(@"%@", ageError);
            // ageValue = 0
            // Error Domain=HCGErrorDomain Code=0 "(null)" UserInfo={kSuggestedAge=Greater than or equal to 0, kInvalidAge=Less than 0}
        }
    }
    
  • 通过 KVC 实现:KVO 监听集合元素的改变

    因为 KVO 的实现原理是在运行时动态生成新的子类并重写目标属性的 setter 方法来达到可以监听目标属性的改变和通知所有观察者对象的目的,所以只有对目标属性直接进行赋值才会触发 KVO
    如果目标属性是集合对象,则对集合对象内部元素进行操作(比如:添加元素、删除元素)是不会触发 KVO 的
    当需要使用 KVO 监听集合对象内部元素的变化时,可以通过 KVC 的可变代理方法来获取集合代理对象,然后再对集合代理对象进行操作。当集合代理对象的内部元素发生改变时,会触发 KVO 的监听方法

    // 返回 NSMutableArray 对象的集合代理对象,返回的集合代理对象表现为一个 NSMutableArray 对象
    -(NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
    -(NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
    
    // 返回 NSMutableOrderedSet 对象的集合代理对象,返回的集合代理对象表现为一个 NSMutableOrderedSet 对象
    -(NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
    -(NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath;
    
    // 返回 NSMutableSet 对象的集合代理对象,返回的集合代理对象表现为一个 NSMutableSet 对象
    -(NSMutableSet *)mutableSetValueForKey:(NSString *)key;
    -(NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
    
  • 苹果官方文档中对于属性的分类

    1. Attributes:简单属性,比如:基本数据类型、不可修改的实例对象(NSString*NSNumber*
    2. To-one relationships:一对一的关系,比如:可以修改的实例对象(Person*Address*
    3. To-many relationships:一对多的关系,比如:集合类型的实例对象(NSArray<Person *>*

    苹果官方文档中对于属性的分类

  • 关于 KVC 的其他重要细节

    1. KVC 可以访问(取值 + 赋值)readonly 的属性
    2. KVC 可以访问(取值 + 赋值)私有的属性与成员变量
    3. 通过 KVC 修改成员变量也会触发 KVO
    4. KVC 的所有默认实现都在 NSObject 的分类 NSKeyValueCoding
      即继承自 NSObject 的子类的 KVC 实现,默认都由 NSObject 提供
      子类可以重写 KVC 的相关方法,提供自定义的实现

自定义 KVC

  • 简述

    根据苹果官方文档描述的 KVC 搜索模式,通过给 NSObject 添加分类 HCGKVC,实现自定义 KVC 取值和赋值的方法

    因为,实现自定义 KVC 的主要目的是为了加深对系统 KVC 搜索模式和取值赋值过程的理解
    所以,下面的代码只是抓住了 KVC 搜索模式的主要规则和取值赋值的主要流程,并没有对系统 KVC 搜索模式和取值赋值的各方面进行详细的考虑

    下面代码至少还存在以下问题:

    1. 没有进行足够的防御性编程(断言?临界条件?)
    2. 没有考虑多线程环境下的并发操作
    3. 没有进行性能优化
    4. 只实现了对象指针类型的属性和成员变量的取值赋值,没有实现基本数据类型、结构体类型的属性和成员变量的取值赋值
    5. hcg_valueForKey 方法的搜索过程不完善,没有对 NSOrderedSetNSSet 相关的方法进行查找
    6. hcg_setValue 方法没有处理对基本数据类型、结构体类型的属性和成员变量赋值 nil 的情况
  • 源码

    NSObject+HCGKVC.h 代码如下:

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface NSObject (HCGKVC)
    
    // 将方法调用者中给定 key 所标识的属性或者成员变量的值设置为给定的 value
    -(void)hcg_setValue:(nullable id)aValue forKey:(NSString *)aKey;
    
    // 获取方法调用者中给定 key 所标识的属性或者成员变量的值
    -(nullable id)hcg_valueForKey:(NSString *)aKey;
    
    // 在通过 HCGKVC 赋值时,如果没有搜索到任何跟 key 有关的属性与成员变量,则会调用该方法。该方法默认会抛出 HCGUnknownKeyException 异常
    // 开发者可以通过重写该方法,以更优雅的方式处理 HCGKVC 在赋值时 key 未搜索到的情况
    -(void)hcg_setValue:(nullable id)value forUndefinedKey:(NSString *)key;
    
    // 在通过 HCGKVC 取值时,如果没有搜索到任何跟 key 有关的属性与成员变量,则会调用该方法。该方法默认会抛出 HCGUnknownKeyException 异常
    // 开发者可以通过重写该方法,以更优雅的方式处理 HCGKVC 在取值时 key 未搜索到的情况
    -(nullable id)hcg_valueForUndefinedKey:(NSString *)key;
    
    // 在通过 HCGKVC 赋值时,如果向非对象指针类型的属性或者成员变量传 nil,则会调用该方法。该方法默认会抛出 HCGInvalidArgumentException 异常
    // 开发者可以通过重写该方法,以更优雅的方式处理 HCGKVC 在赋值时向非对象指针类型的属性或者成员变量传 nil 的情况
    -(void)hcg_setNilValueForKey:(NSString *)key;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    NSObject+HCGKVC.m 代码如下:

    #import "NSObject+HCGKVC.h"
    #import <objc/runtime.h>
    #import <objc/message.h>
    
    @implementation NSObject (HCGKVC)
    
    #pragma mark - 注意
    
    // 需要关闭对 objc_msgSend 函数参数的严格检查:
    // Target - Build Settings - Apple Clang - Preprocessing - Enable strict checking of objc_msgSend Calls - NO
    
    #pragma mark - 赋值
    
    -(void)hcg_setValue:(id)aValue forKey:(NSString *)aKey {
        // key 不能为空
        if (nil == aKey || 0 == aKey.length) return;
        
        NSString* capitalKey = aKey.capitalizedString;  // key 首字母大写
        NSString* lowerKey = aKey;                      // key 首字母小写
        
        // 1.查找 setter 方法进行赋值,顺序是  set<Key>:  _set<Key>:  setIs<Key>:
        NSString* setKey = [NSString stringWithFormat:@"set%@:", capitalKey];
        NSString* _setKey = [NSString stringWithFormat:@"_set%@:", capitalKey];
        NSString* setIsKey = [NSString stringWithFormat:@"setIs%@:", capitalKey];
        // 在确定不会发生内存泄露的情况下,可以使用如下编译器指令来忽略内存泄露的警告(PerformSelector may cause a leak because its selector is unknown)
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        if ([self respondsToSelector:NSSelectorFromString(setKey)]) {
            [self performSelector:NSSelectorFromString(setKey) withObject:aValue];
            return;
        } else if ([self respondsToSelector:NSSelectorFromString(_setKey)]) {
            [self performSelector:NSSelectorFromString(_setKey) withObject:aValue];
            return;
        } else if ([self respondsToSelector:NSSelectorFromString(setIsKey)]) {
            [self performSelector:NSSelectorFromString(setIsKey) withObject:aValue];
            return;
        }
    #pragma clang diagnostic pup
        
        // 2.判断是否可以直接访问当前对象的成员变量
        if (![self.class accessInstanceVariablesDirectly]) {
            [self hcg_setValue:aValue forUndefinedKey:aKey];
        }
        
        // 3.查找相关成员变量进行赋值,顺序是 _<key>、_is<Key>、key、isKey
        NSArray<NSString *>* ivarNames = [self hcg_getIvarNameList];
        NSString* _key = [NSString stringWithFormat:@"_%@", lowerKey];
        NSString* _isKey = [NSString stringWithFormat:@"_is%@", capitalKey];
        NSString* key = [NSString stringWithFormat:@"%@", lowerKey];
        NSString* isKey = [NSString stringWithFormat:@"is%@", capitalKey];
        if ([ivarNames containsObject:_key]) {
            Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
            object_setIvar(self, ivar, aValue);
            return;
        } else if ([ivarNames containsObject:_isKey]) {
            Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
            object_setIvar(self, ivar, aValue);
            return;
        } else if ([ivarNames containsObject:key]) {
            Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
            object_setIvar(self, ivar, aValue);
            return;
        } else if ([ivarNames containsObject:isKey]) {
            Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
            object_setIvar(self, ivar, aValue);
            return;
        }
        
        // 4.如果找不到相应的成员变量,则抛出异常
        [self hcg_setValue:aValue forUndefinedKey:aKey];
    }
    
    #pragma mark - 取值
    
    -(id)hcg_valueForKey:(NSString *)aKey {
        // key 不能为空
        if (nil == aKey || 0 == aKey.length) return nil;
        
        NSString* capitalKey = aKey.capitalizedString;  // key 首字母大写
        NSString* lowerKey = aKey;                      // key 首字母小写
        
        // 1.查找 getter 方法进行取值,顺序是 get<Key>、<key>、is<Key>、_get<Key>、_<key>
        {
            NSString* getKey = [NSString stringWithFormat:@"get%@", capitalKey];
            NSString* key = [NSString stringWithFormat:@"%@", lowerKey];
            NSString* isKey = [NSString stringWithFormat:@"is%@", capitalKey];
            NSString* _getKey = [NSString stringWithFormat:@"_get%@", capitalKey];
            NSString* _key = [NSString stringWithFormat:@"_%@", lowerKey];
            // 在确定不会发生内存泄露的情况下,可以使用如下编译器指令来忽略内存泄露的警告(PerformSelector may cause a leak because its selector is unknown)
        #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(_getKey)]) {
                return [self performSelector:NSSelectorFromString(_getKey)];
            } else if ([self respondsToSelector:NSSelectorFromString(_key)]) {
                return [self performSelector:NSSelectorFromString(_key)];
            }
        #pragma clang diagnostic pup
        }
        
        // 2.查找 NSArray 的相关访问方法  countOf<Key>  objectIn<Key>AtIndex:  <key>AtIndexes:
        // -(NSUInteger)hcg_countOf<Key>;
        // -(id)hcg_objectIn<Key>AtIndex:(NSUInteger)idx;
        // -(NSArray<id> *)hcg_<key>AtIndexes:(NSIndexSet *)indexes;
        NSString* countOfKey = [NSString stringWithFormat:@"hcg_countOf%@", capitalKey];
        NSString* objectInKeyAtIndex = [NSString stringWithFormat:@"hcg_objectIn%@AtIndex:", capitalKey];
        NSString* keyAtIndexes = [NSString stringWithFormat:@"hcg_%@AtIndexes:", lowerKey];
        bool isRespondCountOfKey = [self respondsToSelector:NSSelectorFromString(countOfKey)];
        bool isRespondObjectInKeyAtIndex = [self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)];
        bool isRespondKeyAtIndexes =  [self respondsToSelector:NSSelectorFromString(keyAtIndexes)];
        if (isRespondCountOfKey && (isRespondObjectInKeyAtIndex || isRespondKeyAtIndexes)) {
            NSUInteger count = (NSUInteger)objc_msgSend(self, NSSelectorFromString(countOfKey));
            NSMutableArray* mArr = [NSMutableArray arrayWithCapacity:count];
            if (isRespondObjectInKeyAtIndex) {
                for (int i = 0; i < count; i++) {
                    id obj = objc_msgSend(self, NSSelectorFromString(objectInKeyAtIndex), i);
                    [mArr addObject:obj];
                }
            } else if (isRespondKeyAtIndexes) {
                NSIndexSet* indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, count)];
                NSArray* arr = [self performSelector:NSSelectorFromString(keyAtIndexes) withObject:indexSet];
                [mArr addObjectsFromArray:arr];
            }
            return mArr;
        }
        
        // 3.判断是否可以直接访问当前对象的成员变量
        if (![self.class accessInstanceVariablesDirectly]) {
            [self hcg_valueForUndefinedKey:aKey];
        }
        
        // 4.查找相关成员变量进行取值,顺序是 _<key>、_is<Key>、key、isKey
        {
            NSArray<NSString *>* ivarNames = [self hcg_getIvarNameList];
            NSString* _key = [NSString stringWithFormat:@"_%@", lowerKey];
            NSString* _isKey = [NSString stringWithFormat:@"_is%@", capitalKey];
            NSString* key = [NSString stringWithFormat:@"%@", lowerKey];
            NSString* isKey = [NSString stringWithFormat:@"is%@", capitalKey];
            if ([ivarNames containsObject:_key]) {
                Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
                return object_getIvar(self, ivar);
            } else if ([ivarNames containsObject:_isKey]) {
                Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
                return object_getIvar(self, ivar);
            } else if ([ivarNames containsObject:key]) {
                Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
                return object_getIvar(self, ivar);
            } else if ([ivarNames containsObject:isKey]) {
                Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
                return object_getIvar(self, ivar);
            }
        }
        
        // 5.如果找不到相应的成员变量,则抛出异常
        return [self hcg_valueForUndefinedKey:aKey];
    }
    
    #pragma mark - 异常处理方法
    
    -(void)hcg_setValue:(nullable id)value forUndefinedKey:(NSString *)key {
        NSString* exceptionName = @"HCGUnknownKeyException";
        NSString* exceptionReason = [NSString stringWithFormat:@"****[%@ hcg_setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key %@.****", self, key];
        @throw [NSException exceptionWithName:exceptionName reason:exceptionReason userInfo:nil];
    }
    
    -(nullable id)hcg_valueForUndefinedKey:(NSString *)key {
        NSString* exceptionName = @"HCGUnknownKeyException";
        NSString* exceptionReason = [NSString stringWithFormat:@"****[%@ hcg_valueForUndefinedKey:]: this class is not key value coding-compliant for the key %@.****", self, key];
        @throw [NSException exceptionWithName:exceptionName reason:exceptionReason userInfo:nil];
    }
    
    -(void)hcg_setNilValueForKey:(NSString *)key; {
        NSString* exceptionName = @"HCGInvalidArgumentException";
        NSString* exceptionReason = [NSString stringWithFormat:@"****[%@ hcg_setNilValueForKey:]: could not set nil as the value for the key %@.****", self, key];
        @throw [NSException exceptionWithName:exceptionName reason:exceptionReason userInfo:nil];
    }
    
    #pragma mark - Helper
    
    -(bool)hcg_performSelectorWithMethodName:(NSString *)aMethodName value:(id)aValue {
        if ([self respondsToSelector:NSSelectorFromString(aMethodName)]) {
    // 在确定不会发生内存泄露的情况下,可以使用如下编译器指令来忽略内存泄露的警告(PerformSelector may cause a leak because its selector is unknown)
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [self performSelector:NSSelectorFromString(aMethodName) withObject:aValue];
    #pragma clang diagnostic pup
            return YES;
        }
        return NO;
    }
    
    -(id)hcg_performSelectorWithMethodName:(NSString *)aMethodName {
        if ([self respondsToSelector:NSSelectorFromString(aMethodName)]) {
    // 在确定不会发生内存泄露的情况下,可以使用如下编译器指令来忽略内存泄露的警告(PerformSelector may cause a leak because its selector is unknown)
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            return [self performSelector:NSSelectorFromString(aMethodName)];
    #pragma clang diagnostic pup
        }
        return nil;
    }
    
    -(NSArray<NSString *> *)hcg_getIvarNameList {
        // 创建一个用于存储成员变量名称的可变数组
        NSMutableArray<NSString *>* mArr = [NSMutableArray array];
        // 遍历当前类对象的成员变量列表,获取各个成员变量的名称
        unsigned int count = 0;
        Ivar* ivarList = class_copyIvarList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Ivar ivar = ivarList[i];
            NSString* ivarName = @(ivar_getName(ivar));
            [mArr addObject:ivarName];
        }
        free(ivarList);
        // 返回成员变量名称列表
        return mArr;
    }
    
    @end
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值