美文网首页
Jetpack MVVM七宗罪之一:拿Fragment当Life

Jetpack MVVM七宗罪之一:拿Fragment当Life

作者: 木木玩Android | 来源:发表于2022-01-10 15:42 被阅读0次

    首先承认这个系列有点标题党,Jetpack 的 MVVM 本身没有错,错在开发者的某些使用不当。本系列将分享那些 AAC 中常见的错误用法,指导大家打造更健康的应用架构

    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
    复制代码
    

    原因分析

    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

    解决方法

    明白了问题原因,解决思路也就清楚了:必须要保证订阅的时机和所使用的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);
            }
        }
    复制代码
    

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

    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

    最后: 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(...), 这跟本文无关,但是将涉及到第二宗罪的剧情,敬请期待。

    技术学习分享


    最后的话做一个Android架构师的进阶学习路线及笔记资料分享,希望能帮助到有需要的朋友。

    • 架构师筑基必备技能

    目前Android APP开发主流语言就是Java语言,Java语言最大的特性就是提高了软件的交互可能性,可以说安卓手机几乎所有应用程序都是利用Java语言来进行编写的。

    知识要点:
    1、深入理解Java泛型
    2、注解深入浅出
    3、并发编程
    4、数据传输与序列化
    5、Java虚拟机原理
    6、高效IO

    架构师筑基必备技能
    • 设计思想解读开源框架

    随着互联网企业的不断发展,产品项目中的模块越来越多,用户体验要求也越来越高,想实现小步快跑、快速迭代的目的越来越难,插件化技术应用而生。如果没有插件化技术,美团、淘宝这些集成了大量“app”的应用,可能会有几个g那么大。

    所以,当今的Android移动开发,不会热修复、插件化、组件化,80%以上的面试都过不了。

    知识要点:
    1、热修复设计
    2、插件化框架设计
    3、组件化框架设计
    4、图片加载框架
    5、网络访问框架设计
    6、RXJava响应式编程框架设计

    设计思想解读开源框架
    • 360°全方位性能调优

    在不同层次的开发工程师手里,因为技术水平的参差不齐,即使很多手机在跑分软件性能非常高,打开应用依然存在卡顿现象。

    另外,随着产品内容迭代,功能越来越复杂,UI页面也越来越丰富,也成为流畅运行的一种阻碍。综上所述,对APP进行性能优化已成为开发者该有的一种综合素质,也是开发者能够完成高质量应用程序作品的保证。

    1、设计思想与代码质量优化

    2、程序性能优化

    • 启动速度与执行效率优化
    • 布局检测与优化
    • 内存优化
    • 耗电优化
    • 网络传输与数据储存优化
    • APK大小优化

    3、开发效率优化

    • 分布式版本控制系统Git
    • 自动化构建系统Gradle

    4、项目实战

    • 启动速度
    • 流畅度
    • 抖音在APK包大小资源优化的实践
    • 优酷响应式布局技术全解析
    • 网络优化
    • 手机淘宝双十一性能优化项目揭秘
    • 高德APP全链路源码依赖分析
    • 彻底干掉OOM的实战经验分享
    • 微信Android终端内存优化实践
    360°全方位性能调优
    • Android框架体系架构

    Android框架体系架构(高级UI+FrameWork源码) 这块知识是现今使用者最多的,我们称之Android2013~2016年的技术。

    Android开发者也往往因为网上Copy代码习惯了而导致对这块经常“使用”的代码熟悉而又陌生:熟悉的是几乎天天在和它们打交道, 天天在复制这些代码 ;陌生的是虽然天天和这些代码打交道,但是并没有深入研究过这些代码的原理,代码深处的内涵。

    本篇知识要点:
    1、高级UI晋升
    2、Android内核组件
    3、大型项目必备IPC
    4、数据持久与序列化
    5、Framework内核解析

    Android框架体系架构
    • NDK模块开发(音视频系列)

    NDK(Native Development Kit缩写)一种基于原生程序接口的软件开发工具包,可以让您在 Android 应用中利用 C 和 C++ 代码的工具。通过此工具开发的程序直接在本地运行,而不是虚拟机。

    在Android中,NDK是一系列工具的集合,主要用于扩展Android SDK。NDK提供了一系列的工具可以帮助开发者快速的开发C或C++的动态库,并能自动将so和Java应用一起打包成apk。

    本篇知识要点:
    1、NDK开发之C/C++入门
    2、JNI模块开发
    3、Linux编程
    4、底层图片处理
    5、音视频开发
    6、机器学习

    NDK模块开发
    • Flutter学习进阶

    2019 年无疑是 Flutter 技术如火如荼发展的一年。

    每一个移动开发者都在为 Flutter 带来的“快速开发、富有表现力和灵活的 UI、原生性能”的特色和理念而痴狂,从超级 App 到独立应用,从纯 Flutter 到混合栈,开发者们在不同的场景下乐此不疲的探索和应用着 Flutter 技术,也在面临着各种各样不同的挑战。

    本篇知识要点:
    1、Flutter跨平台开发概述
    2、Windows中Flutter开发环境搭建
    3、编写你的第一个Flutter APP
    4、Flutter Dart语言系统入门

    ......

    Flutter学习
    • 微信小程序开发

    微信小程序作为现在比较火的编程开发应用场景之一,深受市场的青睐,这让不少开发者眼馋不已。但是对于初学者来说,就完全摸不着头脑了,不知道微信小程序开发制作需要学习那些知识,有需要的朋友可以参考本篇。

    本篇知识要点:
    1、小程序概述及入门
    2、小程序UI开发
    3、API操作
    4、购物商场项目实战

    微信小程序开发
    • Android相关源码解读
      只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
    Android相关源码解读
    Android相关源码解读部分内容

    由于篇幅原因,以上完整学习笔记pdf如有需要,可以点击这里免费获取

    相关文章

      网友评论

          本文标题:Jetpack MVVM七宗罪之一:拿Fragment当Life

          本文链接:https://www.haomeiwen.com/subject/siimcrtx.html