iOS-知识梳理(观察者模式-KVO、NSNotification的实现原理.KVC原理)

观察者模式的定义:一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。


KVO基于runtime实现,当你观察一个对象的时候,一个新类被动态创建继承于被观察对象的类,并重写所被观察属性的setter方法,并在赋值语句前后分别加上valueWillChange:和 valueDidChange方法和响应通知,最后把被观察对象的isa指针指向了这个新类。(关于isa指针的问题可以了解下iOS-知识梳理(类探究、isa)

虽然修改了实例对象的isa指针指向,但是调用class 方法的时候依旧返回的之前的类信息

+ class是保存在之前类的元类里,应该是不会变的 肯定是返回之前的类。

- class 按理说isa指向了新类,应该是调用了新类methodList的class,但是返回的却是父类的Class,所以我猜测这里应该是对class方法没有进行重写直接调用到了父类里面(没有考证)。

看上去没有什么问题,但是好多大牛一直在吐槽KVO,为啥子嘞?

1,

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

    if(object == someObj && [keyPath isEqualToString:@"keypath"]) {

    [self doSomeThing];

}

上面是我们用kvo时的回调方式,当我们添加好多observer时这里的代码将会非常长,极不优雅。

2,keyPath 必须是NSString, 很容易写错,编译的时候并不能找出这个错误。

3,需要自己处理superClass 里面的observe,

这里稍微解释一下什么意思,举个例子:

A类里有个scrollview,在A类里对scrollview的 contentOffset进行观察,通常我们还需要在dealloc里面 removeObserver.

现在B继承于A,也对scrollView进行了contentOffset观察,为了保证父类的方法能被执行到我们必须这么写

if(object == obj && [keyPath isEqualToString:@"contentOffset"]) {

    [self doSomeThing];

}else{

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

麻烦不?

并且我们会在B类里的dealloc里面removeObserver。

dealloc是在A类和B类都要调用的 也就是remove了两次, 闪退了。。。

4,还有人在使用的时候吐槽 change, 和 context 这两个参数

首先看change:

NSKeyValueObservingOptionNew: 指示change字典中包含新属性值;

NSKeyValueObservingOptionOld: 指示change字典中包含旧属性值;

NSKeyValueObservingOptionInitial: 相对复杂一些,NSKeyValueObserving.h文件中有详细说明,此处略过;

NSKeyValueObservingOptionPrior: 相对复杂一些,NSKeyValueObserving.h文件中有详细说明,此处略过;

有没有觉得超级复杂。。。

再来想一下context,我就想问问用过kvo的小伙伴们都传过什么context,大家是不是都在用NULL?

其实是不对的,下面有正确的使用方法。

既然有这么多槽点,那么如何使用呢

首先keypath这个参数 我们可以用NSStringFromSelector来避免书写错误。

解决superClass的那个问题就得设置一个独一无二的context,回调的时候进行校验,当然会很麻烦。

再回头说一下观察者模式。。。

以NSNotification为例,试着自己实现一个通知中心。大概想一下的它的实现思路:

首先要创建个单例

添加观察者的时候 要保存对象、方法,并且与Name关联,应该是把target-acttion放在数组中 然后以name为key放入字典中。

PostName的时候 寻找name对应的target-action数组,遍历执行。。。(当然中间还有带参数的处理)

现在抛出一个问题,既然是数组和字典存储那肯定是强引用,那么如何保证单例对观察者的引用是弱引用呢(如果不是弱引用观察者永远不释放)?或者如果不是这种实现方式,还有别的好办法吗?

新建一个对象,对象里面添加 property(weak) id target(观察者); 保存方法 及参数。然后将该对象放入数组当中。这样就实现了弱引用target。

PostName的时候遍历执行的时候,如果该对象的target为nil,说明观察者已经释放了,此时将该对象移除数组,也就省了iOS9之前添加观察者 在观察者dealloc的时候还要removeObserver。iOS9之前使用的unsafe_unretained修饰的target,所以释放的时候要求removeObserver,要不然会造成野指针。。。

以上为猜测,如果有错误或者好的方案请大神指点一下,感兴趣的也可以自己实现一下(系统的api还有block的使用,也可以试一下)。

这种方案我在项目实现一个白天夜景的切换的时候封装一个category时使用过,使用起来很方便。(ios-白天夜景切换方案

最后说一下KVC的实现原理

首先查找是否实现了setter方法吗,如果有,优先调用完成赋值。

如果没有setter方法,调用 accessInstanceVariablesDirectly询问,如果返回的YES,则顺序匹配变量名与 _<key>,_is<Key>,<key>,is<Key>,匹配到则设定其值。如果返回NO,结束查找。并调用 setValue:forUndefinedKey:报异常

如果既没有setter也没有实例变量,调用 setValue:forUndefinedKey:。

猜你喜欢

转载自blog.csdn.net/evol_f/article/details/82775896