仪器测试中Dispatchers.Main并发设置引发IllegalStateException崩溃
我在仪器测试中碰到一个偶发崩溃,大概每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

