UPDATE:
Default delay behavior on Android creates additional threads when using external dispatchers #2972
coroutine 1.6.0 版本修复了该问题,协程上下文调度器未实现 Delay 时,退化使用 Dispatchers.Main 支持,在 android 平台即为 HandlerContext 实现
本文使用 Compose 版本为 1.0.5
一次偶然机会,在 Profiler 下测试查看 Compose 应用的 CPU 使用情况时,发现按下或点击一个使用 Modifier.clickable
修饰的控件时,会新增一个名为 DefaultExecutor
的线程,反复间隔点击可能新增多个同名线程,并在一段时间后消失
测试排查后,确定问题为 Modifier.clickable
触摸或点击事件处理过程中,会调用 delay
方法导致线程新建,具体调用链为 Modifier.clickable
-> Modifier.pointerInput
-> PointerInputScope.detectTapAndPress
-> onPress
-> PressGestureScope.handlePressInteraction
-> delay(TapIndicationDelay)
具体成因要从 delay
的实现说起,源码如下
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait
// forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
internal val CoroutineContext.delay: Delay
get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
internal actual val DefaultDelay: Delay = DefaultExecutor
internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {
//...
private var _thread: Thread? = null
override val thread: Thread
get() = _thread ?: createThreadSync()
@Synchronized
private fun createThreadSync(): Thread {
return _thread ?: Thread(this, THREAD_NAME).apply {
_thread = this
isDaemon = true
start()
}
}
//...
}
public class AndroidUiDispatcher private constructor(
public val choreographer: Choreographer,
private val handler: android.os.Handler
) : CoroutineDispatcher() {
//...
}
当协程上下文中携带 ContinuationInterceptor
且其为 Delay
的子类时,定时任务由其调度实现,当条件不满足时,定时任务会由 DefaultExecutor
实现,DefaultExecutor
为 EventLoopImplBase
子类,内部是由线程承载的事件循环实现调度,当循环队列为空,且所有事件都已完成或取消时,_thread
会被释放,再次有新任务则再次新建线程开启循环进行调度
回到 Modifier.clickable
的问题,Compose
上下文中的 ContinuationInterceptor
为 AndroidUiDispatcher
并不是 Delay
的子类,所以在 Compose
上下文中使用 delay
由 DefaultExecutor
实现调度,导致新线程的生成和销毁
根据 Modifier.clickable
调用链分析,使用 delay
是为了处理区分 onPress
和 onTap
事件。如果仅关心和使用 onTap
点击事件,可自行使用 Modifier.pointerInput
规避 delay
的使用。简单的事例代码如下
fun Modifier.onTap(
enabled: Boolean = true,
block: () -> Unit
): Modifier = then(pointerInput(enabled) {
detectTapGestures(onTap = {
if (enabled) {
block()
}
})
})