携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
今天接着来分析下“效应”家族的另一个成员:DisposableEffect
DisposableEffect
disposable,顾名思义,这就是一个自带清理作用的Effect。DisposableEffect 类似于 LaunchedEffect,有 1至N 个key参数的重载版本(0个参数的已经deprecated,调用将抛异常),也就是说,类似地,它也是通过key来驱动触发Effect的执行的。
@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}
@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
key2: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1, key2) { DisposableEffectImpl(effect) }
}
// 等等……不列举了
原理分析
来看看带一个key的版本是怎么做的。
首先,它的Effect是一个扩展型lambda,即 DisposableEffectScope 类型下、并返回DisposableEffectResult 的lambda
class DisposableEffectScope {
/**
* Provide [onDisposeEffect] to the [DisposableEffect] to run when it leaves the composition
* or its key changes.
*/
inline fun onDispose(
crossinline onDisposeEffect: () -> Unit
): DisposableEffectResult = object : DisposableEffectResult {
override fun dispose() {
onDisposeEffect()
}
}
}
interface DisposableEffectResult {
fun dispose()
}
DisposableEffect 本质上是一个 RememberObserver 子类,这一点和 LaunchedEffect 一样,其实现为DisposableEffectImpl:
private val InternalDisposableEffectScope = DisposableEffectScope()
private class DisposableEffectImpl(
private val effect: DisposableEffectScope.() -> DisposableEffectResult
) : RememberObserver {
private var onDispose: DisposableEffectResult? = null
override fun onRemembered() {
// remember完成后,onDispose初始化
onDispose = InternalDisposableEffectScope.effect()
}
override fun onForgotten() {
onDispose?.dispose()
onDispose = null
}
override fun onAbandoned() {
// Nothing to do as [onRemembered] was not called.
}
}
可以看到,前面所说的 DisposableEffectScope 即由 InternalDisposableEffectScope 直接提供
那前文的 DisposableEffectResult 返回值是怎么来的呢?答案就是 DisposableEffectScope.onDispose() 方法,它的返回值就是DisposableEffectResult,所以这就是为什么实现Effect的时候,结尾需要调用onDispose()
再回到DisposableEffectImpl,onForgotten 的回调,有以下情况:
第一,Composition的changes发生的时候(见SideEffect的分析):
internal class CompositionImpl((/*...*/): ControlledComposition {
private fun applyChangesInLocked(changes: MutableList<Change>) {
val manager = RememberEventDispatcher(abandonSet)
try {
if (changes.isEmpty()) return
applier.onBeginChanges()
// Apply all changes
slotTable.write { slots ->
val applier = applier
changes.fastForEach { change ->
change(applier, slots, manager)
}
changes.clear()
}
applier.onEndChanges()
// Side effects run after lifecycle observers so that any remembered objects
// that implement RememberObserver receive onRemembered before a side effect
// that captured it and operates on it can run.
// 这里分发observer
manager.dispatchRememberObservers()
manager.dispatchSideEffects()
if (pendingInvalidScopes) {
pendingInvalidScopes = false
observations.removeValueIf { scope -> !scope.valid }
derivedStates.removeValueIf { derivedValue -> derivedValue !in observations }
}
} finally {
// Only dispatch abandons if we do not have any late changes. The instances in the
// abandon set can be remembered in the late changes.
if (this.lateChanges.isEmpty())
manager.dispatchAbandons()
}
}
}
private class RememberEventDispatcher(
private val abandoning: MutableSet<RememberObserver>
) : RememberManager {
fun dispatchRememberObservers() {
// Send forgets
if (forgetting.isNotEmpty()) {
for (i in forgetting.size - 1 downTo 0) {
val instance = forgetting[i]
if (instance !in abandoning) {
// 循环回调
instance.onForgotten()
}
}
}
// Send remembers
if (remembering.isNotEmpty()) {
remembering.fastForEach { instance ->
abandoning.remove(instance)
instance.onRemembered()
}
}
}
}
所谓的changes变化,也就是(re)composition触发的流程,而外在原因,即是key值的变化。
第二,composition消亡的时候,比如界面关闭了:
internal class CompositionImpl(/*...*/) : ControlledComposition {
// ...
override fun dispose() {
synchronized(lock) {
if (!disposed) {
disposed = true
composable = {}
val nonEmptySlotTable = slotTable.groupsSize > 0
if (nonEmptySlotTable || abandonSet.isNotEmpty()) {
val manager = RememberEventDispatcher(abandonSet)
if (nonEmptySlotTable) {
slotTable.write { writer ->
writer.removeCurrentGroup(manager)
}
applier.clear()
// dispose处理,分发到各observer
manager.dispatchRememberObservers()
}
manager.dispatchAbandons()
}
composer.dispose()
}
}
parent.unregisterComposition(this)
}
// ...
}
顶层实现中,由remember来记忆了参数key,其记忆值为 DisposableEffectImpl(effect),这样一来,key值的变化将触发 DisposableEffectImpl 的重新构造;如果不是首次构造,就会调用已记忆值的onDispose作清理
观察到Composition的实现中,dispatchRememberObservers 是先于 dispatchSideEffects 执行的,所以 Disposable的Effect,是先于 Side 的执行的。
例子
好,咱又到了案例验证学习成果的时间,继续沿用 SideEffect 中的例子:
val clicked = remember { mutableStateOf(0) }
Log.d("det", "disposable effect before: ${clicked.value}")
DisposableEffect(key1 = clicked.value) {
Log.d("det", "disposable effect called")
onDispose {
Log.d("det", "disposable effect disposed")
}
}
Log.d("det", "disposable effect after: ${clicked.value}")
Column {
Text(
text = "Clicked: ${clicked.value}", modifier = Modifier
.padding(16.dp)
.clickable {
clicked.value = clicked.value + 1
}
)
}
启动后:
2022-08-02 17:21:47.233 3312-3312 det com.jd.example.dettest D disposable effect before: 0
2022-08-02 17:21:47.234 3312-3312 det com.jd.example.dettest D disposable effect after: 0
2022-08-02 17:21:47.248 3312-3312 det com.jd.example.dettest D disposable effect called
类似地,也是composition完成后,执行effect。点击两下:
2022-08-02 17:24:23.864 4400-4400 det com.jd.example.dettest D disposable effect before: 1
2022-08-02 17:24:23.865 4400-4400 det com.jd.example.dettest D disposable effect after: 1
2022-08-02 17:24:23.875 4400-4400 det com.jd.example.dettest D disposable effect disposed
2022-08-02 17:24:23.875 4400-4400 det com.jd.example.dettest D disposable effect called
2022-08-02 17:24:32.034 4400-4400 det com.jd.example.dettest D disposable effect before: 2
2022-08-02 17:24:32.034 4400-4400 det com.jd.example.dettest D disposable effect after: 2
2022-08-02 17:24:32.046 4400-4400 det com.jd.example.dettest D disposable effect disposed
2022-08-02 17:24:32.046 4400-4400 det com.jd.example.dettest D disposable effect called
如前面分析,在recomposition过程中,首先调用onDispose,清理前次effect,而后执行当次effect;而且,清理也是在recomposition完成后
再加一个DisposableEffect:
// ...
Log.d("det", "disposable effect before: ${clicked.value}")
DisposableEffect(key1 = clicked.value) {
Log.d("det", "disposable effect called")
onDispose {
Log.d("det", "disposable effect disposed")
}
}
Log.d("det", "disposable effect after: ${clicked.value}")
DisposableEffect(key1 = clicked.value) {
Log.d("det", "disposable effect called [2]")
onDispose {
Log.d("det", "disposable effect disposed [2]")
}
}
// ...
启动并点击一次:
2022-08-03 09:50:35.199 24517-24517 det com.jd.example.dettest D disposable effect before: 0
2022-08-03 09:50:35.199 24517-24517 det com.jd.example.dettest D disposable effect after: 0
2022-08-03 09:50:35.214 24517-24517 det com.jd.example.dettest D disposable effect called
2022-08-03 09:50:35.214 24517-24517 det com.jd.example.dettest D disposable effect called [2]
2022-08-03 09:52:22.943 24517-24517 det com.jd.example.dettest D disposable effect before: 1
2022-08-03 09:52:22.944 24517-24517 det com.jd.example.dettest D disposable effect after: 1
2022-08-03 09:52:22.954 24517-24517 det com.jd.example.dettest D disposable effect disposed [2]
2022-08-03 09:52:22.954 24517-24517 det com.jd.example.dettest D disposable effect disposed
2022-08-03 09:52:22.954 24517-24517 det com.jd.example.dettest D disposable effect called
2022-08-03 09:52:22.954 24517-24517 det com.jd.example.dettest D disposable effect called [2]
由日志可以看出,多个 DisposableEffect 是顺序执行effect并逆序dispose的。再加两个SideEffect呢:
// ...
Log.d("det", "disposable effect before: ${clicked.value}")
DisposableEffect(key1 = clicked.value) {
Log.d("det", "disposable effect called")
onDispose {
Log.d("det", "disposable effect disposed")
}
}
Log.d("det", "disposable effect after: ${clicked.value}")
SideEffect {
Log.d("det", "side effect called [1]")
}
DisposableEffect(key1 = clicked.value) {
Log.d("det", "disposable effect called [2]")
onDispose {
Log.d("det", "disposable effect disposed [2]")
}
}
Column {
// ....
}
SideEffect {
Log.d("det", "side effect called [2]")
}
同样启动并点击一次:
2022-08-03 09:56:55.763 25947-25947 det com.jd.example.dettest D disposable effect before: 0
2022-08-03 09:56:55.763 25947-25947 det com.jd.example.dettest D disposable effect after: 0
2022-08-03 09:56:55.777 25947-25947 det com.jd.example.dettest D disposable effect called
2022-08-03 09:56:55.777 25947-25947 det com.jd.example.dettest D disposable effect called [2]
2022-08-03 09:56:55.778 25947-25947 det com.jd.example.dettest D side effect called [1]
2022-08-03 09:56:55.778 25947-25947 det com.jd.example.dettest D side effect called [2]
2022-08-03 09:57:30.670 25947-25947 det com.jd.example.dettest D disposable effect before: 1
2022-08-03 09:57:30.671 25947-25947 det com.jd.example.dettest D disposable effect after: 1
2022-08-03 09:57:30.681 25947-25947 det com.jd.example.dettest D disposable effect disposed [2]
2022-08-03 09:57:30.681 25947-25947 det com.jd.example.dettest D disposable effect disposed
2022-08-03 09:57:30.681 25947-25947 det com.jd.example.dettest D disposable effect called
2022-08-03 09:57:30.681 25947-25947 det com.jd.example.dettest D disposable effect called [2]
2022-08-03 09:57:30.681 25947-25947 det com.jd.example.dettest D side effect called [1]
2022-08-03 09:57:30.681 25947-25947 det com.jd.example.dettest D side effect called [2]
由日志有以一结论:
- 多个
SideEffect,effect将顺序执行 - 所有
SideEffect将后于所有DisposableEffect的执行 —— 这也印证了 Composition 的 applyChanges 中的分发逻辑:先 RememberObserver,后 SideEffect
既然都这样了,不差 LaunchedEffect 了:
val clicked = remember { mutableStateOf(0) }
Log.d("det", "disposable effect before: ${clicked.value}")
DisposableEffect(key1 = clicked.value) {
Log.d("det", "disposable effect called")
onDispose {
Log.d("det", "disposable effect disposed")
}
}
Log.d("det", "disposable effect after: ${clicked.value}")
LaunchedEffect(key1 = clicked.value) {
Log.d("det", "launched effect called [1]")
}
SideEffect {
Log.d("det", "side effect called [1]")
}
DisposableEffect(key1 = clicked.value) {
Log.d("det", "disposable effect called [2]")
onDispose {
Log.d("det", "disposable effect disposed [2]")
}
}
LaunchedEffect(key1 = clicked.value) {
Log.d("det", "launched effect called [2]")
}
Column {
Text(
text = "Clicked: ${clicked.value}", modifier = Modifier
.padding(16.dp)
.clickable {
clicked.value = clicked.value + 1
}
)
}
SideEffect {
Log.d("det", "side effect called [2]")
}
启动:
2022-08-03 10:07:40.249 28445-28445 det com.jd.example.dettest D disposable effect before: 0
2022-08-03 10:07:40.249 28445-28445 det com.jd.example.dettest D disposable effect after: 0
2022-08-03 10:07:40.264 28445-28445 det com.jd.example.dettest D disposable effect called
2022-08-03 10:07:40.264 28445-28445 det com.jd.example.dettest D disposable effect called [2]
2022-08-03 10:07:40.265 28445-28445 det com.jd.example.dettest D side effect called [1]
2022-08-03 10:07:40.265 28445-28445 det com.jd.example.dettest D side effect called [2]
2022-08-03 10:07:40.306 28445-28445 det com.jd.example.dettest D launched effect called [1]
2022-08-03 10:07:40.307 28445-28445 det com.jd.example.dettest D launched effect called [2]
LaunchedEffect 是三个Effect最后执行的,原因就在于其effect的执行是由composition的协程上下文控制的(见LaunchedEffect到底为我们处理了什么问题?)
小结
至此,我们不仅清楚了 DisposableEffect 作用和实现原理,也综合 SideEffect 和 LaunchedEffect 一起作了讨论。三者同是Effect,却各有其特定的使用场景。不过呢,更加深入的东西,比如composition和recomposition过程中的流程及触发原理问题,或者composition的协程控制等,未作详细分析,有兴趣的可以来讨论下