KVC 探索
KVC 是 Key-Value Coding 的缩写,它是苹果 macOS 和 iOS 中 Cocoa 和 Cocoa Touch 框架中使用的一种机制。KVC 允许间接访问对象的属性,使用字符串来标识属性名称,而不是直接调用方法或访问实例变量。这提供了面向对象编程中更多的灵活性和抽象性。
KVC 通常与其他的 Cocoa 和 Cocoa Touch 框架一起使用,例如 Key-Value Observing 绑定,以提供强大和灵活的用户界面和数据模型。
KVC 设值原理
// Person.h @interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *gender;
@end
// Person.m
@interface Person ()
@property (nonatomic, assign) int age;
@end
// 调用
Person *p = [[Person alloc] initWithName:@"小王" gender:@"男" age:18];
[p print];
[p setValue:@28 forKey:@"age"];
// KVC 设值
[p print];
// 打印
姓名 = 小王, 性别 = 男,私密年龄 = 18
姓名 = 小王, 性别 = 男,私密年龄 = 28
age 属性是私有变量,在这里我们通过 KVC 可以访问到 Person 的私有变量,那么,KVC 是怎么帮助我们通过字符串对属性进行访问的呢?如下图所示:
- 第一步,KVC 首先会按顺序查找对象是否有名为 set,_set 的方法,其中 是属性名,在例子中表示 setAge,_setAge 方法。如果找到了,它会将属性值作为参数传递给这个方法,完成修改;如果没有找到,则进入第二步。
- 第二步,KVC 会先访问 acessInstanceVariablesDirectly 方法询问是否有访问私有变量的权限,如果不允许,则直接进入第三步;如果允许,则按照 _、_is、、is 顺序依次查找是否有成员变量,如果有,则直接修改成员变量;如果没有,则进入第三步。
- 第三步,对象既没有类似的方法实例变量,KVC 会直接调用 setValue:forUndefinedKey: 方法,这个方法同时会抛出一个 NSUndefinedKeyException 异常。
因此,使用KVC设置属性值时,属性名称必须与对象实际拥有的属性名称完全匹配,否则就会抛出NSUnknownKeyException异常。此外,KVC 只能用于设置对象属性,不能用于设置结构体属性或 C 数组属性,对于值类型或结构体类型,需要手动封装成 NSNumber 和 NSValue 再传入。
KVC 取值原理
KVC 同样也提供取值方法,如下代码所示:
Person *p = [[Person alloc] initWithName:@"小王" gender:@"男" age:18];
NSLog([NSString stringWithFormat:@"name = %@, age = %@", p.name, [p valueForKey:@"age"]]);
为了实现取值,KVC 同样也设计了一套类似于设值的原理,如下图所示:
- 第一步,KVC 首先按照 get、、is 的顺序查找 getter 方法,找到调用后返回 OC 对象;如果没找到,则进入第二步。
- 第二步,KVC 会先访问 acessInstanceVariablesDirectly 方法询问是否有访问私有变量的权限,如果不允许,则直接进入第三步;如果允许,则按照 _、_is、、is 顺序依次查找是否有成员变量,如果有,则返回实例变量的 OC 对象;如果没有,则进入第三步。
- 第三步,如果对象既没有对应的实例变量,也没有对应的方法,KVC会调用对象的 valueForUndefinedKey: 方法,并将属性名称作为参数传递给它。这个方法会抛出一个 NSUndefinedKeyException 异常。
KVC 取值有一点需要我们注意,若实例变量不是 OC 对象,KVC 会自动帮我们转换,把值类型封装成 NSNumber,把结构体封装成 NSValue。
keyPath 使用
在实际开发中,我们会需要访问属性的属性,例如访问一个 Person 对象嵌套的 Address 对象的 city 属性。在 KVC 中,keyPath 是一个由属性名和点号(.)连接起来的字符串,用于指定一个对象的属性路径。它可以用于访问对象属性的值,也可以用于对集合属性进行过滤、映射、排序等操作。
通过 keyPath,我们可以访问对象的嵌套属性,可以使用 @”address.city” 访问到 被嵌套的 Address 对象的 city 属性。keyPath 也支持一些集合操作,提供了@”avg”、@”count”、@”max”、@”min”、@”sum” 等操作。比如,在一个包含 Book 对象的数组中计算所有书的总价、均价、数量、最小价格和最大价格,如下代码所示:
Book *book1 = [Book new];
book1.name = @"The Great Gastby";
book1.price = 10;
Book *book2 = [Book new];
book2.name = @"Time History";
book2.price = 20;
Book *book3 = [Book new];
book3.name = @"Wrong Hole";
book3.price = 30;
Book *book4 = [Book new];
book4.name = @"Wrong Hole";
book4.price = 40;
NSArray *arrBooks = @[book1,book2,book3,book4];
NSNumber *sum = [arrBooks valueForKeyPath:@"@sum.price"];
NSLog(@"sum:%f",sum.floatValue);
NSNumber *avg = [arrBooks valueForKeyPath:@"@avg.price"];
NSLog(@"avg:%f",avg.floatValue);
NSNumber *count = [arrBooks valueForKeyPath:@"@count"];
NSLog(@"count:%f",count.floatValue);
NSNumber *min = [arrBooks valueForKeyPath:@"@min.price"];
NSLog(@"min:%f",min.floatValue);
NSNumber *max = [arrBooks valueForKeyPath:@"@max.price"];
NSLog(@"max:%f",max.floatValue);
// 输出
sum:100.000000
avg:25.000000
count:4.000000
min:10.000000
max:40.000000
小结
我们通过对 KVC 的学习和理解,可以发现 KVC 是一种非常有用的技术,以至于在 Swift 仍保留这个机制。合理使用 KVC,可以给我们带来以下便利:
- KVC 能够动态的对 Objective-C 对象的实例(包括私有实例)进行设值和取值;
- KVC 对 Objective-C 容器 setValue:forKey: 和 valueForKey: 做了特殊优化,如 NSMutableDictionary 对象调用 setValue:forKey: 进行设值,传入的 value 为 nil 时,NSMutableDictionary 对象会自动调用 removeObject:forKey:。
KVO 探索
KVO (Key-Value Observing) 是指通过观察者模式来监听对象属性的变化。当被观察对象的属性发生变化时,KVO 会自动通知注册的观察者对象,观察者对象可以在接收到通知后执行相应的操作。
在 Objective-C 和 Swift 中,KVO 是一种非常常用的技术,可以帮助开发者编写更加灵活、可扩展的代码。
KVO 使用
KVO 的使用并不复杂,例如我们要对一个 Person 对象的 name 属性进行观察。
第一步,先给 Person 对象注册观察者对象:在 Person 对象上调用 addObserver:forKeyPath:options:context: 方法,注册观察者对象。该方法会将观察者对象添加到 Person 对象的观察者列表中,并指定要观察的属性和观察选项。如下代码所示:
// 创建一个被观察对象
Person *person = [[Person alloc] init];
// 注册观察者对象
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
- observer 是观察者对象,这里指的是当前实例;
- keyPath 指的是 Person 对象中被观察的 name 属性,允许嵌套;
- options 是 NSKeyValueObservingOptions 类型的枚举,通过 options 可以定义观察行为;
- context 表示上下文,用于区分消息,一般传入 nil。
NSKeyValueObservingOptions 类型的枚举有四类,其中 NSKeyValueObservingOptionNew 表示 Person 对象的 name 属性修改后的新值;NSKeyValueObservingOptionOld 表示 Person 对象中 name 属性修改前的旧值;NSKeyValueObservingOptionInitial 表示 Person 对象注册了观察者后会立马调用观察者方法;NSKeyValueObservingOptionPrior 表示 Person 对象的 name 属性被修改前也会调用一次观察者方法,因此修改一次 name 属性,会调用两次观察者方法。
第二步,实现观察者方法:在观察者对象中实现 observeValueForKeyPath:ofObject:change:context: 方法,该方法会在 Person 对象的属性变化时被调用,可以在该方法中实现相应的操作。如下代码所示:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"Name changed to %@", change[NSKeyValueChangeNewKey]);
}
}
- keyPath 是 Person 对象中被观察的属性名称,和注册时传入的 keyPath 一样是;
- object 表示观察对象;
- change 是一个字典,用于获取 Person 对象属性变化前后的值都;
- context 是注册时传入的 context,用于区分不同的消息。
第三步,修改 Person 对象的 name 属性时,需要用 KVC 或属性调用,才能触发 KVO 的机制,如下代码所示:
// 修改被观察对象的属性
person.name = @"Tom";
[person setValue:@"Tom" forKey:@"name"];
第四步,在不需要再监听属性变化时,需要调用 removeObserver:forKeyPath: 方法,将观察者对象从被观察对象的观察者列表中移除。如下代码所示:
[person removeObserver:self forKeyPath:@"name"];
KVO 原理
KVO 的原理是基于 Objective-C 的 Runtime 机制实现的,当我们为一个对象注册观察者时,KVO 会在运行时动态生成一个新类,并将原对象的 isa 指针指向新类,从而实现对原对象的属性观察。如下图所示:
- KVO 会在运行时动态生成个名为 NSKVONotifying_Person 的类,该类继承自 Person 类,并重写了 setValue:forKey: 和 valueForKey: 方法;
- 当我们为 Person 对象的 name 属性注册观察者后,KVO 会将 name 属性的 setter 方法替换成新的 setter,从而实现属性变化时通知;
- 当我们为 Person 对象注册观察者后,KVO 会将该对象的 isa 指针指向 NSKVONotifying_Person 类,从而使得该对象的所有方法的调用都会被转发到 NSKVONotifying_Person 类中。所以当我们调用 Person 对象 name 属性的 setter 方法,实际上调用的是 NSKVONotifying_Person 类的 setter 方法,该方法会先调用父类的 setter 方法,然后再发送通知,通知所有观察者属性发生了变化。
有一点我们需要注意,KVO 只能观察对象的属性,不能观察成员变量,因为 KVO 是基于 setter 方法实现的,而成员变量的赋值不会触发 setter 方法的调用,所以 KVO 无法观察到成员变量的变化。
总结
今天我们分享了 KVC 和 KVO,我们显示介绍了相关的基础用法,也深入了解它们的本质,虽然我们平时可能用的不多,但两者结合起来的功能十分强大,我们可以借助他们来实现对象间的解耦和模型——视图的 Data Binding,从而提高代码的可拓展性和维护性。