9f330a9d0545a708d4a13ccf365de219
重学安卓:LiveData 数据倒灌 背景缘由全貌 独家解析

温馨提示:如果这是第一次接触《重学安卓》,可借助 这份在 GitBook 上维护的 “导读” 来快速了解《重学安卓》专栏、获取它的目录、试读内容,以及了解它的最新动态 和 发展状况。

截至目前,专栏已对 体系化文章 做了 1730 余次修订,数十位群友告诉我 受专栏的启发 他们也开启了写作之路。群里不定期会有小伙伴讨论适配问题、分享原创的开源库 和 提供内推机会,订阅后可随时进群交流。

·

注:本文以

《重学安卓:为你还原一个真实的 Jetpack Lifecycle》

《重学安卓:LiveData 鲜为人知的 身世背景 和 独特使命》

《重学安卓:有了 Jetpack ViewModel 真的可以为所欲为》

《重学安卓:是让人耳目一新的 Jetpack MVVM 精讲》 以及

《GitHub:Jetpack MVVM Best Practice》 作为前置知识,假设小伙伴已完整查阅并吸收了上述内容。

前言

很高兴见到你!

上周有位在字节工作的小伙伴私下感叹:“文章相见恨晚,恨晚呐”。

据该小伙伴描述,他早在 2017 年就开始在项目中尝试 AAC(Android Architecture Components),原以为好日子即将开始,没想到被 LiveData 的 “数据倒灌” 问题折腾得怀疑人生

是网上关于这方面的资料太少吗?不是的,与之无关。多数开发者自学一门新技术 都是首选官方文档,然而这一次的问题的根源就在于,官方文档 亲自推荐通过 SharedViewModel + LiveData 的组合来解决页面通信的问题,却在相当一段长的时间里 都没有亲自验证过并解决 该方案在场景下存在的致命缺陷

—— 架构组件的存在,本是为了规避不可预期的错误,没想到却因为这个 徒添了不可预期的错误。

姗姗来迟的笼统描述

与此类现象相关的解决方案,最早是在 2018 年中旬才在网上陆续刊登。

比如国外的:

LiveData with SnackBar, Navigation and other events

以及国内美团的:

Android消息总线的演进之路:用LiveDataBus替代RxBus、EventBus

但无论哪一种,都没有就此现象的背景做过 “因地制宜” 的概括,而只是笼统地用 “粘性事件” 来表达,也因此多数新上手 Jetpack 架构组件的小伙伴 根本无从理解这个 “粘性事件” 到底意味着什么,更不曾料想到 当下正困扰着自己的,就是这个

因此,本文的目标即是 提供对 “数据倒灌” 现象的 独家概括、缘由分析,以及趋近完美的解决方案

看完后,如你对 “数据倒灌” 这个概念有了印象、未来在遇到类似状况时能想起、并重新翻出和回溯这篇文章,那我的愿望也就达到了。

文章目录一览

  • 前言
  • 姗姗来迟的笼统描述
  • “数据倒灌” 的由来
  • 为什么会有数据倒灌
    • Note:2020.07.17 加餐:
    • 图文解析数据倒灌发生的场景
  • 为何非要通过 ViewModel + LiveData 来处理页面通信?
  • 为什么通过全局 ViewModel 而不是 静态单例?
  • 为什么不用 LiveDataBus?
  • 最新的 Result API 如何?
  • Event 事件包装器、反射方式、SingleLiveEvent 分别存在什么问题?
  • UnPeekLiveData 是如何解决这些问题的?
  • GitHub & JCenter 依赖
  • 综上
  • Note 2020.07.21 加餐:
    • UnPeekLiveData 增加一层 ProtectedUnPeekLiveData 的设计缘由解析
  • Note 2020.08.01 加餐:
    • UnPeekLiveData 屏蔽 observeForever 的缘由
  • Note 2020.10.15 加餐:
    • 升级到 UnPeekLiveData v4.0 享用 “更快更稳” 的防倒灌机制
  • Note 2021.04.22 加餐:
    • 升级到 UnPeekLiveData v5.0 获得更好的内存性能和更友好的 API 访问
  • Note 2021.06.18 加餐:
    • 是代码复杂度更小的 UnPeekLiveData v6.0 设计
  • Note 2021.07.19 加餐:
    • 是代码复杂度进一步减小的 UnPeekLiveData v6.1

“数据倒灌” 的由来

事实上,“数据倒灌” 一词,是我为了方便理解和记忆 “页面在 ‘二进宫’ 时收到旧数据推送” 的情况,而在 2019 年 自创并在网上传播的 对此类现象的概括

遗憾的是,官方文档 至今 (截至 2020.7.15) 都没有意识到 并就该致命问题做出警告。

为什么会有数据倒灌

前言中已经提到,新上手的小伙伴们依赖官方文档,而官方文档给出的 “页面间通信” 场景的解决方案 存在致命缺陷 ——

一方面,用于通信的 LiveData 是被托管在 Activity / Application 级作用域的 SharedViewModel 中,于是 LiveData 的生存期长于任何一个 Fragment(假设通信双方是 Fragment):当二级 Fragment 出栈时,LiveData 实例仍存在

另一方面,LiveData 本身是被设计为粘性事件的,也即,一旦 LiveData 中持有数据,那么在观察者订阅该 LiveData 时,会被推送最后一次数据。

这样的设定本身也符合 LiveData 的常用场景 —— 比如 在页面重建时,自动推送最后一次数据,而不必重新去向后台请求

只不过,这个特性在 “页面间通信” 场景下,就是致命的灾难 ——

新上手的小伙伴完全意料不到,还会有这样 “预期外” 的状况在等着自己,于是成就了读者群 “高频提问和解答” TOP 2。

不知道我这样说,有没把话说明白?——

我举一个存在 “数据倒灌” 隐患的例子:

假设 此处我们有 列表、详情、编辑 三个页面,返回栈中已经到达了三级页面 “编辑页”,编辑页的作用是编辑内容 并保存和返回到二级页面。

为了让 编辑的结果在详情页中体现,所以在保存的同时,我们会从编辑页通过 SharedViewModel 中的 MutableLiveData 去给详情页发消息,通知其刷新界面。

此时用户回到详情页,觉得内容 ok,便进一步回退到一级页面,即列表页,

那么接下来,用户在列表页中 选择一个 item 点击,并再次跳到详情页时,就会因为注册 “已实例化过、并携带有旧数据” 的 MutableLiveData,而收到它的 “不符合预期” 的推送

这种 “粘性设计” 的存在意义 刚刚上文已经提到了,很显然,它在当下这种场景下会带来致命的灾难。

为何非要通过 ViewModel + LiveData 来处理页面通信?

关于 页面通信的场景,我们能拼凑出的背景主要有以下几块:

  • 事件的观察者是页面,但页面可能非激活状态、甚至处于被销毁状态,那么此时在消息回调中接触页面成员时,存在生命周期安全的隐患
  • 事件的观察者必须是页面,对页面的消息推送,不能让其他地方 能够监听和收到 不可预期的推送
  • 事件的发送者必须来自唯一可信源,如此在 “多页面状态同步” 等场景下,能够因 “数据总是来自唯一可信源的统一决策和分发”,而 确保了状态同步的一致性和可靠性

而 ViewModel + LiveData 的组合,无疑是这种现状下 超低成本 解决上述 3 大问题的最优解,因而 问题也就进一步地转化为 解决 LiveData 的 “数据倒灌” 问题

为什么通过全局 ViewModel 而不是 静态单例?

这个问题是过去 1 年读者群里问到最多的问题,被排在 “高频提问和解答” 的 TOP 1。

原因很简单,因为:

1.ViewModel 是可以声明为页面的私有成员来使用,如此可以 将 “页面通信” 的消息限制在 “仅在视图控制器之间传播”,避免污染到外部

2.同时也可 避免 ViewModel 持有的 LiveData 被外部组件拿到,造成不可预期的推送

为什么不用 LiveDataBus?

这个问题在 “高频提问和解答” 的排位是 TOP 3。

不在 “页面通信” 的场景下使用 LiveDataBus 是因为,我们是以 “多人协作、页面繁杂的软件工程” 为背景来谈论架构设计。在这样的背景下,任何微不足道的隐患,都可能被无限放大。

Bus 自身 缺乏唯一可信源的理念约束、仅仅只是消息的中转站、无法做到 “读写分离(访问权限控制 和 内部统一决策),乃至难以确保 “状态同步的一致性” 和 “消息的可靠性”,应彻底从项目中移除,以免团队新手的误用乃至滥用。

具体缘由可参考 《LiveData 鲜为人知的 身世背景 和 独特使命》 中关于 “唯一可信源” 的介绍。

最新的 Result API 如何?

事实上,Result API 和 Bus 没有本质区别,应尽可能避免使用。

Event 事件包装器、反射方式、SingleLiveEvent 分别存在什么问题?

《Jetpack MVVM 精讲》中我分别提到了 Event 事件包装器、反射方式、SingleLiveEvent 这三种方式来解决 “数据倒灌” 的问题。它们分别来自上文我们提到的外网美团的文章,和官方最新 demo

然而它们分别存在如下问题:

Event 事件包装器:

  • 对于多观察者的情况,只允许第一个观察者消费,这不符合现实需求;
  • 而且手写 Event 事件包装器,在 Java 中存在 null 安全的一致性问题。
top Created with Sketch.