You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin Coroutine使用IO Dispatcher运行任务仍出现UI冻结及ANR问题咨询

问题原因
  • 回调执行在主线程,高频调用阻塞主线程:绝大多数Android网络库默认将请求完成回调切到主线程执行,你启动的40-50个请求陆续返回时,会持续往主线程抛任务,再加上你在回调中执行写SharedPreference、发广播的操作,直接占满主线程时间片,导致卡顿甚至ANR。如果你写SP用的是commit()方法,同步IO操作会直接阻塞主线程,是ANR的高发原因。
  • 锁竞争加重阻塞:你给processBatchProgress和sendProgressUpdate方法加了@Synchronized注解,同一时间只能有一个协程执行这两个方法,大量协程同时完成请求时会出现锁等待,进一步加重调度卡顿。
  • 协程Scope使用错误:你调用execute方法时直接传入了Dispatchers.getIO(),但方法参数要求的是CoroutineScope,调度器和Scope混用会导致协程调度逻辑不符合预期,部分代码可能误跑到主线程。
  • 回调频率过高:每一个请求项完成都会触发一次进度回调,40-50个请求短时间内触发数十次回调,没有做节流控制,进一步加大主线程负载。
解决方案

1. 回调逻辑切到后台线程执行

所有进度回调的处理逻辑强制跑在IO线程,避免占用主线程资源,修改sendProgressUpdate方法如下:

@Synchronized
private fun sendProgressUpdate(batchProgressCallback: BatchProgressCallback?) {
    if (batchProgressCallback == null) {
        return
    }
    // 先拷贝数据避免并发修改问题
    val progressData = BatchExecutorProgress(
        Progress(
            totalItems,
            totalItemsCompleted,
            totalItemsFailed
        ), HashMap(batchProgressMap)
    )
    // 切IO线程执行回调
    CoroutineScope(Dispatchers.IO).launch {
        batchProgressCallback.onBatchProgress(progressData)
    }
}

回调中写SharedPreference必须使用apply()而非commit(),避免同步IO阻塞。如果是应用内广播,优先使用LocalBroadcastManager降低开销。

2. 增加回调节流机制

限制进度回调的触发频率,避免短时间大量回调,参考实现:

// 增加全局变量控制回调间隔
private var lastUpdateTime = 0L
private const val MIN_UPDATE_INTERVAL = 200L // 最低200ms触发一次回调

@Synchronized
private fun sendProgressUpdate(batchProgressCallback: BatchProgressCallback?) {
    if (batchProgressCallback == null) {
        return
    }
    val currentTime = System.currentTimeMillis()
    // 间隔不足直接跳过本次回调
    if (currentTime - lastUpdateTime < MIN_UPDATE_INTERVAL) {
        return
    }
    lastUpdateTime = currentTime
    // 原有回调逻辑
}

3. 替换同步锁降低锁竞争

将普通成员变量替换为并发安全的实现,去掉方法级的@Synchronized锁,减少锁等待:

// 计数用原子类实现
private val totalItems = AtomicInteger(0)
private val totalItemsCompleted = AtomicInteger(0)
private val totalItemsFailed = AtomicInteger(0)
// 进度Map用并发容器
private val batchProgressMap = ConcurrentHashMap<String, BatchProgress>()

修改后processBatchProgress方法不需要再加@Synchronized注解,直接操作原子类即可。

4. 修正协程Scope使用

调用execute方法时传入正确的后台协程Scope,不要直接传入调度器:

// Java调用示例
CoroutineScope ioScope = new CoroutineScope(Dispatchers.IO + new SupervisorJob());
new ParallelBatchExecutor(50).execute(this, ioScope, batches, progress -> {
    // 回调逻辑
});

5. 限制协程并行数量

一次性启动40-50个网络请求会导致带宽竞争、TCP握手开销增大,建议将并行数限制在10-15个,使用信号量实现限流:

// 全局信号量,最多同时跑12个请求
private val requestSemaphore = Semaphore(12)

private suspend fun executeBatch(
    context: Context,
    batch: Batch,
    batchProgressCallback: BatchProgressCallback
) {
    requestSemaphore.withPermit {
        batch.execute(context) { batchProgress ->
            processBatchProgress(batchProgress, batchProgressCallback)
        }
    }
}

内容的提问来源于stack exchange,提问作者Chandan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.09 23:33:01