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

往期回顾专栏目录更新动态优惠政策版权须知

温馨提示:如果这是第一次接触《重学安卓》,可通过上述链接来访问和快速了解《重学安卓》专栏、获取它的目录、试读内容,以及了解它的最新动态 和 发展状况。

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

·

注:本文以

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

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

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

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

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

前言

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

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

由于了解 Jetpack 乃至爬坑 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 & Maven 依赖
  • 综上
  • Note 2020.07.21 加餐:
    • UnPeekLiveData 增加一层 ProtectedUnPeekLiveData 的设计缘由解析
  • 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
  • Note 2021.08.10 加餐:
    • 分享一份 v6.1 源码简化版,方便对核心 “防倒灌” 机制的理解
  • Note 2021.08.12 加餐:
    • 是内存管理效率进一步优化的 UnPeekLiveData v7.0
  • Note 2021.08.21 加餐:
    • 分享一份 v7.2 源码完整版,是简练完整的防倒灌设计

“数据倒灌” 的由来

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

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

为什么会有数据倒灌

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

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

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

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

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

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

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

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

Note 2021.8.16:重要提示,有言在先:

如想更好地理解和加深印象,可自行基于原生的 LiveData 来编写 demo 和复原灾难现场。

(最简单的办法是 修改 UnPeekLiveData 开源项目,在 sample module 下的 SharedViewModel 中,将 ProtectedUnPeekLiveData 改为 LiveData、将 UnPeekLiveData 改为 MutableLiveData,然后照着下面的例子跑一遍)

教程可以明确告诉你哪里有坑,但 凡事唯有从 0 到 1 完整踩过一遍坑,才会真的有体会

👆 👆 👆 划重点

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

为了让 编辑的结果在详情页中体现,所以在保存的同时,我们会从编辑页通过 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 没有本质区别,应尽可能避免使用。

top Created with Sketch.