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

本文详细解析了Key-Value Coding (KVC)在iOS开发中的核心概念、使用方法、异常处理及自定义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 对象之间的转换:

    基本数据类型 装箱操作 拆箱操作
    BOOL numberWithBool: boolValue (in iOS) / charValue (in macOS)
    char numberWithChar: charValue
    double numberWithDouble: doubleValue
    float numberWithFloat: floatValue
    int numberWithInt: intValue
    long numberWithLong: longValue
    long long numberWithLongLong: longLongValue
    short numberWithShort: shortValue
    unsigned char numberWithUnsignedChar: unsignedChar
    unsigned int numberWithUnsignedInt: unsignedInt
    unsigned long numberWithUnsignedLong: unsignedLong
    unsigned long long numberWithUnsignedLongLong: unsignedLongLong
    unsigned short numberWithUnsignedShort: 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 对象之间的转换:

    基本数据类型 装箱操作 拆箱操作
    CGPoint valueWithCGPoint: CGPointValue
    CGRect valueWithCGRect: CGRectValue
    CGSize valueWithCGSize: CGSizeValue
    NSRange valueWithRange: 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值