Fragment使用ViewBinding+Kotlin委托导致的崩溃问题

1,911 阅读5分钟

一、前言

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#onDestroyViewonDestroy后使用产生了空指针崩溃,那么就说明在某个地方binding实例被重置为了null,而重置null的操作只在委托类的viewLifecycler#onDestroy中有设置。所以我们可以合理的怀疑,在Fragment#onDestroyViewonDestroy生命周期被回调前,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委托使用产生的,那么很简单的一个思路就是放弃使用其中的一个即可。

  1. 放弃使用ViewBinding,使用findViewById,虽然findViewById一直在被吐槽难用,但是其出问题的概率相对其他方案来说无疑是少了很多。
  2. 放弃使用委托方案,按照官方的方法,在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。