之前的文章里说到,OC主要是基于Smalltalk进行设计的,因此它有很多类似Python的动态特性,例如动态类型,动态加载,动态绑定等。今天我们将介绍OC中的键值编码(KVC)和键值监听(KVO)特性。
键值编码(KVC)
在C#我们可以反射读写一个对象的属性,有时候这种方式特别方便,因为我们可以利用字符串的方式来动态控制一个对象。因为OC的语言特性(基于Smalltalk),你根本不必进行任何操作就可以进行属性的动态读写,这种方式就是KVC(Key Value Coding)。
KVC的操作方法由NSKeyValueCoding协议提供,而NSObject就实现了这个协议,也就是说OC中几乎所有的对象都支持KVC操作,常用的KVC操作方法如下:
1. 动态设置:setValue:属性值 forKey:属性名(只适用于简单路径)、setValue:属性值forKeyPath:属性名路径(用于复合路径,例如HXPerson有一个HXAccount类型的属性,那么person.account就是一个复合属性)。
2. 动态读取:valueForKey:属性名、valueForKeyPath:(用于复合路径)。
下面通过例子来讲解KVC
HXDog.h
#import <Foundation/Foundation.h>
@interface HXDog : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
HXDog.m
#import "HXDog.h"
@implementation HXDog
@end
HXPerson.h
#import <Foundation/Foundation.h>
@class HXDog;
@interface HXPerson : NSObject {
@private
int age;// 声明一个私有变量
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) HXDog *dog;
/**
* 读取该实例的私有属性
*/
- (void)showInfor;
@end
HXPerson.m
#import "HXPerson.h"
@implementation HXPerson
- (void)showInfor {
NSLog(@"self age is: %d", age);
}
@end
在main函数中:
#import <Foundation/Foundation.h>
#import "HXPerson.h"
#import "HXDog.h"
int main(int argc, const char * argv[]) {
HXPerson *person = [[HXPerson alloc] init];
[person setValue:@"shx" forKey:@"name"];
[person setValue:@28 forKey:@"age"];// 可以给私有变量设置属性值
[person showInfor];// 打印私有变量的属性值
NSLog(@"person name = %@", person.name);
HXDog *dog = [[HXDog alloc] init];
person.dog = dog;
dog.age = 2;
// 复合路径设置属性值
[person setValue:@"小黄" forKeyPath:@"dog.name"];
NSLog(@"---%@", [person valueForKeyPath:@"dog.name"]);
return 0;
}
打印结果:
KVC使用起来比较简单,但是用怎样的规则对属性进行读取的呢?大概查找规则总结一下(假设现在要利用KVC对value进行读取):
1. 如果是动态设置属性,则优先考虑调用setValue方法,再考虑使用成员变量_value,再考虑使用成员变量value,在考虑使用setValue:forUndefinedKey:方法(不管这些方法、成员变量是私有的还是公共的都能正确设置))
2. 如果是动态读取属性,则优先考虑调用.value(getter方法),再考虑使用成员变量_value,再考虑使用成员变量value,在考虑使用valueForUndefinedKey:方法(不管这些方法、成员变量是私有的还是公共的都能正确读取))
键值监听(KVO)
KVO其实是一种观察者模式,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器的视图组件就会被激发,激发时就会回调监听器自身。在OC中要实现KVO则必须实现NSKeyValueObServing协议,不过幸运的是NSObject已经实现了该协议,因此几乎所有的OC对象都可以使用KVO。
在OC中使用KVO操作常用的方法如下:
1. 注册指定Key路径的监听器:addObserver: forKeyPath: options: context:
2. 删除指定Key路径的监听器:removeObserver: forKeyPath、removeObserver:forKeyPath: context:
3. 回调监听:observeValueForKeyPath: ofObject: change: context:
使用KVO的具体步骤如下:
1. 通过addObserver:forKeyPath: options: context:为被监听对象(它通常是数据模型)注册监听器
2. 重写监听器的observeValueForKeyPath:ofObject: change: context:方法
还利用上面的例子,dog的age改变时,主人person要获得通知。此时dog作为被监听对象,person作为监听器对象。
HXDog.h
#import <Foundation/Foundation.h>
@interface HXDog : NSObject
@property (nonatomic, assign) int age;
@end
HXDog.m
#import "HXDog.h"
@implementation HXDog
@end
HXPerson.h
#import <Foundation/Foundation.h>
@class HXDog;
@interface HXPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) HXDog *dog;
@end
HXPerson.m
#import "HXPerson.h"
#import "HXDog.h"
@implementation HXPerson
- (void)setDog:(HXDog *)dog {
_dog = dog;
// 使得person成为监听对象(要重写observeValueForKeyPath方法),dog成为被监听对象
[self.dog addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
}
/**
* 监听器重写触发监听回调的方法
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"age"]) {// 只监听dog年龄的概念
NSLog(@"keyPath=%@,object=%@,newValue=%.2f,context=%@",keyPath,object,[[change objectForKey:@"new"] floatValue],context);
}
}
- (void)dealloc {
// 移除dog被监听状态
[self.dog removeObserver:self forKeyPath:@"age"];
}
@end
main.m
#import <Foundation/Foundation.h>
#import "HXPerson.h"
#import "HXDog.h"
int main(int argc, const char * argv[]) {
HXPerson *person = [[HXPerson alloc] init];
person.name = @"shxkvo";
HXDog *dog = [[HXDog alloc] init];
person.dog = dog;
dog.age = 2;// 该行会触发监听器的回调方法
dog.age = 3;// 该行会触发监听器的回调方法
return 0;
}
打印结果:
在上面的代码中我们在给person分配dog时,就给dog的age属性添加了监听,并且在监听回调方法中输出了监听到的信息,同时在对象销毁时移除监听,这就构成了一个典型的KVO应用。