⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。
Android Jetpack 开发套件是 Google 推出的 Android 应用开发编程范式,为开发者提供了解决应用开发场景中通用的模式化问题的最佳实践,让开发者可将时间精力集中于真正重要的业务编码工作上。
这篇文章是 Android Jetpack 系列文章的第 2 篇文章,完整目录可以移步至文章末尾~
前言
- LiveData 是 Jetpack 组件中较常用的组件之一,曾经也是实现 MVVM 模式的标准组件之一,不过目前 Google 更多推荐使用 Kotlin Flow 来代替 LiveData;
- 虽然 LiveData 不再是 Google 主推的组件,但考虑到 LiveData 依然存在于大量存量代码中,以及 LiveData 伴随着 Android 生态发展过程中衍生的问题和解决方案,我认为 LiveData 依然有存在的意义。虽然我们不再优先使用 LiveData,但不代表学习 LiveData 没有价值。
- 认识 LiveData
1.1 为什么要使用 LiveData?
LiveData 是基于 Lifecycle 框架实现的生命周期感知型数据容器,能够让数据观察者更加安全地应对宿主(Activity / Fragment 等)生命周期变化,核心概括为 2 点:
- 1、自动取消订阅: 当宿主生命周期进入消亡(DESTROYED)状态时,LiveData 会自动移除观察者,避免内存泄漏;
- 2、安全地回调数据: 在宿主生命周期状态低于活跃状态(STAETED)时,LiveData 不会回调数据,避免产生空指针异常或不必要的性能损耗;当宿主生命周期不低于活跃状态(STAETED)时,LiveData 会重新尝试回调数据,确保观察者接收到最新的数据。
1.2 LiveData 的使用方法
- 1、添加依赖: 在 build.gradle 中添加 LiveData 依赖,需要注意区分过时的方式:
1 | kotlin复制代码// 过时方式(lifecycle-extensions 不再维护) |
- 2、模板代码: LiveData 通常会搭配 ViewModel 使用,以下为使用模板,相信大家都很熟悉了:
NameViewModel.kt
1 | kotlin复制代码class NameViewModel : ViewModel() { |
MainActivity.kt
1 | kotlin复制代码class MainActivity : AppCompatActivity() { |
- 3、注册观察者: LiveData 支持两种注册观察者的方式:
- LiveData#observe(LifecycleOwner, Observer) 带生命周期感知的注册: 更常用的注册方式,这种方式能够获得 LiveData 自动取消订阅和安全地回调数据的特性;
- LiveData#observeForever(Observer) 永久注册: LiveData 会一直持有观察者的引用,只要数据更新就会回调,因此这种方式必须在合适的时机手动移除观察者。
Observer.java
1 | csharp复制代码// 观察者接口 |
- 4、设置数据: LiveData 设置数据需要利用子类 MutableLiveData 提供的接口:setValue() 为同步设置数据,postValue() 为异步设置数据,内部将 post 到主线程再修改数据。
MutableLiveData.java
1 | scala复制代码public class MutableLiveData<T> extends LiveData<T> { |
1.3 LiveData 存在的局限
LiveData 是 Android 生态中一个的简单的生命周期感知型容器。简单即是它的优势,也是它的局限,当然这些局限性不应该算 LiveData 的缺点,因为 LiveData 的设计初衷就是一个简单的数据容器,需要具体问题具体分析。对于简单的数据流场景,使用 LiveData 完全没有问题。
- 1、LiveData 只能在主线程更新数据: 只能在主线程 setValue,即使 postValue 内部也是切换到主线程执行;
- 2、LiveData 数据重放问题: 注册新的订阅者,会重新收到 LiveData 存储的数据,这在有些情况下不符合预期(具体见第 TODO 节);
- 3、LiveData 不防抖问题: 重复 setValue 相同的值,订阅者会收到多次
onChanged()
回调(可以使用distinctUntilChanged()
优化); - 4、LiveData 丢失数据问题: 在数据生产速度 > 数据消费速度时,LiveData 无法观察者能够接收到全部数据。比如在子线程大量
postValue
数据但主线程消费跟不上时,中间就会有一部分数据被忽略。
1.4 LiveData 的替代者
- 1、RxJava: RxJava 是第三方组织 ReactiveX 开发的组件,Rx 是一个包括 Java、Go 等语言在内的多语言数据流框架。功能强大是它的优势,支持大量丰富的操作符,也支持线程切换和背压。然而 Rx 的学习门槛过高,对开发反而是一种新的负担,也会带来误用的风险。
- 2、Kotlin Flow: Kotlin Flow 是基于 Kotlin 协程基础能力搭建的一套数据流框架,从功能复杂性上看是介于 LiveData 和 RxJava 之间的解决方案。Kotlin Flow 拥有比 LiveData 更丰富的能力,但裁剪了 RxJava 大量复杂的操作符,做得更加精简。并且在 Kotlin 协程的加持下,Kotlin Flow 目前是 Google 主推的数据流框架。
关于 Kotlin Flow 的更多内容,我们在 4、Flow:LiveData 的替代方案 这篇文章讨论过。
- LiveData 实现原理分析
2.1 注册观察者的执行过程
LiveData 支持使用 observe() 或 observeForever() 两种方式注册观察者,其内部会分别包装为 2 种包装对象:
- 1、observe(): 将观察者包装为 LifecycleBoundObserver 对象,它是 Lifecycle 框架中
LifecycleEventObserver
的实现类,因此它可以绑定到宿主(参数 owner)的生命周期上,这是实现 LiveData 自动取消订阅和安全地回调数据的关键; - 2、observeForever(): 将观察者包装为 AlwaysActiveObserver,不会关联宿主生命周期,当然你也可以理解为全局生命周期。
注意: LiveData 内部会禁止一个观察者同时使用 observe() 和 observeForever() 两种注册方式。但同一个 LiveData 可以接收 observe() 和 observeForever() 两种观察者。
LiveData.java
1 | java复制代码private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>(); |
2.2 生命周期感知源码分析
LifecycleBoundObserver 是 LifecycleEventObserver
的实现类,当宿主生命周期变化时,会回调其中的 LifecycleEventObserve#onStateChanged() 方法:
LiveData$ObserverWrapper.java
1 | java复制代码private abstract class ObserverWrapper { |
Livedata$LifecycleBoundObserver.java
1 | java复制代码// 注册方式:observe() |
AlwaysActiveObserver.java
1 | scala复制代码// 注册方式:observeForever() |
2.3 同步设置数据的执行过程
LiveData 使用 setValue() 方法进行同步设置数据(必须在主线程调用),需要注意的是,设置数据后并不一定会回调 Observer#onChanged() 分发数据,而是需要同时 2 个条件:
- 条件 1: 观察者绑定的生命周期处于活跃状态;
- observeForever() 观察者:一直处于活跃状态;
- observe() 观察者:owner 宿主生命周期处于活跃状态。
- 条件 2: 观察者的持有的版本号小于 LiveData 的版本号时。
LiveData.java
1 | typescript复制代码// LiveData 持有的版本号 |
总结一下回调 Observer#onChanged() 的情况:
- 1、注册观察者时,观察者绑定的生命处于活跃状态,并且 LiveData 存在已设置的旧数据;
- 2、调用 setValue() / postValue() 设置数据时,观察者绑定的生命周期处于活跃状态;
- 3、观察者绑定的生命周期由非活跃状态转为活跃状态,并且 LiveData 存在未分发到该观察者的数据(即观察者持有的版本号小于 LiveData 持有的版本号);
提示: observeForever() 虽然没有直接绑定生命周期宿主,但可以理解为绑定的生命周期是全局的,因此在移除观察者之前都是活跃状态。
2.4 异步设置数据的执行过程
LiveData 使用 postValue() 方法进行异步设置数据(允许在子线程调用),内部会通过一个临时变量 mPendingData 存储数据,再通过 Handler 将切换到主线程并调用 setValue(临时变量)。因此,当在子线程连续 postValue() 时,可能会出现中间的部分数据不会被观察者接收到。
LiveData.java
1 | java复制代码final Object mDataLock = new Object(); |
总结一下 LiveData 可能丢失数据的场景,此时观察者可能不会接收到所有的数据:
- 情况 1(背压问题): 使用 postValue() 异步设置数据,并且观察者的消费速度小于数据生产速度;
- 情况 2: 在观察者处理回调(Observer#obChanged())的过程中重新设置新数据,此时会中断旧数据的分发,部分观察者将无法接收到旧数据;
- 情况 3: 观察者绑定的生命周期处于非活跃状态时,连续使用 setValue() / postValue() 设置数据时,观察将无法接收到中间的数据。
注意: 丢失数据不一定是需要解决的问题,需要视场景分析。
2.5 LiveData 数据重放原因分析
LiveData 的数据重放问题也叫作数据倒灌、粘性事件,核心源码在 LiveData#considerNotify(Observer) 中:
- 首先,LiveData 和观察者各自会持有一个版本号 version,每次 LiveData#setValue 或 postValue 后,LiveData 持有的版本号会自增 1。在 LiveData#considerNotify(Observer) 尝试分发数据时,会判断观察者持有版本号是否小于 LiveData 的版本号(Observer#mLastVersion >= LiveData#mVersion 是否成立),如果成立则说明这个观察者还没有消费最新的数据版本。
- 而观察者的持有的初始版本号是 -1,因此当注册新观察者并且正好宿主的生命周期是大于等于可见状态(STARTED)时,就会尝试分发数据,这就是数据重放。
为什么 Google 要把 LiveData 设计为粘性呢?LiveData 重放问题需要区分场景来看 —— 状态适合重放,而事件不适合重放:
- 当 LiveData 作为一个状态使用时,在注册新观察者时重放已有状态是合理的;
- 当 LiveData 作为一个事件使用时,在注册新观察者时重放已经分发过的事件就是不合理的。
- LiveData 数据重放问题的解决方案
这里我们总结一下业界提出处理 LiveData 数据重放问题的方案:
3.1 Event 事件包装器
实现一个事件包装器,内部使用一个标志位标记事件是否已经被消费过。这样的话,当观察者收到重放的数据时,由于其中的标记位已经显示被消费,因此会抛弃该事件。
不过,虽然这个方法能够解决数据倒灌问题,但是会有副作用:对于多个观察者的情况,只允许第一个观察者消费,而后续的观察者无法消费实现,这一般是不能满足需求的。
1 | kotlin复制代码open class Event<out T>(private val content: T) |
3.2 SingleLiveData 事件包装器变型方案
SingeLiveData 是 Google 官方的方案,在 LiveData 内部通过一个原子标志位来标记事件是否已经被消费过。这个方法本质上和 Event 实现包装器是一样的,因此也存在完全相同的副作用。
SingleLiveEvent.java
1 | java复制代码public class SingleLiveEvent<T> extends MutableLiveData<T> { |
3.3 反射修改观察者版本号
业界分享出来的一个方案,不确定思路原创源。实现方法是在注册新观察者时,通过反射的手段将观察者持有的版本号(Observer#mLastVersion)同步为 LiveData 的版本号。缺点是使用反射,但确实能够解决多观察者问题。
1 | ini复制代码private void hook(@NonNull Observer<T> observer) throws Exception { |
3.4 UnPeekLiveData 反射方案优化
UnPeekLiveData 是 KunMinX 提出并开源的方案,主要思路是将 LiveData 源码中的 Observer#mLastVersion 和 LiveData#mVersion 在子类中重新实现一遍。在 UnPeekLiveData 中会有一个原子整型来标记数据版本,并且每个 Observer 在注册时会拿到当前 LiveData 的最新数据版本,而在 Observer#onChanged 中会对比两个版本号来决定是否分发。这个过程中没有使用反射,也不会存在不支持多观察者的问题。
ProtectedUnPeekLiveData.java
1 | less复制代码public class ProtectedUnPeekLiveData<T> extends LiveData<T> { |
UnPeekLiveData.java
1 | typescript复制代码public class UnPeekLiveData<T> extends ProtectedUnPeekLiveData<T> { |
3.5 Kotlin Flow
Google 对 Flow 的定位是 Kotlin 环境下对 LiveData 的替代品,使用 SharedFlow 可以控制重放数量,可以设置为 0 表示禁止重放。
- 基于 LiveData 的事件总线 LiveDataBus
如果我们把事件理解为一种数据,LiveData 可以推数据自然也可以推事件,于是有人将 LiveData 封装为 “广播”,从而实现 “事件发送者” 和 “事件观察者” 的代码解耦,例如美团版本的 LiveDataBus。相较于 EventBus,LiveDataBus 实现更强的生命周期安全;相较于接口,LiveData 的约束力更弱。
4.1 LiveDataBus 什么场景适合?
无论是 EventBus 还是 LiveDataBus,它们本质上都是 “多对多的广播”,它们仅适合作为全局的事件通信,而页面内的事件通信应该继续采用 ViewModel + LiveData 等方案。这是因为事件总线缺乏 MVVM 模式建立的唯一可信源约束,事件发出后很难定位是哪个消息源推送出来的。
4.2 LiveDataBus 的实现
LiveDataBus 代码不多,核心在于使用哈希表保存事件名到 LiveData 的映射关系:
LiveDataBus.java
1 | typescript复制代码public final class LiveDataBus { |
使用 LiveDataBus:
1 | less复制代码LiveDataBus.get().with("key_test").setValue(""); |
4.3 如何加强 LiveDataBus 事件约束
无论是 EventBus 还是 LiveDataBus 都没有对事件定义进行约束,不同开发者 / 不同组件可能会定义相同的事件字符串而导致冲突。
为了优化这个问题,可以使用美团 ModularEventBus 方案:用接口定义事件来实现强约束,在动态代理中取 接口名_方法名
作为事件名,再完成后续 LiveDataBus 的交互。
LiveDataBus.java
1 | kotlin复制代码class LiveDataBus { |
另外,事件接口可以交给 APT 注解处理器生成:通过 DemoEvent 定义事件名常量,用 APT 将事件名转换为事件接口的方法:
DemoEvents.java
1 | java复制代码//可以指定module,若不指定,则使用包名作为module名 |
EventsDefineOfDemoEvents.java
1 | scss复制代码package com.sankuai.erp.modularevent.generated.com.meituan.jeremy.module_b_export; |
使用:
1 | scss复制代码LiveDataBus |
—— 图片引用自美团技术博客
- 总结
到这里,Jetpack 中的 LiveData 组件就讲完了,由于美团的 modular-event 并没有开源,下篇文章我们直接来做一次学习落地。关注我,带你了解更多。
2022 年 8 月 30 日更新
ModularEventBus 组件化事件总线框架现已发布,点击查看
参考资料
- LiveData 概览 —— 官方文档
- 重学安卓:吃透 LiveData 本质,享用可靠消息鉴权机制 —— KunMinX 著
- 重学安卓:LiveData 数据倒灌 “背景缘由全貌” 独家解析 —— KunMinX 著
- 关于 LiveData 粘性事件所带来问题的解决方案—— 慕尼黑 著
- 带你了解 LiveData 重放污染的前世今生—— 徐宜生 著
- Android 消息总线的演进之路:用 LiveDataBus 替代 RxBus、EventBus —— 美团技术团队
- Android 组件化方案及组件消息总线 modular-event 实战 —— 美团技术团队
- 基于 LiveData 实现事件总线思路和方案 —— toothpickTina 著
推荐阅读
Android Jetpack 系列文章目录如下(2023/07/08 更新):
- #1 Lifecycle:生命周期感知型组件的基础
- #2 为什么 LiveData 会重放数据,怎么解决?
- #3 为什么 Activity 都重建了 ViewModel 还存在?
- #4 有小伙伴说看不懂 LiveData、Flow、Channel,跟我走
- #5 Android UI 架构演进:从 MVC 到 MVP、MVVM、MVI
- #6 ViewBinding 与 Kotlin 委托双剑合璧
- #7 AndroidX Fragment 核心原理分析
- #8 OnBackPressedDispatcher:Jetpack 处理回退事件的新姿势
- #9 食之无味!App Startup 可能比你想象中要简单
- #10 从 Dagger2 到 Hilt 玩转依赖注入(一)
⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
本文转载自: 掘金