浮躁的年代里得加个留住人的开头。
懒人直接看下面第五节,说废话前先上代码:
https://gitee.com/pkgogai/gydfoundation_public/tree/master/GYDFoundation/Code/Base/KVO
核心代码十多行,见页面底部,算是比较简单的写法。
第一节,初见KVO
KVO大概是这样的效果,给一个属性注册一个监听,然后当要修改这个属性时,会触发一个已经约定好的方法。
第二节,相识KVO
简单用代码验证下,大概了解到
- 普通的属性和成员变量都可以用KVO。
- 用点方法(setter方法)或者setValueForKey都可触发。
- 在修改属性的线程内触发。
至于一些极端情况,比如添加一个成员变量,手写个同名或不同名的setter方法,甚至放到类别里……可能一辈子也用不到的情况就不要理会了,真正用的时候验一下即可。
第三节,KVO的小脾气
从上面我们发现了KVO的与众不同之处
- 同一个key的触发的顺序并不是添加监听的顺序。因此使用时不要对添加顺序有依赖。
- 移除时即使不传context,也只是移除1个监听,而不是移除全部。
- 触发的方法名是固定的,也就是说子类会覆盖父类的方法,类别会覆盖本类的方法,用的时候必须考虑这些。
- 属性的修改并不一定在添加移除kvo的线程中进行,这意味着存在线程安全问题。
就因为这些,导致我不到迫不得已的情况下是不会使用KVO的。但总有迫不得已的时候,所以还是得安抚一下。
第四节,安抚KVO
- 顺序问题可以跳过,知道即可。如果我们有顺序要求,完全可以合在一起写,如果不需要合在一起写,通常也就不需要在乎顺序。
- 添加和移除必须一一对应,通常可以在init添加和dealloc移除,利用这类本身就是一一对应的方法。如果要对KVO呼之即来挥之即去,用到的时候添加,不用的时候移除,那就一定要记录好每一个KVO的添加状态。
- 同名方法才是最麻烦的,我们先考虑继承,为了确保区分父类和子类对同一个属性进行监听并执行各自处理,context一定要有区别,而context的类型是void *,也就是说系统推荐我们用一个地址来区分,例如用一个静态变量的地址。那么处理时,也需要按照context的值来区分是自身处理还是父类处理。再来考虑类别,为了确保类别中也可能复写处理方法,与继承类似,类别也需要我们使用context来区分由哪个类别中的方法来处理,与继承不同的是,继承一个super就可以交给父类的方法去处理,类别却不行,想要把所有类别复写的同一个方法都跑一遍也是一个麻烦事。最后,也是最难的,一个大项目,我们继承别人,也可能有别人继承我们,可能有别人给我们添加类别,或者我们给别人添加类别,有项目内部的,有第三方提供给我们的,也可能我们要提供给第三方。要保证每一个人的想法都一致,这是不可能实现的。
- 线程的问题,处理线程问题就必然伴随着性能浪费和复杂的代码,而大部分KVO场景都发生在UI线程,因此不处理线程安全问题也是合情合理的。至于我们,应该尽量把添加和移除监听的操作放在修改属性的线程内进行,即使不能也无需太担心,毕竟KVO只是迫不得已才使用,这种神级事故现场不是故意制造的话,可能直到苹果公司倒闭后都不会出现。总之忽略就好了。
第五节,为KVO找个一心一意的对象
其实一开始我是拒绝的,但总会有一些只适合用KVO的场景,所以我们要学会原谅,懂得接纳。
1和4可以不管,2和3是必须要解决的。
这2点脾气源自于一个观察者同时交往多个KVO,却使用同一个方法作为约会地点,一旦碰面,自然彼此闹脾气。所以最简单的解决思路就是不让KVO彼此碰面,一是为每一个KVO指定独立的方法作为约会地点,这点系统限制死了不好实现;二是为每一个KVO找一个独立的对象,这一点倒是很容易。
所以最简单的解决问题的方法来了,为每一个KVO找一个独立的对象,一心一意至死方休。
核心代码如下:
再补充一个方便调用的方法,见下面的用法
终章,用法
开始监听的同时创建GYDKeyValueObserver对象,对象被释放时移除监听。
如果作为成员变量的话,当self被释放,成员变量自然也会随之释放,监听也就被移除了。
注意:block会被GYDKeyValueObserver对象持有,所以不要循环引用了。