了解 Compose 副作用 API 的实现原理
什么是副作用?
Compose 最大的优势就是它的声明式编程。而掌握声明式 UI 的关键在于处理好副作用。
简单来说,副作用就是在可组合函数的执行,影响了函数外部的应用状态发生的变化。可组合函的重组非常频繁,如果在 Composable 里直接写从网络取数据、查数据库这种业务逻辑,因为说不定啥时候就重组了,这些操作就可能会多跑好几遍,搞出一堆 bug,或者把性能也给搞崩了。
为了解决这这类问题,Jetpack Compose 提供了一套副作用 API让我们安全地处理副作用。今天重点盘点最常用的三个 API : LaunchedEffect、DisposableEffect 和 SideEffect 。带大家从底层原理了解它们的功能,以后写代码心里会更有底气。
LaunchedEffect
LaunchedEffect 是使用频率最高的 API,它能让我们用一种能感知 Composable 生命周期(注意哈,不是 Activity 的生命周期)的方式去启动协程,而且只要指定的关键参数不变,里头的代码块就不会再执行。
这特性让它在处理一次性事件,像弹个 Toast、Snackbar,或者打点日志,再或者触发点业务逻辑的时候,特别好使。就像
Now in Android 项目里的代码示例这样:
val snackbarHostState = remember { SnackbarHostState() }
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
// 如果用户没联网,就弹个Snackbar通知一下
val notConnectedMessage = stringResource(R.string.not_connected)
LaunchedEffect(isOffline) {
if (isOffline) {
snackbarHostState.showSnackbar(
message = notConnectedMessage,
duration = Indefinite
)
}
}
需要注意,LaunchedEffect 在底层会搞出一个新的协程作用域。这就意味着它主要是为了在可组合函数的作用域里,执行那些基于协程的任务设计的。而且一旦可组合函数从组合(Composition)里退出来,它的协程就会自动取消。
所以说,LaunchedEffect 特别适合用来搞数据获取、延迟效果,或者事件处理这些跟协程相关的活儿,要是就想执行个非挂起函数,用它就有点杀鸡用牛刀了。下面咱就深入看看它内部到底咋运作的:
@Composable
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
internal class LaunchedEffectImpl(
parentCoroutineContext: CoroutineContext,
private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver {
private val scope = CoroutineScope(parentCoroutineContext)
private var job: Job? = null
override fun onRemembered() {
// 正常情况不该发生,但以防万一留着
job?.cancel("LaunchedEffect already running!")
job = scope.launch(block = task)
}
override fun onForgotten() {
job?.cancel(LeftCompositionCancellationException())
job = null
}
override fun onAbandoned() {
job?.cancel(LeftCompositionCancellationException())
job = null
}
}
从它的内部实现能看出来,LaunchedEffect 会创建一个 LaunchedEffectImpl,然后把给定的键值存到内存里。要是键值变了,就用这参数再重新创建一个 LaunchedEffectImpl 实例。
再看 LaunchedEffectImpl 这个类,它实现了 RememberObserver,一开始会弄出个新的协程作用域。等可组合函数进入组合阶段,传进去的 lambda 表达式就在这个作用域里跑起来。等可组合函数离开组合的时候,协程作用域就自动取消了,这样就能把资源好好清理干净,也不用担心内存泄漏或者性能问题。
要是你的任务跟协程没啥关系,只是键值变了需要重新执行一下,用 LaunchedEffect 就有点浪费了。虽说创建协程作用域的开销一般不大,但要是根本用不着协程,那这开销就是多余的。这种情况,咱可以考虑用个轻量级的副作用处理库,像 RememberedEffect 处理非挂起任务可能就更合适。
还有个常见误区值得一提,有人觉得 LaunchedEffect 能感知 Android 的生命周期,其实不是那么回事儿。从内部实现能清楚看到,它只跟 Composable 生命周期挂钩,它不能感知 Activity、Fragment 的 onStop()、onDestroy() 这些生命周期事件。这就意味着,要是在 LaunchedEffect 里启动个协程,结果 Android 组件(比如 Activity)被销毁了,除非你专门把这协程跟安卓生命周期绑定起来,不然这协程可能还在执行。
DisposableEffect
DisposableEffect 是另一个副作用处理 API。它适合把一些设置和清理逻辑,跟可组合函数的生命周期同步起来。一旦可组合函数离开组合,它就会提供一个清理的代码块。这特性让它在管理像监听器、回调,或者得手动销毁的广播接收器这些外部资源的时候,特别方便。
val lifecycleOwner = LocalLifecycleOwner.current
if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.STARTED) {
DisposableEffect(lifecycleOwner) {
// 创建一个观察者,在我们记住的回调上触发事件
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_PAUSE || event == Lifecycle.Event.ON_STOP) {
// 干点啥事儿
}
}
// 把观察者加到生命周期里
lifecycleOwner.lifecycle.addObserver(observer)
// 可组合函数离开组合的时候,把观察者去掉
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
上面这例子就是用 DisposableEffect 给 lifecycleOwner 注册了一个 LifecycleEventObserver,保证可组合函数离开组合的时候,能把这观察者安全移除,把相关资源清理得明明白白。下面再深挖一下它的内部原理:
@Composable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}
private class DisposableEffectImpl(
private val effect: DisposableEffectScope.() -> DisposableEffectResult
) : RememberObserver {
private var onDispose: DisposableEffectScope.() -> Unit = {}
private var done: Boolean = false
override fun onRemembered() {
val result = DisposableEffectScope().effect()
onDispose = result.onDispose
done = result.done
}
override fun onForgotten() {
if (!done) {
onDispose()
}
onDispose = {}
}
override fun onAbandoned() {
if (!done) {
onDispose()
}
onDispose = {}
}
}
class DisposableEffectScope {
inline fun onDispose(
crossinline disposable: () -> Unit
): DisposableEffectResult {
return DisposableEffectResult(
onDispose = disposable,
done = false
)
}
fun disposable(
crossinline disposable: () -> Unit
): DisposableEffectResult {
disposable()
return DisposableEffectResult(
onDispose = {},
done = true
)
}
}
data class DisposableEffectResult(
val onDispose: () -> Unit,
val done: Boolean
)
从 DisposableEffect 的内部实现能看出,它会用传进去的效果创建一个 DisposableEffectImpl 实例,然后存到内存里。DisposableEffectImpl 类实现了 RememberObserver,当可组合函数进入组合阶段,它会在 DisposableEffectScope 里先创建一个效果。等可组合函数离开组合的时候,onDispose 函数就会自动调用,保证在可组合函数完全从组合里移除之前,把资源清理得妥妥当当。
SideEffect
SideEffect 主要是用来安全地通知那些在组合之外、不受组合管理的对象,告诉它们组合里状态发生变化了。这在触发那些依赖于 UI 最终稳定状态的副作用时,特别有用。
用 SideEffect 能避免在组合过程中执行操作带来的风险。要是直接在可组合函数里写效果,一旦状态变了,组合过程可能就会被打断。这就使得 SideEffect 在跟日志工具、分析工具,或者命令式 UI 组件这些外部系统打交道的时候,能派上大用场。
看下面这例子:
@Composable
fun FirebaseAnalytics(user: User) {
val firebaseAnalytics = remember { FirebaseAnalytics.getInstance(context) }
SideEffect {
// 每次成功组合之后,更新Firebase Analytics
if (user.hasThisMergedDataAttached) {
firebaseAnalytics.setUserProperty("user_type", user.userType)
}
}
}
接下来看看内部是如何运作的:
@Composable
fun SideEffect(
effect: () -> Unit
) {
currentComposer.recordSideEffect(effect)
}
fun <T> changeSideEffect(
effect: () -> T
): T {
return currentComposer.recordSideEffect(effect)
}
乍一看这代码好像挺简单,但实际上 SideEffect 跟 Compose 运行时的底层联系非常紧密,通过 Compose 源码的注释可知,SideEffect 会在当前组合尝试成功,而且组合状态没被快照操作改掉的时候才运行。这就能防止在当前组合操作失败的时候,让相关对象处于不一致的状态。
副作用在重新组合的过程中是绝对不会运行的,它们都是在成功重新组合之后才会跑起来。也就是说,SideEffect 仅在每次成功组合之后才执行。
总结
本文带大家详细研究了 Jetpack Compose 里最常用的三个主要副作用处理 API。在声明式 UI 的世界里,状态能影响到执行行为的方方面面,所以把副作用管理好,对保证任务能正确、可预测地执行,是非常关键的。
更多推荐
所有评论(0)