在 Compose 上下文中使用协程 delay 方法可能导致新线程产生

1,807 阅读1分钟

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 的线程,反复间隔点击可能新增多个同名线程,并在一段时间后消失

Profiler-CPU.jpg

测试排查后,确定问题为 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 实现,DefaultExecutorEventLoopImplBase 子类,内部是由线程承载的事件循环实现调度,当循环队列为空,且所有事件都已完成或取消时,_thread 会被释放,再次有新任务则再次新建线程开启循环进行调度

回到 Modifier.clickable 的问题,Compose 上下文中的 ContinuationInterceptorAndroidUiDispatcher 并不是 Delay 的子类,所以在 Compose 上下文中使用 delayDefaultExecutor 实现调度,导致新线程的生成和销毁

根据 Modifier.clickable 调用链分析,使用 delay 是为了处理区分 onPressonTap 事件。如果仅关心和使用 onTap 点击事件,可自行使用 Modifier.pointerInput 规避 delay 的使用。简单的事例代码如下

fun Modifier.onTap(
    enabled: Boolean = true,
    block: () -> Unit
): Modifier = then(pointerInput(enabled) {
    detectTapGestures(onTap = {
        if (enabled) {
            block()
        }
    })
})