Effect之DisposableEffect的使用与原理分析

3,088 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

今天接着来分析下“效应”家族的另一个成员:DisposableEffect

DisposableEffect

disposable,顾名思义,这就是一个自带清理作用的EffectDisposableEffect 类似于 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()

再回到DisposableEffectImplonForgotten 的回调,有以下情况:

第一,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]

由日志有以一结论:

  1. 多个 SideEffect,effect将顺序执行
  2. 所有 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 作用和实现原理,也综合 SideEffectLaunchedEffect 一起作了讨论。三者同是Effect,却各有其特定的使用场景。不过呢,更加深入的东西,比如composition和recomposition过程中的流程及触发原理问题,或者composition的协程控制等,未作详细分析,有兴趣的可以来讨论下