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
相关产品推荐
相关产品推荐

