iOS Object-C KVO的正确使用方法及实现原理分析

读完本文内容,你将对KVO的使用有一个更深一层的理解。

KVO,即键值观察,是Cocoa为我们提供的一种模式,用于监听对其他对象属性的更改。尽管有很多人认为KVO的API设计很糟糕,但我们并不去讨论它为何糟糕,本文主要是为需要用KVO来实现需求的童靴写的(避免踩坑)。

KVO正确使用方法

网上一大片的文章中KVO使用是这样的:

[_tableView addObserver:self forKeyPath:@"contentSize" options:0 context:nil];

- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{ 

    if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) { 

        [self configureView]; 

    } 

[_tableView removeObserver:self forKeyPath:@"contentSize"context:nil];

这种写法有以下一些问题:

1.如果属性contentSize被rename了,因为这里传的是字符串,所以rename的时候无法修改,容易忽略导致错误发生。

2.如果self的父类中也使用了KVO,那这里就少了[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];的调用。

3.如果我们试图两次删除相同的观察者,KVO将抛出一个异常并崩溃我们的应用程序。如果父类也在同一个对象上观察相同的参数,会发生什么?它会被移除两次,第二次会导致崩溃。

要解决1、3的问题,我们需要用到context参数,我们确定好添加观察者和删除观察者时的上下文,这里我们用静态指针static void *ClassNameTableViewContentSizeContext = &ClassNameTableViewContentSizeContext;把ClassNameTableViewContentSizeContext定义在.m文件的头部,这样就只能在本文件访问它,也达到了区分上下文的效果。正确的使用KVO方法是:

[_tableView addObserver:self forKeyPath:@"contentSize" options:0 context:ClassNameTableViewContentSizeContext]; 

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary  *)change context:(void *)context {

    if (context == ClassNameTableViewContentSizeContext) { 

        [self doThing]; 

    } else if (context == OtherContext) { 

        [self doOtherThing]; 

    } else { 

        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 

    }  

}

[_tableView removeObserver:self forKeyPath:@"contentSize"context:ClassNameTableViewContentSizeContext]; 

KVO实现原理

基本的原理:

当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

深入剖析

Apple 使用了 runtime来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。

NSKVONotifying_A 类剖析:在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_A 类,来实现当前类属性值改变的监听;

所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类,就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A 的中间类,并指向这个中间类了。

(isa指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象了) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。

—>我猜,这也是 KVO 回调机制,为什么都俗称KVO技术为黑魔法的原因之一吧:内部神秘、外观简洁。

子类setter方法剖析:KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用两个方法:被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。

参考文章:

Khanlou | KVO Considered Harmful

iOS开发 -- KVO的实现原理与具体应用 -

你可能感兴趣的:(iOS Object-C KVO的正确使用方法及实现原理分析)