老调重弹:对kvo的封装思路

关于kvo,kvo能做什么?

kvo作为cocoa框架的重要特性之一,在底层框架中被大量使用。在特定的场合使用该特性往往能够带来难以想象的好处,让整个方案变得相当简洁和优雅。比如大名鼎鼎的下拉刷新的svpulltorefresh框架,其实现采用了category动态添加属性和kvo结合的方案,在egoRefresh框架的基础上获得了极大的改善,使调用者所要书写的代码量直接下降了一个量级。其中的奥秘在于通过kvo很好的处理了frame变化的问题,调用者不用再处理frame相关的代码,仅需要聚焦下拉刷新带来的业务逻辑事件本身。此后所有流行的下拉刷新框架无一例外的都采用了此方案。仅仅想象一下kvo本身的灵活性、广泛的适用性而言,其强大毋庸置疑;但是如何把它用好,则是一个很值得研究的课题。

直接使用kvo的麻烦?

对于kvo来说,我认为他最大的坑在于生命周期的管理。举个例子一个对象A一旦发起了对别的对象属性的观察以后,在A对象释放之前,必须要手动调用解除监听的代码;否则在该对象属性变化时,则会引起crash。(麻烦1)
其实delegate以前也有类似的问题,但在iOS的演进过程中这个问题被解决了。解决方案是这样:在ARC以后delegate应该被设置成weak属性,这样一来当代理者被释放时,雇主的delegate属性是weak的情形中,这个delegate属性会被自动置为空,向一个空对象发消息,不会有任何问题。但是kvo的特性比较古老,苹果也未对其实现做任何改进,因此生命周期的管理对于合理的使用kvo非常重要。
在另外一种场景下的使用也会crash:当一个对象A在没有发起观察之前就调用解除监听的代码时。所以很多人在调用解除监听的代码时都会引入try-catch来捕获这个错误,从而增加程序的健壮性。这也是一个不得已而为之的无奈之举。(麻烦2)
其他: kvo的调用代码本身略显繁琐,所有的kvo处理代码都被指定在一个方法内部,这样在监听多个对象时会导致该方法内部相当混乱,且该处理方法和调用方法一样,拥有4个参数,其中2个参数几乎可以说是没有作用(一般会被固定填写相同的值); 其中对于属性字段传递的参数是通过字符串来表示的,如果拼写错误也容易带来问题且难以检查(编译器不会告警)。这也是经常为人吐槽或者诟病的地方;人们总希望api清晰简洁,如果能够支持像block这样的现代语法则更棒。(麻烦3)

kvo的封装思路

如上,我们认为kvo对于我们的技术链来说是一个不可或缺的环节;但是直接使用它的话,又有一些麻烦需要解决。所以,我们需要对它进行封装,封装的目的就是为了解决上面所列举的麻烦。
解决麻烦1:封装一个对象(我们将它看作是一个专门处理kvo的代理),专门用来管理属性的监听事件;此对象和一次监听动作唯一绑定;在该代理的dealloc里面我们会自动的调用解注册的方法。
解决麻烦2:在此前封装的代理对象中,我们会记录在此对象中的监听事件,如此一来我们自动调用的解注册方法和对外提供的解注册方法都能够保证安全(监听和解监听是一一对应的关系,且在时序上是监听在前,解监听在后)。
解决麻烦3: 封装api方法,支持block或target action的操作,并去除冗余参数。实际需要监听kvo的对象持有上面封装的代理对象,并从代理对象转发回调block或sel方法到实际需要监听kvo的对象。
这样一来我们就完成了一个简单的封装。我们还可以通过category和一些runtime技术的结合,来使得我们的api更简洁,使用更方便;这里就不再展开了。

未完待续

我们的封装示例代码和demo即将被开源到github.敬请期待。

参考和致谢

http://www.cocoachina.com/industry/20131106/7303.html
http://nshipster.com/key-value-observing/
https://github.com/th-in-gs/THObserversAndBinders
https://github.com/ibireme/YYKit/blob/master/YYKit/Base/Foundation/NSObject%2BYYAddForKVO.h
本文的写作参考了上述的文章和开源代码,也在此对其作者们表达感谢。

你可能感兴趣的:(cocoa,Runtime,weak,KVO,坑)