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

仪器测试中Dispatchers.Main并发设置引发IllegalStateException崩溃

解决仪器测试中Dispatchers.Main并发设置的崩溃问题

我在仪器测试中碰到一个偶发崩溃,大概每10次测试就会出现一次,触发时机是设置或使用Dispatchers.Main的时候。

崩溃堆栈跟踪

java.lang.IllegalStateException: Dispatchers.Main is used concurrently with setting it
at kotlinx.coroutines.test.internal.TestMainDispatcher$NonConcurrentlyModifiable.concurrentRW(TestMainDispatcher.kt:71)
at kotlinx.coroutines.test.internal.TestMainDispatcher$NonConcurrentlyModifiable.setValue(TestMainDispatcher.kt:89)
at kotlinx.coroutines.test.internal.TestMainDispatcher.setDispatcher(TestMainDispatcher.kt:36)
at kotlinx.coroutines.test.TestDispatchers.setMain(TestDispatchers.kt:24)
at com.zao.testcommon.CoroutineTestRule.starting(CoroutineTestRule.kt:20)
at org.junit.rules.TestWatcher.startingQuietly(TestWatcher.java:113)
at org.junit.rules.TestWatcher.access$000(TestWatcher.java:52)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:59)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:67)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:58)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:446)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2205)
Caused by: java.lang.Throwable: reader location
at kotlinx.coroutines.test.internal.TestMainDispatcher$NonConcurrentlyModifiable.getValue(TestMainDispatcher.kt:75)
at kotlinx.coroutines.test.internal.TestMainDispatcher.dispatch(TestMainDispatcher.kt:29)
at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:320)
at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith$default(DispatchedContinuation.kt:276)
at kotlinx.coroutines.internal.ScopeCoroutine.afterCompletion(Scopes.kt:27)
at kotlinx.coroutines.JobSupport.continueCompleting(JobSupport.kt:939)
at kotlinx.coroutines.JobSupport.access$continueCompleting(JobSupport.kt:25)
at kotlinx.coroutines.JobSupport$ChildCompletion.invoke(JobSupport.kt:1158)
at kotlinx.coroutines.JobSupport.notifyCompletion(JobSupport.kt:1494)
at kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:324)
at kotlinx.coroutines.JobSupport.finalizeFinishingState(JobSupport.kt:241)
at kotlinx.coroutines.JobSupport.tryMakeCompletingSlowPath(JobSupport.kt:909)
at kotlinx.coroutines.JobSupport.tryMakeCompleting(JobSupport.kt:866)
at kotlinx.coroutines.JobSupport.makeCompletingOnce$kotlinx_coroutines_core(JobSupport.kt:831)
at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:100)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

当前使用的CoroutineTestRule

class CoroutineTestRule : TestWatcher() {
    override fun starting(description: Description) {
        super.starting(description)

        val testDispatcher = StandardTestDispatcher()

        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        super.finished(description)

        Dispatchers.resetMain()
    }
}

测试类中通过@get:Rule引入该规则:

@get:Rule
val coroutineTestRule = CoroutineTestRule()

问题分析

从堆栈可以看出,崩溃的核心原因是设置Dispatchers.Main和使用它的操作并发执行:当前测试在starting方法中设置新的TestDispatcher,而前序测试的某个协程还在主线程上恢复执行,两者同时访问全局的Dispatchers.Main导致冲突。

JUnit虽然会隔离每个测试的实例,但Dispatchers.Main是全局静态变量,默认的resetMain只会恢复原始调度器,不会清理前序测试残留的协程任务——如果前序测试中有未正确取消的协程、或者依赖Android主线程的异步操作没完成,就会在新测试启动时继续占用Dispatchers.Main,触发并发访问异常。

解决方案

1. 维护测试专属的CoroutineScope,确保测试结束时取消所有协程

修改测试规则,创建一个绑定测试Dispatcher的CoroutineScope,测试结束时主动取消所有协程,彻底清理残留任务:

class CoroutineTestRule : TestWatcher() {
    private val testDispatcher = StandardTestDispatcher()
    val testScope = CoroutineScope(testDispatcher)

    override fun starting(description: Description) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        super.finished(description)
        // 取消所有绑定到测试Scope的协程
        testScope.cancel()
        Dispatchers.resetMain()
    }
}

测试中所有协程都要使用这个testScope,不要创建独立的Scope,确保所有任务都能被统一清理。

2. 切换到UnconfinedTestDispatcher避免异步调度冲突

如果测试不需要严格控制协程的执行顺序,可以用UnconfinedTestDispatcher替代StandardTestDispatcher——它会直接在当前线程执行协程,避免主线程调度的异步冲突:

override fun starting(description: Description) {
    super.starting(description)
    Dispatchers.setMain(UnconfinedTestDispatcher())
}

3. 测试结束前清空Main调度器的任务队列

如果坚持使用StandardTestDispatcher,可以在测试结束前主动执行所有待处理任务,确保没有残留:

override fun finished(description: Description) {
    super.finished(description)
    // 执行所有待处理的调度任务
    (Dispatchers.Main as? StandardTestDispatcher)?.runCurrent()
    Dispatchers.resetMain()
}

4. 确保测试中的异步操作完全完成

对于Activity/Fragment测试,要确保所有生命周期回调、网络请求模拟等异步操作在测试结束前完成。比如用Espresso的IdlingResource等待异步任务,或者调用await方法等待生命周期状态:

// 示例:等待Fragment创建完成
launchFragmentInContainer<TestFragment>().onFragment { fragment ->
    fragment.viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)
}

根因总结

这个问题本质是测试隔离不彻底:全局的Dispatchers.Main没有在测试间被完全清理,前序测试的副作用影响了当前测试。通过主动管理协程生命周期、清理调度器队列,可以彻底解决这种偶发的并发崩溃。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 15:12:03