一、前言
ViewBinding,即视图绑定,是Android Studio 3.6推出的新特性,旨在替代findViewById(其实内部实现还是使用findViewById),ViewBinding使用简单,并且可以有效减少项目中大量的样板代码,好处多多,官方教程讲的很详细了,这里就不细说了。
委托是一种设计模式,在Kotlin中使用by关键字更好的支持委托模式,这里也不细说,想要了解更多的同学可以看这篇文章,搞懂Kotlin委托。
这里贴出来官方教程中ViewBinding在Fragment中是如何使用的。
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 重置
}
可以看到,在onDestroyView函数中(也就是view销毁时)把_binding实例重置为null。这是因为View的生命周期与Fragment的生命周期不一致,当关闭Fragment时,会调用onDestroyView函数,View会被detached,而_binding实例作为Fragment的成员变量,如果不在onDestroyView函数重置为null,ViewBinding引用的资源也就不会被释放,这样当页面重新生成时,就会出现view并不是当前的页面的问题。
二、结合Kotlin委托实现
前面说了,在Fragment中使用ViewBinding时必须要在onDestroyView函数释放资源,这部分属于样板代码,每次在Fragment中使用binding时都需要写,麻烦且不优雅,所以如果有个方法能自动释放binding就好了。
自动释放binding的方案当然有,网上比较推荐的方案是结合委托(by关键字)释放binding资源。我们项目中也是采取这种方案,该方案实现方式如下。
class AutoClearedValue<T : Any>(val fragment: Fragment) : ReadWriteProperty<Fragment, T> {
private var _value: T? = null
init {
fragment.lifecycleScope.launch(Dispatchers.Main.immediate) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
// 添加viewLifecycle的监听
viewLifecycleOwner?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
// 在Fragment#onDestoryView()函数中释放binding实例。
_value = null
}
})
}
}
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return _value ?: throw IllegalStateException(
"Accessing the AutoClearedValue after it has been nulled out"
)
}
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
_value = value
}
}
/**
* Creates an [AutoClearedValue] associated with this fragment.
*/
fun <T : Any> Fragment.autoCleared() = AutoClearedValue<T>(this)
这里通过Fragment的viewLifeCycle来监听view的生命周期,在viewLifecycler.onDestroy生命周期时,将_value设为null,释放委派类的binding实例。
同时还提供了一个Fragment的扩展函数autoCleared(),方便进行委托。
使用起来也很简单,如下:
class TestFragment : Fragment(R.layout.fragment_Test) {
private var binding: FragmentTestBinding by autoCleared()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentSearchBinding.bind(view)
...
}
}
这里首先把binging实例委托给了Fragment的扩展函数autoCleared(),然后在onViewCreated中对binding进行赋值。
三、问题
有了上面的委托类,我们就可以在Fragment中愉快的使用ViewBinding而不需要手动进行重置。
这时来了一个需求,为了完成这个需求,我需要设置view的ViewTreeObserver来观察视图的变化,大家都知道,设置ViewTree监听后需要在onDestoryView生命周期中调用remove函数把监听清理掉,于是我就在Fragment#onDestroyView中调用binding进行清理。代码如下:
private val onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentTest.bind(view)
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
}
override fun onDestroyView() {
super.onDestroyView()
binding.root.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener)
}
运行后却出现空指针异常。这是为什么呢?
四、原因分析
既然在fragment其他生命周期中使用binding没有问题,仅在Fragment#onDestroyView或onDestroy后使用产生了空指针崩溃,那么就说明在某个地方binding实例被重置为了null,而重置null的操作只在委托类的viewLifecycler#onDestroy中有设置。所以我们可以合理的怀疑,在Fragment#onDestroyView或onDestroy生命周期被回调前,viewLifecycler#onDestroy就已经被回调。
这里可以查看Fragment的源码验证一下前面的怀疑。
//Fragment.java
void performDestroyView() {
mChildFragmentManager.dispatchDestroyView();
if (mView != null && mViewLifecycleOwner.getLifecycle().getCurrentState()
.isAtLeast(Lifecycle.State.CREATED)) {
// ViewLifecycler回调
mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}
……
onDestroyView(); // onDestoryView回调
……
}
可以看到,调用的顺序是Fragment.performDestroyView->viewLifecycle.onDestory->Fragment.onDestoryView(),也就是说在Fragment.onDestoryView被回调前,viewLifecycle.onDestory已经被回调了,在这里binding被重置为了null。在Fragment.onDestoryView()中再使用binding自然就会报错。
五、解决
很多情况下,我们需要在onDestoryView生命周期中执行一些清理操作,例如对ViewTree监听的清理等等,所以这里就要想办法处理掉这个空指针异常。
方案一
比较粗暴,既然这个问题是ViewBinding结合Kotlin委托使用产生的,那么很简单的一个思路就是放弃使用其中的一个即可。
- 放弃使用ViewBinding,使用
findViewById,虽然findViewById一直在被吐槽难用,但是其出问题的概率相对其他方案来说无疑是少了很多。 - 放弃使用委托方案,按照官方的方法,在Fragment#onDestoryView函数中手动进行重置。不过每一个类都进行重置也不太美观,所以我们可以抽出一个父类 Fragment#onDestoryView来实现重置功能,需要使用ViewBinding的Fragment继承该父类,重写onDestoryView()方法,在该方法中实现完binding后调用
super进行重置。
方案二
方案一并没有解决问题,所以不推荐使用方案一
如方案一中的第2种方法的思路,可以使用完binding后再对其进行置空,也就是把置空放到onDestory后进行,顺着这个思路我们可以使用Handler#post进行置空操作,这样就可以保证置空在onDestory之后进行。完整代码如下
class AutoClearedValue<T : Any>(val fragment: Fragment) : ReadWriteProperty<Fragment, T> {
private var _value: T? = null
init {
fragment.lifecycleScope.launch(Dispatchers.Main.immediate) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
// 添加viewLifecycle的监听
viewLifecycleOwner?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
Handler(Looper.getMainLooper()).post {
_value = null
}
}
})
}
}
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return _value ?: throw IllegalStateException(
"Accessing the AutoClearedValue after it has been nulled out"
)
}
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
_value = value
}
}
/**
* Creates an [AutoClearedValue] associated with this fragment.
*/
fun <T : Any> Fragment.autoCleared() = AutoClearedValue<T>(this)
六、总结
其实这个问题本质是一个回调顺序的问题,所以使用Handler#post执行置空操作,达到“延迟”置空的目的,保证在onDestory中能够正常使用binding。