Bootstrap

Jetpack 易错分享:还在使用 Fragment 作为 LifecycleOwner ?

本系列将分享那些 AAC 中常见的错误用法,以帮助大家打造更健康的应用架构。

1

Fragment 作为 LifecycleOwner 的问题

MVVM 的核心是数据驱动UI,在 Jetpack 中,这一思想体现在以下场景:Fragment 通过订阅 ViewModel 中的 LiveData 以驱动自身 UI 的更新。

关于订阅的时机,一般会选择放到 onViewCreated 中进行,如下:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel.liveData.observe(this) { // Warning :Use fragment as the LifecycleOwner
           updateUI(it) 
        } 

}
 

我们知道订阅 LiveData 时需要传入 LifecycleOwner 以防止泄露,此时一个容易犯的错误是使用 Fragment 作为这个 LifecycleOwner,某些场景下会造成重复订阅的Bug。

做个实验如下:

val handler = Handler(Looper.getMainLooper())

class MyFragment1 : Fragment() {
    val data = MutableLiveData<Int>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        tv.setOnClickListener {
            parentFragmentManager.beginTransaction()
                .replace(R.id.container, MyFragment2())
                .addToBackStack(null)
                .commit()

            handler.post{ data.value = 1 }
        }

        data.observe(this, Observer {
            Log.e("fragment", "count: ${data.value}")
        })

}
 

当跳转到 MyFragment2 然后再返回 MyFragment1 中时,会打出输出两条log。

E/fragment: count: 1
E/fragment: count: 1

2

原因分析

LiveData 之所以能够防止泄露,是当 LifecycleOwner 生命周期走到 DESTROYED 的时候会 remove 调其关联的 Observer。

//LiveData.java

@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
   if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
          removeObserver(mObserver);
          return;
   }
   activeStateChanged(shouldBeActive());

}

前面例子中,基于 FragmentManager#replace 的页面跳转,使得 MyFragment1 发生了从 BackStack 的出栈/入栈,由于 Framgent 实例被复用并没有发生 onDestroy, 但是 Fragment的 View 的重建导致重新 onCreateView, 这使得 Observer 被 add 了两次,但是没有对应的 remove。

所以归其原因, 是由于 Fragment 的 Lifecycle 与 Fragment#mView 的 Lifecycle 不一致导致我们订阅 LiveData 的时机和所使用的 LivecycleOwner 不匹配,所以在任何基于 replace 进行页面切换的场景中,例如 ViewPager、Navigation 等会发生上述bug。

 

3

解决方法

明白了问题原因,解决思路也就清楚了:必须要保证订阅的时机和所使用的LifecycleOwner相匹配,即要么调整订阅时机,要么修改LifecycleOwner。

在 onCreate 中订阅

思路一是修改订阅时机,讲订阅提前到 onCreate, 可以保证与 onDestory 的成对出现,但不幸的是这会带来另一个问题。

当 Fragment 出入栈造成 View 重建时,我们需要重建后的 View 也能显示最新状态。但是由于 onCreate 中的订阅的 Observer 已经获取过 LiveData 的最新的 Value,如果 Value 没有新的变化是无法再次通知 Obsever 的。

在 LiveData 源码中体现在通知 Obsever 之前对 mLastVersion 的判断:

 
//LiveData.java

private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }

    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    if (observer.mLastVersion >= mVersion) {// Value已经处于最新的version
        return;
    }

    observer.mLastVersion = mVersion;
    //noinspection unchecked
    observer.mObserver.onChanged((T) mData);
}
 

正是为了保证重建后的 View 也能刷新最新的数据, 我们才在 onViewCreated 中完成订阅。因此只能考虑另一个思路,替换 LifecycleOwner。

使用 ViewLifecycleOwner

Support-28 或 AndroidX-1.0.0 起,Fragment 新增了 getViewLifecycleOwner 方法。顾名思义,它返回一个与 Fragment#mView 向匹配的 LifecycleOwner,可以在 onDestroyView 的时候走到 DESTROYED ,删除 onCreateView 中注册的 Observer, 保证了 add/remove 的成对出现。

 

看一下源码,原理非常简单。

//Fragment.java
void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
        //...

        mViewLifecycleOwner = new LifecycleOwner() {
            @Override
            public Lifecycle getLifecycle() {
                if (mViewLifecycleRegistry == null) {
                    mViewLifecycleRegistry = new LifecycleRegistry(mViewLifecycleOwner);
                }
                return mViewLifecycleRegistry;
            }
        };
        mViewLifecycleRegistry = null;
        mView = onCreateView(inflater, container, savedInstanceState);
        if (mView != null) {
            // Initialize the LifecycleRegistry if needed
            mViewLifecycleOwner.getLifecycle();
           // Then inform any Observers of the new LifecycleOwner
            mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner); //mViewLifecycleOwnerLiveData在后文介绍
        } else {
            //...
        }
    }
 

基于 mViewLifecycleRegistry 创建 mViewLifecycleOwner。

 @CallSuper
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {// called when onCreateView
    if (mView != null) {
        mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
    }
}


 @CallSuper
public void onDestroyView() {
    if (mView != null) {
        mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
    }
}
 

然后在 onCreateView 和 onDestroyView 时,推进到合适的生命周期。

getViewLifecycleOwnerLiveData

顺道提一下,与 getViewLifecycleOwner 同时新增的还有 getViewLifecycleOwnerLiveData。从前面贴的源码中对 mViewLifecycleOwnerLiveData 的使用,应该可以猜出它的作用:它是前文讨论的思路1的实现方案,即使在 onCreate 中订阅,由于在 onCreateView 中对 LiveData 进行了重新设置,所以重建后的 View 也可以更新数据。

 
// Then inform any Observers of the new LifecycleOwner
mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner);

需要特别注意的是,根据 MVVM 最佳实践,我们希望由 ViewModel 而不是 Fragment 持有 LiveData,所以不再推荐使用 getViewLifecycleOwnerLiveData。

4

最后:StateFlow 与 lifecycleScope

前面都是以 LiveData 为例介绍对 ViewLifecycleOwner 的使用, 如今大家也越来越多的开始使用协程的 StateFlow , 同样要注意不要错用 LifecycleOwner。

订阅 StateFlow 需要 CoroutineScope, AndroidX 提供了基于 LifecycleOwner 的扩展方法。

val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

当我们在 Fragment 中获取 lifecycleScope 时,切记要使用 ViewLifecycleOwner。

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //使用 viewLifecycleOwner 的 lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.someDataFlow.collect {
                    updateUI(it)
                }
            }
        }
    }
}
 

注意此处出现了一个 repeatOnLifecycle(...), 这跟本文无关

 

转自:https://mp.weixin.qq.com/s/_2YSV_JsjDJ7CuHJngMbqQ?st=03200109C2AA472F90A21097486B79B06EBBF9EF28A990A2C18DA718BED3E705612245742257C29C2F5FC334ED9B801D57C85A04D1F877ABCCB73B0A4100B4134F58C48BAE9A931975921251BAAB327767ACA19B427B7A8C84DC9B34872F1B95AB6E8D52305BB62908EC7100CEE693B6A83D06E4B9D13FFB849E6375C279770475974C02A9879BDE7A348F89CCB9B71AA6458CEA8B5A7D0E00367AF3EA4C80D583DED47051931E297200F800D973748C&vid=1688850467124544&cst=F43F20347AB9EA7EF740F3CFA831E54FB4FDF6CCC425B5824B7838FB57E62B22DEAC2A20FDE8AEE702B6F8347E651FA9&deviceid=39a61f78-0c4e-491f-ac13-49b6c78704e2&version=3.1.8.90238&platform=mac

;