KVO(键值观察)键值观察是一种机制,它允许将其他对象的指定属性的更改通知给对象。
重要提示: 为了了解键值观察,您必须首先了解键值编码。
使用
要使用KVO,首先必须确保观察到的对象符合KVO。通常,如果您的对象继承NSObject并以常规方式创建属性,则您的对象及其属性将自动符合KVO标准
- 注册观察者对象
- (void)addObserver:(NSObject *)observer //观察者
forKeyPath:(NSString *)keyPath //键路径
options:(NSKeyValueObservingOptions)options //选项,新值或旧值
context:(void *)context;
参数说明:
context 传递给观察者的任意数据,没有可以填NULL(void *是C的指针类型)
[self.person addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew
context:NULL];
- 在观察者内部实现以下方法,以接受更改通知消息。
- (void)observeValueForKeyPath:(NSString *)keyPath //键路径
ofObject:(id)object //受观察属性的对象
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context;
参数context
说明:
上下文指针
context
包含任意数据,这些数据将在相应的更改通知中传递回观察者。您可以指定NULL并完全依赖键路径字符串来确定更改通知的来源,但是这种方法可能会给对象的父类带来问题,该对象的父类也出于不同的原因而观察相同的键路径。
一种更安全,更可扩展的方法是使用上下文context
确保您收到的通知是发给观察者的,而不是超类的。
可以为每个观察到的键路径创建一个不同的上下文,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析
如果同时添加了多个观察者,也可以通过这种方式实现而不用嵌套多层去判断具体是那个属性
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
这样在observeValueForKeyPath
的方法里通过context
区分
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
- 当观察者不再接收消息时,使用
removeObserver:forKeyPath:
该方法注销观察者。至少在观察者释放内存之前调用此方法。
如果没有移除观察者,会造成野指针错误
比如写在dealloc
方法里
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"name"];
}
- 自动和手动触发KVO
在被观察的对象中,通过重写automaticallyNotifiesObserversForKey:
方法返回YES
自动触发,NO
手动触发.也可以通过参数key
指定自动观察的属性
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"account"]) {
return YES;
}
return NO;
}
- 手动触发需要在在更改值之前调用
willChangeValueForKey:
和更改值之后调用didChangeValueForKey:
方法,通常是写在setter方法中
- (void)setAccount:(NSString *)account{
if (account != _account) {
[self willChangeValueForKey:@"account"];
_account = account;
[self didChangeValueForKey:@"account"];
}
}
- 可变数组
KVO是建立在KVC基础之上的
可变数组hobby
@property(nonatomic)NSMutableArray *hobby;
注册监听
[self.person addObserver:self
forKeyPath:@"hobby"
options:NSKeyValueObservingOptionNew
context:NULL];
这种方式没有使用KVC机制,自然也不会触发监听.
[self.person.hobby addObject:@"1"];
mutableArrayValueForKey :
方法是KVC的方法,这样写就会触发监听
[[self.person mutableArrayValueForKey:@"hobby"] addObject:@"1"];
KVO原理探究
自动键值观察是使用isa-swizzling的技术实现的。
- 对象的
isa
指针指向对象的类,这个类中保存一个调度表。该分派表实质上包含指向该类实现的方法的指针以及其他数据。 - 在为对象的属性注册观察者时,将修改对象的isa指针,指向中间类而不是真实类。(因此,isa指针的值不一定反映实例的实际类,不要依靠
isa
指针来确定类成员。相反,您应该使用该class
方法确定对象实例的类。 - 观察是属性的setter方法
- 动态生成子类(中间类),原对象的isa指向生成的中间类
(1) 通过断点,查看注册前后isa的指向
isa = (Class)Person 0x000000010326ab38
注册后isa指向
isa = (Class) NSKVONotifying_Person 0x000060000037ba80
(2) 通过Runtime的API在注册观察者前后打印子类
Class cls = [self.person class];
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 用来存放类
NSMutableArray *mArray = [NSMutableArray array];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
注册观察者之前数组为空,注册之后多了一个子类,证明是动态产生的,并且是继承关系(中间类继承自实际类)
"NSKVONotifying_Person"
- 重写setter方法
动态生成的中间类,重写了父类的setter
, class
, dealloc
以及_isKVOA
通过下面方式打印出两个类的所有方法:
- (void)classAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableArray *list = [NSMutableArray array];
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
printf("%-16s--%p\n",[NSStringFromSelector(sel) UTF8String],imp);
}
free(methodList);
}
真实的类Person
hobby --0x10992da90
setHobby: --0x10992dab0
init --0x10992d8f0
.cxx_destruct --0x10992daf0
name --0x10992d9d0
setName: --0x10992d9f0
setAccount: --0x10992d840
account --0x10992da70
age --0x10992da30
setAge: --0x10992da50
中间类NSKVONotifying_Person
setHobby: --0x109cb4c7a
setAccount: --0x109cb4c7a
class --0x109cb373d
dealloc --0x109cb34a2
_isKVOA --0x109cb349a
比较中间类和真实类的方法IMP,证明子类是重写了被观察属性的setter
方法;以及class
方法,保证外界通过class方法返回的类是真实的Person
;dealloc在移除观察者之后,isa指回真实Person
类;之后动态生成的中间类会一直存在缓存中,不会销毁。
3.移除的时候,对象的isa指回原来的类
isa = (Class)Person 0x000000010326ab38
总结一下原理就是:
- 注册观察者时,系统会动态生成中间类(子类)
- 对象isa指向中间类,该类重写了父类的
setter
,class
,dealloc
以及_isKVOA
- 移除观察者以后,对象isa又指回原来的类
网友评论