Introduction to Key-Value Observing Programming Guide
Key-value observing 键-值观察是一种机制,可以用于监听某个对象的指定属性值在发生更改时得到通知,是Objective-C对观察者设计模式的一种实现。经常用于程序中Controller对象观察Model对象的属性,View对象通过Controller观察Model对象的属性。此外,Model对象可以观察其他Model对象(通常用于确定从属值何时更改),甚至可以观察自身(再次确定从属值何时更改)。
可以观察属性,包括简单属性、to-one关系和to-many关系。to-many关系的观察者会被告知所做更改的类型以及更改涉及的对象。
KVO的实现基础之一是被监控对象必须拥有相应的setter方法,换句话说只有ivar(成员变量)的类是无法进行监控的。
成员变量直接修改需要手动触发KVO:
[self willChangeValueForKey:@"keyPath"];
ivar = newivar;
[self didChangeValueForKey:@"keyPath"];
举个例子说明KVO如何发挥作用。假设Account
表示Person
在银行的储蓄帐户。Person
实例可能需要知道Account
实例的某些方面何时发生更改,例如余额或利率。
如果这些属性是Account
的公共属性,Person
可以定期轮询Account
以发现变化,但这显然是低效的,且常常是不切实际的。更好的方法是使用KVO,它类似于在发生更改时接收中断。
要使用KVO,首先必须确保被观察的对象(本例中的Account
)与KVO兼容。通常,如果对象继承自“NSObject”,并且以常规方式创建属性,那么对象及其属性将自动兼容KVO。也可以手动实现。KVO compliance描述了自动和手动实现KVO的区别,以及如何实现两者。
接下来,必须将观察者实例Person
注册到被观察的实例Account
。对于每个被观察到的key path,Person
发送一条addObserver:forKeyPath:options:context:消息给Account
,并将自己命名为观察者。
为了从Account
接收更改通知,Person
实现了observeValueForKeyPath:ofObject:change:context:方法,这个方法所有观察者都需要实现。Account
在每次注册的key paths发生更改时将此消息发送给Person
。然后 Person
可以根据变更通知进行适当的处理。
最后,当它不再需要通知,至少在它被deallocated之前, Person
实例必须通过发送removeObserver:forKeyPath:消息给Account
取消注册。
Registering for Key-Value Observing 描述了注册、接收和取消注册KVO通知的整个生命周期。
Registering Dependent Keys 解释了如何指定一个键的值依赖于另一个键的值。
与使用NSNotificationCenter
的通知不同,KVO没有为所有观察者提供更改通知的中心对象。相反,当发生更改时,通知会直接发送到观察对象。`NSObject'提供了键值观察的基本实现,我们很少需要重写这些方法。
重要提示:
并非所有类都对所有属性都兼容KVO。通过遵循KVO Compliance中描述的步骤,可以确保自己的类符合KVO。通常情况下,苹果提供的框架中的属性只有在上面文档记录的才符合KVO。
addObserver:forKeyPath:options:context:
方法不对观察对象、被观察对象或上下文的强引用。如有必要应该确保维护对观察对象、被观察对象和上下文的强引用。
移除观察者时,记住以下几点:
如果尚未注册为观察员,则请求将其作为观察员删除会导致
NSRangeException
。addObserver:forKeyPath:options:context:
和removeObserver:forKeyPath:context:
要对应,或者在try/catch块内调用removeObserver:forKeyPath:context:
以处理潜在异常。释放时,观察者不会自动删除自身。被观察对象继续发送通知,而不考虑观察者的状态。但是,与任何其他消息一样,发送到已释放对象的更改通知会触发内存访问异常。因此,要确保观察者在从内存中释放之前将自己移除。
协议没有提供询问对象是观察者还是被观察者的方法。构造代码以避免与释放release相关的错误。一种典型的模式是在观察者初始化期间(例如在init或viewDidLoad中)注册为观察者,并在释放期间注销(通常在dealoc中),确保正确配对和有序地添加和删除消息,并且在将观察者从内存中释放之前取消注册。
KVO实现细节
1、 KVO 的实现依赖于 Objective-C 的 Runtime 。自动KVO是使用 isa-swizzling
的技术实现的。isa 指针指向维护调度表的对象类。这个调度表本质上包含了指向类实现的方法和其他数据的指针。
2、 当观察者为对象K的属性注册时,runtime 动态创建一个继承自K对象的中间类NSKVONotifying_K,并将K的isa
指针指向这个中间类。因此,isa
指针的值不一定反映实例的实际类。不能依赖isa
指针来确定类成员身份。
3、重写了被观察属性的 setter 方法。setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改。当修改实例对象的属性时秒回调用Foundation的 _NSSetXXXCalueAndNotify函数
- willChangeValueForKey:
- 父类K原来的 setter
- didChangeValueForKey:,其内部会触发监听器(Oberser)的监听方法:- (void)observeValueForKeyPath: ofObject: change: context:
示例验证(这里需要引入一个分类NSObject+DLIntrospection打印instanceMethods)
//KVOModel类
@interface KVOModel : NSObject
@property(nonatomic, copy)NSString *name;
@end
//controller
_kvo1 = [KVOModel new];
NSLog(@"1、-------监听之前");
NSLog(@"setter_地址_:%p", [_kvo1 methodForSelector:@selector(setName:)]);
NSLog(@"class_:%@", [_kvo1 class]);
NSLog(@"object_getClass_:%@", object_getClass(_kvo1));
NSLog(@"object_getClass_instanceMethods_:%@", [object_getClass(_kvo1) instanceMethods]);
[_kvo1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"2、-------监听之后");
NSLog(@"setter_地址_:%p", [_kvo1 methodForSelector:@selector(setName:)]);
NSLog(@"class_:%@", [_kvo1 class]);
NSLog(@"object_getClass_:%@", object_getClass(_kvo1));
NSLog(@"object_getClass_instanceMethods_:%@", [object_getClass(_kvo1) instanceMethods]);
[_kvo1 removeObserver:self forKeyPath:@"name"];
NSLog(@"3、-------去掉监听之后");
NSLog(@"setter_地址_:%p", [_kvo1 methodForSelector:@selector(setName:)]);
NSLog(@"class_:%@", [_kvo1 class]);
NSLog(@"object_getClass: %@", object_getClass(_kvo1));
NSLog(@"object_getClass_instanceMethods_:%@", [object_getClass(_kvo1) instanceMethods]);
运行项目
2020-09-16 16:58:46.955488+0800 TT[14496:252466] 1、-------监听之前
2020-09-16 16:58:46.955717+0800 TT[14496:252466] setter_地址_:0x105b2c9a0
2020-09-16 16:58:46.955856+0800 TT[14496:252466] class_:KVOModel
2020-09-16 16:58:46.955962+0800 TT[14496:252466] object_getClass_:KVOModel
2020-09-16 16:58:46.956310+0800 TT[14496:252466] object_getClass_instanceMethods_:(
"- (void).cxx_destruct",
"- (id)name",
"- (void)setName:(id)arg0 "
)
2020-09-16 16:58:46.956743+0800 TT[14496:252466] 2、-------监听之后
2020-09-16 16:58:46.956898+0800 TT[14496:252466] setter_地址_:0x105e0798b
2020-09-16 16:58:46.957024+0800 TT[14496:252466] class_:KVOModel
2020-09-16 16:58:46.957154+0800 TT[14496:252466] object_getClass_:NSKVONotifying_KVOModel
2020-09-16 16:58:46.957337+0800 TT[14496:252466] object_getClass_instanceMethods_:(
"- (void)setName:(id)arg0 ",
"- (class)class",
"- (void)dealloc",
"- (BOOL)_isKVOA"
)
2020-09-16 16:58:46.957484+0800 TT[14496:252466] 3、-------去掉监听之后
2020-09-16 16:58:46.957606+0800 TT[14496:252466] setter_地址_:0x105b2c9a0
2020-09-16 16:58:46.957710+0800 TT[14496:252466] class_:KVOModel
2020-09-16 16:58:46.957840+0800 TT[14496:252466] object_getClass: KVOModel
2020-09-16 16:58:46.958318+0800 TT[14496:252466] object_getClass_instanceMethods_:(
"- (void).cxx_destruct",
"- (id)name",
"- (void)setName:(id)arg0 "
)
上面的结果说明,在KVOModel对象的实例 _kvo1 被观察时,runtime动态创建了一个KVOModel类的子类NSKVONotifying_KVOModel,而且为了隐藏这个行为,NSKVONotifying_KVOModel重写了- (Class)class方法返回之前的KVOModel类。但是使用object_getClass()就暴露了,因为这个方法返回的是这个对象的isa指针,isa指针指向的一定是个这个对象的类对象。
NSObject+DLIntrospection 的instanceMethods是在arc下所有dealloc调用完成后负责释放所有的变量。
从上面2、-------监听之后
的打印可以看出,动态类重写了4个方法:
-
- (void)setName:(id)arg0
:最主要的重写方法,set值时调用通知函数; -
- (class)class
隐藏自己,返回原来类的class; -
- (void)dealloc
清理监听时的动态修改; -
- (BOOL)_isKVOA
内部使用的标示,判断这个类有没被KVO动态生成子类。
KVO 的缺点:
- 只能通过重写 -observeValueForKeyPath:ofObject:change:context: 方法来获得通知,在复杂的业务逻辑中,准确判断被观察者相对比较麻烦。
- 需要手动移除观察者,且移除观察者的时机必须合适;
- 注册观察者的代码和事件发生处的代码上下文不同,传递上下文是通过 void * 指针。
自己代码实现KVO
系统是自动实现的中间类NSKVONotifying_KVOModel,我们自己手动创建一个中间类CustomKVO_KVOModel。给NSObject创建一个分类,让每一个对象都拥有我们自定义的KVO特性。这里只做简单实现以帮助增加对KVO原理的理解。
//NSObject+KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (KVO)
- (void)m_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
//NSObject+KVO.m
#import "NSObject+KVO.h"
#import <objc/message.h>
#import <objc/message.h>
@implementation NSObject (KVO)
- (void)m_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
//这里自定义kvo
{
//注册一个类
//生成中间类的类名 CustomKVO_XXX
NSString *oldName = NSStringFromClass([self class]);
NSString *newName = [NSString stringWithFormat:@"CustomKVO_%@", oldName];
//动态创建类
Class customClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
//设置对象的isa指针,修改 isa 指向
object_setClass(self, customClass);
//重写 setter 方法
NSString *methodName = [NSString stringWithFormat:@"set%@:", keyPath.capitalizedString];
SEL sel = NSSelectorFromString(methodName);
//将方法添加到动态类
class_addMethod(customClass, sel, (IMP)kvo_setter, "v@:@");
//关联 观察者 属性
objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_ASSIGN);
}
//IMP ----setter:
void kvo_setter(id self, SEL _cmd, NSString *name){
//改变父类的属性值
struct objc_super superClass = {
self,
class_getSuperclass([self class])
};
//调用父类
objc_msgSendSuper(&superClass, _cmd, name);
//获取观察者
id observer = objc_getAssociatedObject(self, (__bridge const void *)@"objc");
//获取setter方法名
NSString *methodName = NSStringFromSelector(_cmd);
//settName: 获取 name
NSString *key = getValueKey(methodName);
//通知观察者变化 调用observeValueForKeyPath
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), key, self, @{key:name}, nil);
}
NSString *getValueKey(NSString *setter)
{
//在setter方法中截取属性key 如 setName: 中截取 name,没有做容错
NSRange range = NSMakeRange(3, setter.length-4);
NSString *key = [setter substringWithRange:range];
key = [key lowercaseString];
return key;
}
此时我们调用自己定义的监听方法, 效果和系统的也是一样的
[_kvo1 m_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
补充
- 直接修改成员变量不会触发 KVO,因为没有通过 setter 方法。
- 通过 KVC 修改属性会触发 KVO。
- KVC 通过
setValue: forKey:
或setValue: forKeyPath:
赋值 - KVC 在修改值得前后会分别自动调用willChangeValueForKey和didChangeValueForKey:
-
(BOOL)accessInstanceVariablesDirectly是否可以访问成员变量默认是 YES, 可以访问,所当根据 key找到的是成员变量时(没有setKey:和 _setKey:方法)也会触发 KVO
-