KVO是什么
- kvo:Key-Value Observing(键值观察),键值观察是一种机制,它允许将其他对象的指定属性的更改通知给对象。
KVO的使用
- 注册观察者:
addObserver:forKeyPath:options:context:
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
- 接收通知回调:
observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"change:%@",change);
}
- 最后,当不再需要通知时,至少在释放之前,调用取消注册方法:
removeObserver:forKeyPath:
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"name"];
}
- KVO的好处是不用自己实现就能监听到属性的改变,其定义明确的基础架构具有框架级别的支持,使其易于使用,您不必在项目中添加任何代码。此外,基础结构已经具备了完整的功能,这使得轻松支持单个属性的多个观察者以及相关值变得容易。
与使用NSNotificationCenter的通知不同,没有中央对象为所有观察者提供更改通知。而是在进行更改时将通知直接发送到观察对象。 NSObject提供了键值观察的基本实现,因此您几乎不需要重写这些方法。 - KVO的触发有两种情况,自动触发和手动触发,默认情况下都是自动触发的
- 自动触发
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
- 手动触发
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"name"]) {
return NO;
}else{
return [super automaticallyNotifiesObserversForKey:key];
}
}
- (void)setName:(NSString *)name
{
if (name != _name) {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
- 参数
context
addObserver:forKeyPath:options:context:方法中的上下文context指针包含任意数据,这些数据将在相应的更改通知中传递回观察者。可以通过指定context为NULL,从而依靠keyPath即键路径字符串传来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因也观察到相同的键路径而导致问题。所以可以为每个观察到的keyPath创建一个不同的context,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析
一种更安全,更可扩展的方法是使用上下文确保您收到的通知是发给观察者的,而不是超类的。
static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:PersonNickNameContext];
-
removeObserver:forKeyPath:context:
移除观察者需要注意以下3点
When removing an observer, keep several points in mind:
Asking to be removed as an observer if not already registered as one results in an NSRangeException. You either call removeObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception.
An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.
1,要求被移除为观察者(如果尚未注册为观察者)会导致NSRangeException
。您可以对removeObserver:forKeyPath:context:
进行一次调用,以对应对addObserver:forKeyPath:options:context:
的调用,或者,如果在您的应用中不可行,则将removeObserver:forKeyPath:context:
调用在try / catch块内处理潜在的异常。
2,释放后,观察者不会自动将其自身移除。被观察对象继续发送通知,而忽略了观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。因此,您可以确保观察者在从内存中消失之前将自己删除。
3,该协议无法询问对象是观察者还是被观察者。构造代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间(例如,在init或viewDidLoad中)注册为观察者,并在释放过程中(通常在dealloc中)注销,以确保成对和有序地添加和删除消息,并确保观察者在注册之前被取消注册。从内存中释放出来。
- 观察有依赖关系的属性,比如说名字,由firstName和lastName组成
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- 也可以直接写如下代码
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
- 观察集合对象,比如数组,我们调用数组addObject并不会触发kvo,需要做下面处理才能触发
[[self.person mutableArrayValueForKeyPath:@"mArray"] addObject:@"111"];
KVO的实现原理
- 官方文档上有对
Key-Value Observing Implementation Details
的描述,翻译过来
自动键值观察是使用称为isa-swizzling的技术实现的。
顾名思义,isa指针指向维护分配表的对象的类。 该分派表实质上包含指向该类实现的方法的指针以及其他数据。
当为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。 结果,isa指针的值不一定反映实例的实际类。
您永远不应依靠isa指针来确定类成员身份。 相反,您应该使用class方法来确定对象实例的类。
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
-
我们通过lldb调试来观察kvo的实现细节
- 运行程序进入到断点,通过打印数据发现
(lldb) po self.p1
(lldb) po self.p2
(lldb) po self.p1->isa
NSKVONotifying_Person
(lldb) po self.p2->isa
Person
- 这时候会发现添加了Observer后的self.p1对象的isa指针不是指向Person,而是指向一个新的类对象NSKVONotifying_Person,而self.p2对象由于没有添加Observer,所以它的isa指针指向的是类对象Person。
由于我们并没有创建过NSKVONotifying_Person类,所以NSKVONotifying_Person是在运行时动态生成的一个新的类,新类生成之后,又将self.p1的isa指针指向了新的类对象。
为了了解NSKVONotifying_Person的内部构造,我们自定义一个方法来打印Class的方法列表和superClass
- (void)classInfo:(id)obj
{
Class objClass = object_getClass(obj);
Class superClass = class_getSuperclass(objClass);
NSLog(@"class:%@----superClass:%@",objClass,superClass);
unsigned int outCount;
Method *methods = class_copyMethodList(objClass, &outCount);
for (int i = 0; i < outCount; i ++) {
Method me = methods[i];
NSLog(@"method:%@",NSStringFromSelector(method_getName(me)));
}
free(methods);
}
- 执行上图中的程序,得到打印结果
KVO[8996:3547641] class:NSKVONotifying_Person----superClass:Person
KVO[8996:3547641] method:setAge:
KVO[8996:3547641] method:class
KVO[8996:3547641] method:dealloc
KVO[8996:3547641] method:_isKVOA
KVO[8996:3547641] class:Person----superClass:NSObject
KVO[8996:3547641] method:age
KVO[8996:3547641] method:setAge:
从打印结果中可以看出,p1对象由于加了KVO监听,所以它的类对象变成了NSKVONotifying_Person,而NSKVONotifying_Person对象的superClass是Person,说明NSKVONotifying_Person是Person的子类。
在NSKVONotifying_Person实例方法列表中主要有4个方法,setAge:、class、dealloc和_isKVOA,下面我们就来一一分析这四个方法。NSKVONotifying_Person重写了父类中的setAge:方法,在setAge:方法中调用了Foundation框架中的_NSSetXXXValueAndNotify方法,而_NSSetXXXValueAndNotify方法就执行了监听KVO的核心逻辑,由于Person的age属性是int,所有调用_NSSetIntValueAndNotify,伪代码如下:
- (void)setAge:(int)age{
//调用Foundationf框架中的_NSSetIntValueAndNotify方法
[self _NSSetIntValueAndNotify];
}
- (void)_NSSetIntValueAndNotify{
//将要修改age的值
[self willChangeValueForKey:@"age"];
//调用父类的setAge方法去修改age的值
[super setAge:age];
//完成修改age的值,并且执行observeValueForKeyPath方法
[self didChangeValueForKey:@"age"];
}
- 通过观察上面的打印数据
(lldb) po self.p1
可以发现NSKVONotifying_Person会重写父类的class方法,原因是Apple不想让调用者知道NSKVONotifying_Person这个中间类的存在,所以重写class,返回原类的class对象,伪代码如下
- (Class)class{
return [Person class];
}
- 当NSKVONotifying_Person类被销毁的时候,dealloc方法就被用来做一些收尾工作
- _isKVOA则是用来标识当前类是否是通过runtime动态生成的类对象,如果是,就返回YES,不是,则返回NO
还原NSKVONotifying_Person对象的内部构造
-
由于NSKVONotifying_Person是Class类型的对象,所以它内部肯定拥有isa指针和superClass指针,由此可以得到NSKVONotifying_Person的结构如下:
继而可以猜测出NSKVONotifying_Person的实现代码:
@interface NSKVONotifying_Person : Person
@end
@implementation NSKVONotifying_Person
- (void)setAge:(int)age{
//调用Foundationf框架中的_NSSetIntValueAndNotify方法
[self _NSSetIntValueAndNotify];
}
- (void)_NSSetIntValueAndNotify{
//将要修改age的值
[self willChangeValueForKey:@"age"];
//调用父类的setAge方法去修改age的值
[super setAge:age];
//完成修改age的值,并且执行observeValueForKeyPath方法
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
//触发observeValueForKeyPath方法
[self observeValueForKeyPath:@"age" ofObject:self change:nil context:nil];
}
- (void)dealloc{
//释放操作
}
- (Class)class{
return [Person class];
}
- (BOOL)_isKVOA{
return YES;
}
@end
KVO总结
- 首先,给一个实例对象添加KVO,内部是利用Runtime动态生成一个此实例对象的类对象的子类,具体的格式为_NSKVONotifying_XXX,并且让实例对象的isa指针指向这个新生成的类。
- 重写属性的set方法,当调用set方法时,会调用Foundation框架的NSSetXXXValueAndNotify函数
- 在_NSSetXXXValueAndNotify中会执行一下步骤
- 调用willChangeValueForKey:方法
- 调用父类的set方法,重新赋值
- 调用didChangeValueForKey:方法,didChangeValueForKey:内部会触发监听器的observeValueForKeyPath:ofObject:change:context:方法