背景
前一阵子我们在使用viewbinding的委托的时候碰到了点crash问题,然后发现了一个比较有意思的解决方案,就和大家展开聊聊。
另外一点就是我后面打算将kotlin extensions这个插件统一移除掉。
估计大家应该对viewbinding的委托应该都有一定的了解,好几个大佬分享过类似的文章,但是大佬们的代码貌似也有一阵子都没有维护了,所以我找到了一个外国大佬写的仓库,其实应该算是一个相对来说比较稳定的库了,而且也一直处于一个持续更新迭代的状态。
仓库地址 viewbindingpropertydelegate
从crash到有意思的源码
委托模式是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。
kotlin 直接支持委托模式,更加优雅,简洁。kotlin 通过关键字 by 实现委托。
上述是kotlin对于委托的释义,viewbinding委托就是把生成viewbinding实例的过程交给委托类去完成,然后让使用方可以忽略掉其中的细节,是一种非常好玩的模式了。
但是由于viewbinding的特殊性,它其实就会和当前的lifecycle绑定在一起。因为我们要在销毁的情况下把实例重置为空。否则当我们页面重新生成的情况下,就会出现view并不是当前的页面的困扰。
作者在定义的时候就将viewbinding委托获取的实例定义为了非空,这里我和我的同事其实是存在一些分歧的,我认为非空其实挺合理的,但是对方并不认为。
恰巧这种空非空的问题,在实际的使用中就出现了很多不可预期的crash问题。比如说在一个异步操作中获取viewbinding实例然后进行赋值操作,就会出现空指针异常。另外由于使用的是lifecycle的页面销毁方法,如果我们复写了销毁方法之后在设置这个值,也会出现崩溃问题。
上述问题我在几个我之前参考的库中其实都发现了对应的问题。我参考了binding,还有之前彭旭说的那个也有类似的情况。
另外在fragment中,其实问题尤其的明显。因为我们很多时候使用的fragment相关的lifecycleowner是fragment本身,但是android官方其实推荐我们使用的是fragment内部的view相关的lifecycleowner。因为fragment相比较于activity,存在的问题就是多了几个生命周期,比如createview,和ondestroyview。其中出现最多问题的也就是ondestroyview和ondestroy。
有趣的代码
接下来我们看下这个作者是如何解决这些奇奇怪怪的问题的哦。
private class fragmentviewbindingproperty<in f : fragment, out t : viewbinding>( private val viewneedinitialization: boolean, viewbinder: (f) -> t, onviewdestroyed: (t) -> unit, ) : lifecycleviewbindingproperty<f, t>(viewbinder, onviewdestroyed) { private var fragmentlifecyclecallbacks: fragmentmanager.fragmentlifecyclecallbacks? = null private var fragmentmanager: reference<fragmentmanager>? = null // 赋值操作 override fun getvalue(thisref: f, property: kproperty<*>): t { val viewbinding = super.getvalue(thisref, property) registerfragmentlifecyclecallbacksifneeded(thisref) return viewbinding } private fun registerfragmentlifecyclecallbacksifneeded(fragment: fragment) { if (fragmentlifecyclecallbacks != null) return val fragmentmanager = fragment.parentfragmentmanager.also { fm -> this.fragmentmanager = weakreference(fm) } fragmentlifecyclecallbacks = clearondestroy(fragment).also { callbacks -> fragmentmanager.registerfragmentlifecyclecallbacks(callbacks, false) } } override fun isviewinitialized(thisref: f): boolean { if (!viewneedinitialization) return true if (thisref !is dialogfragment) { return thisref.view != null } else { return super.isviewinitialized(thisref) } } override fun clear() { super.clear() fragmentmanager?.get()?.let { fragmentmanager -> fragmentlifecyclecallbacks?.let(fragmentmanager::unregisterfragmentlifecyclecallbacks) } fragmentmanager = null fragmentlifecyclecallbacks = null } override fun getlifecycleowner(thisref: f): lifecycleowner { try { return thisref.viewlifecycleowner } catch (ignored: illegalstateexception) { error("fragment doesn't have view associated with it or the view has been destroyed") } } // 有意思的代码 private inner class clearondestroy( fragment: fragment ) : fragmentmanager.fragmentlifecyclecallbacks() { private var fragment: reference<fragment> = weakreference(fragment) override fun onfragmentdestroyed(fm: fragmentmanager, f: fragment) { // fix for destroying view for case with issue of navigation if (fragment.get() === f) { postclear() } } } }
从上述代码上我们可以看出来,其中获取的lifecycleowner就是我上文说的viewlifecycleowner。这个就其实已经非常精彩了。
另外我们可以看下他在内部定义了clearondestroy这个类,然后当onfragmentdestroyed触发的时候调用postclear方法。而这个方法就是解决当我们在destroyed中还执行了viewbinding内的对象的操作的空指针问题。
经典面试题的真实使用场景,handler.post执行。很多人觉得handler相关的面试题都是八股文,这次我们就通过这个真是场景来给大家说说这个有意思的问题。
首先从onfragmentdestroyed方法会执行在fragment本身的ondestroyview之前,原来我们会在这个方法下执行引用清空的操作。然后当ondestroyview执行的时候就会出现空指针异常了。那么lifecycle有没有提供一个在ondestroyview之后的方法呢?我们是不是可以考虑自己造一个呢?面试中,我们知道所有生命周期方法都是有主线程handler来负责调度的,这也就是说活我么可以把生命周期方法认为就是一个message,当onfragmentdestroyed执行的时候,ondestroyview也已经被添加到主线程的messagequeue中,这个时候我们在post一个runnable,那么他的排序规则上来说,就必然在ondestroyview之后了。
另外一些有意思的地方
这个库另外一个优点就是他同时支持反射和非反射的写法。同时也支持了activity,fragment,view,fragmentdialog,viewholder等等。反射写法是基于非反射写法的,所以也保证了底层库的一致性。
//非反射写法 private val viewbinding by viewbinding(viewprofilebinding::bind) //反射写法 private val viewbinding: itemprofilebinding by viewbinding()
同时他的反射相关的混淆配置文件也非常有意思。
allowoptimization 指定对象可能会被优化,即使他们被keep选项保留。所指定对象可能会被改变(优化步骤),但可能不会被混淆或者删除。该修饰符只对实现异常要求有用。
-keep,allowoptimization class * implements androidx.viewbinding.viewbinding { public static *** bind(android.view.view); public static *** inflate(...); }
它只会keep实现了viewbinding的类的bind和inflate方法。因为viewbinding会将所有的xml转化成一个类实例,如果不删除掉没有实际被调用的类的情况下就会导致dex包变大,大家对于包体积优化都是有追求的吗。然后用了-keep,allowoptimization,这样在混淆的代码优化过程中就可以删除掉没有被调用的viewbinding类了。
结尾
本次内卷到此结束。但是又是一个老生常谈的话题,一个开源库还是要持续的进行迭代和解决问题才能持续变好,而不是一次性的工作。拥抱变化的代码世界,解决一些奇奇怪怪的问题,都是挺好玩的,更多关于android开发viewbinding委托的资料请关注其它相关文章!