十四:底层探索 – KVC

KVC:全名Key Value Coding,通常我们将它叫作“键值编码”,官网有详细的解释。它可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值,而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。那么它的原理究竟是什么样子的呢?

一: 基本使用

常见API

- (nullable id)valueForKey:(NSString *)key;                          //直接通过Key来取值

- (void)setValue:(nullable id)value forKey:(NSString *)key;          //通过Key来设值

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

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

其他API

// 默认为YES。 如果返回为YES,如果没有找到 set<Key> 方法的话, 会按照_key, _isKey, key, isKey的顺序搜索成员变量, 返回NO则不会搜索
+ (BOOL)accessInstanceVariablesDirectly;
// 键值验证, 可以通过该方法检验键值的正确性, 然后做出相应的处理
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 如果key不存在, 并且没有搜索到和key有关的字段, 会调用此方法, 默认抛出异常。两个方法分别对应 get 和 set 的情况
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// setValue方法传 nil 时调用的方法
// 注意文档说明: 当且仅当 NSNumber 和 NSValue 类型时才会调用此方法 
- (void)setNilValueForKey:(NSString *)key;
// 一组 key对应的value, 将其转成字典返回, 可用于将 Model 转成字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
复制代码

示例代码:

#import <Foundation/Foundation.h>

#import "WYStudent.h"

typedef struct {

    float x, y, z;

} ThreeFloats;

?
@interface WYPerson : 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) WYStudent         *student;

@end


?
@interface WYStudent : 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

复制代码

1 访问对象属性

setValuevalueForKey

WYPerson *person = [[WYPerson alloc] init];
[person setValue:@"啊云" forKey:@"name"];
NSLog(@"person 的姓名为: %@", [person valueForKey:@"name"]);

打印结果:
person 的姓名为: 啊云
复制代码

valueForKeyPath: 和 setValue:ForKeyPath

WYStudent *student = [WYStudent alloc];
[person setValue:@"Swift" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

打印结果:
person 的姓名为: Swift
复制代码

2 访问集合属性

第一种方法:直接用一个新数组进行赋值

NSArray *array = @[@"100",@"2",@"3"];

[person setValue:array forKey:@"array"];

NSLog(@"%@",[person valueForKey:@"array"]);


打印结果:100,2,3
复制代码

第二种方法:取出数组以可变数组形式保存,再修改(推荐使用这种方法)

WYPerson *person = [[WYPerson alloc] init];

person.array = @[@"1",@"2",@"3"];

NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];

mArray[0] = @"200";

[mArray addObject:@(100)];

NSLog(@"%@",[person valueForKey:@"array"]);

打印结果:200,2,3,100
复制代码

mutableArrayValueForKey:这个方法会通过传入的 key 返回对应属性的一个可变数组的代理对象,对于集合对象,苹果为我们提供了更友好的方法。

  • mutableArrayValueForKey:mutableArrayValueForKeyPath:

    • These return a proxy object that behaves like an NSMutableArray object.
    • 【译】返回的代理对象表现为一个 NSMutableArray 对象
  • mutableSetValueForKey:mutableSetValueForKeyPath:

    • These return a proxy object that behaves like an NSMutableSet object.
    • 【译】返回的代理对象表现为一个 NSMutableSet 对象
  • mutableOrderedSetValueForKey: and mutableOrderedSetValueForKeyPath:

    • These return a proxy object that behaves like an NSMutableOrderedSet object.

    • 【译】返回的代理对象表现为一个 NSMutableOrderedSet 对象

3 访问字典

setValuesForKeysWithDictionary:用来修改Model中对应key的属性(字典转模型)

dictionaryWithValuesForKeys:输入一组key,返回这组key对应的属性,再组成一个字典

举个?

NSDictionary* dict = @{
                           @"name":@"Cooci",
                           @"nick":@"KC",
                           @"subject":@"iOS",
                           @"age":@18,
                           @"length":@180
                           };
    WYStudent *p = [[WYStudent alloc] init];
    // 字典转模型
    [p setValuesForKeysWithDictionary:dict];
    NSLog(@"%@",p);
    // 键数组转模型到字典
    NSArray *array = @[@"name",@"age"];
    NSDictionary *dic = [p dictionaryWithValuesForKeys:array]
    NSLog(@"%@",dic);
    
    
   ? 打印结果
   <WYStudent: 0x600003636eb0>
   
   age = 18;
   name = Cooci;

**}**
复制代码

4 集合运算符

集合运算符可以分为三大类:

  • 聚合操作符

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

    • @distinctUnionOfObjects: 返回操作对象指定属性的集合–去重
    • @unionOfObjects: 返回操作对象指定属性的集合
  • 嵌套操作符

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

聚合操作符?

NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        WYStudent *p = [WYStudent 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);
复制代码

数组操作符?

NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        WYStudent *p = [WYStudent 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);
复制代码

5 访问非对象属性

非对象属性分为两类,一类是基本数据类型,一类是结构体(struct)

对于基本数据类型,常用的基本数据类型需要在设置属性的时候包装成 NSNumber 类型,然后在读取值的时候使用各自对应的读取方法,如 double 类型的标量读取的时候使用 doubleValue

对于结构体类型来说,需要转化为NSValue

// 结构体
// 设值

    ThreeFloats floats = {1.,2.,3.};
    NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];

// 取值

    NSValue *value1    = [person valueForKey:@"threeFloats"];
    NSLog(@"%@",value1);
    ThreeFloats th;
    [value1 getValue:&th];
    NSLog(@"%f-%f-%f",th.x,th.y,th.z);
复制代码

6 属性验证

该功能是通过validateValue:forKey:error:来实现

需要注意的是这个方法系统不会自己调用,需要我们手动实现

@implementation WYPerson
- (BOOL)validateName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{
    NSString* name = *value;
    name = name.capitalizedString;
    if ([name isEqualToString:@"Not-name"]) {
        return NO;
    }
    return YES;
}
@end
- (void)viewDidLoad {
    [super viewDidLoad];
    
    WYPerson *person = [[WYPerson alloc]init];
    NSError* error;
    NSString *value = @"Not-name"; // result 为 NO 
    //NSString *value = @"BookName";  // result 为 YES
    BOOL result = [person validateValue:&value forKey:@"name" error:&error];
    if (result) {
        NSLog(@"OK");
    } else {
        NSLog(@"NO");
    }
}
复制代码

7 异常处理

key不存在

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {

    NSLog(@"您设置的key:[%@]不存在", key);
    NSLog(@"您设置的value为:[%@]", value);

}

- (id)valueForUndefinedKey:(NSString *)key {

    NSLog(@"您访问的key:[%@]不存在", key);
    return nil;

}
复制代码

value为nil

- (void)setNilValueForKey:(NSString *)key {
    //对不能接受nil的属性进行处理
    if ([key isEqualToString:@"price"]) {
        //对应你具体的业务来处理
        price = 0;
    }else {
        [super setNilValueForKey:key];
    }
}


复制代码

二: 底层原理分析

1 设值原理

setValue:forKey: 方法默认实现会在调用者传入 keyvalue(如果是非对象类型,则指的是解包之后的值) 之后会在对象中按下列的步骤进行模式搜索:

  • 1.以 set<Key>:, _set<Key> 的顺序在对象中查找是否有这样的方法,如果找到了,则把属性值传给方法来完成属性值的设置。

  • 2.判断类方法 accessInstanceVariablesDirectly 结果

    • 如果返回 YES,则以 _<key>, _is<Key>, <key>, is<Key> 的顺序查找成员变量,如果找到了,则把属性值传给方法来完成属性值的设置。
    • 如果返回 NO,跳转到第 3 步
  • 3.调用 setValue:forUndefinedKey:。 默认情况下,这会引发一个异常,但是NSObject 的子类可以提供特定于 key 的行为。

image.png

2 取值原理

valueForKey: 方法会在调用者传入 key 之后会在对象中按下列的步骤进行模式搜索:

  • 1.以 get<Key>, <key>, is<Key> 以及 _<key> 的顺序查找对象中是否有对应的方法。

    • 如果找到了,将方法返回值带上跳转到第 5 步
    • 如果没有找到,跳转到第 2 步
  • 2.查找是否有 countOf<Key>objectIn<Key>AtIndex: 方法(对应于 NSArray 类定义的原始方法)以及 <key>AtIndexes: 方法(对应于 NSArray 方法 objectsAtIndexes:)

    • 如果找到其中的第一个(countOf<Key>),再找到其他两个中的至少一个,则创建一个响应所有 NSArray 方法的代理集合对象,并返回该对象。(翻译过来就是要么是 countOf<Key> + objectIn<Key>AtIndex:,要么是 countOf<Key> + <key>AtIndexes:,要么是 countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:)
    • 如果没有找到,跳转到第 3 步
  • 3.查找名为 countOf<Key>enumeratorOf<Key>memberOf<Key> 这三个方法(对应于NSSet类定义的原始方法)

    • 如果找到这三个方法,则创建一个响应所有 NSSet 方法的代理集合对象,并返回该对象
    • 如果没有找到,跳转到第 4 步
  • 4.判断类方法 accessInstanceVariablesDirectly 结果

    • 如果返回 YES,则以 _<key>, _is<Key>, <key>, is<Key> 的顺序查找成员变量,如果找到了,将成员变量带上跳转到第 5 步,如果没有找到则跳转到第 6 步
    • 如果返回 NO,跳转到第 6 步
  • 5.判断取出的属性值

    • 如果属性值是对象,直接返回
    • 如果属性值不是对象,但是可以转化为 NSNumber 类型,则将属性值转化为 NSNumber 类型返回
    • 如果属性值不是对象,也不能转化为 NSNumber 类型,则将属性值转化为 NSValue 类型返回
  • 6.调用 valueForUndefinedKey:。 默认情况下,这会引发一个异常,但是 NSObject 的子类可以提供特定于 key 的行为。

image.png

三:应用

动态的设置和取值

利用KVC动态的取值和设值是最基本的用途

访问私有属性

对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的

字典转模型

和runtime配合,使用setValuesForKeysWithDictionary实现字典转模型。

访问控件的内部属性

举个?,使用KVC修改textfield占位文字颜色和字号

[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"]; 
[textField setValue:[UIFont boldSystemFontOfSize:16] forKeyPath:@"_placeholderLabel.font"];
复制代码

消息传递

举个?

NSArray *array = @[@"Hank",@"Cooci",@"Kody",@"CC"];
NSArray *lenStr= [array valueForKeyPath:@"length"];    NSLog(@"%@",lenStr);// 消息从array传递给了string
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"%@",lowStr);

? 打印结果:
beijing,shanghai,guangzhou,shenzhen
复制代码

以上例子是把数组内所有元素首字母变成小写,valueForKey会将消息传递给每个元素,而不是容器本身。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享