安卓 Kotlin 协程在严格模式下崩溃

10

我已经创建了一个非常简化的问题版本,如下所示。
以下是设置严格模式的策略:

   StrictMode.setThreadPolicy(
            StrictMode.ThreadPolicy.Builder()
                .detectDiskReads()
                .detectDiskWrites()
                .detectNetwork()   // or .detectAll() for all detectable problems
                .penaltyLog()
                .penaltyDeath()
                .build()
        )

视图模型只有一个函数,调用该函数会导致应用程序崩溃。该函数什么也不做(它的主体为空)。

class MyViewModel : ViewModel() {
    fun foo() {
        viewModelScope.launch(Dispatchers.IO){  }
    }
}

onCreate中调用viewModel.foo(),导致应用程序崩溃,并显示以下跟踪信息。

   --------- beginning of crash
2019-04-08 22:07:49.579 1471-1471/com.example.myapplication E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.myapplication, PID: 1471
    java.lang.RuntimeException: StrictMode ThreadPolicy violation
        at android.os.StrictMode$AndroidBlockGuardPolicy.onThreadPolicyViolation(StrictMode.java:1705)
        at android.os.StrictMode$AndroidBlockGuardPolicy.lambda$handleViolationWithTimingAttempt$0(StrictMode.java:1619)
        at android.os.-$$Lambda$StrictMode$AndroidBlockGuardPolicy$9nBulCQKaMajrWr41SB7f7YRT1I.run(Unknown Source:6)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
     Caused by: android.os.strictmode.DiskReadViolation
        at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1504)
        at java.io.UnixFileSystem.getBooleanAttributes(UnixFileSystem.java:241)
        at java.io.File.isDirectory(File.java:845)
        at dalvik.system.DexPathList$Element.maybeInit(DexPathList.java:696)
        at dalvik.system.DexPathList$Element.findResource(DexPathList.java:729)
        at dalvik.system.DexPathList.findResources(DexPathList.java:526)
        at dalvik.system.BaseDexClassLoader.findResources(BaseDexClassLoader.java:174)
        at java.lang.ClassLoader.getResources(ClassLoader.java:839)
        at java.util.ServiceLoader$LazyIterator.hasNextService(ServiceLoader.java:349)
        at java.util.ServiceLoader$LazyIterator.hasNext(ServiceLoader.java:402)
        at java.util.ServiceLoader$1.hasNext(ServiceLoader.java:488)
        at kotlin.collections.CollectionsKt___CollectionsKt.toCollection(_Collections.kt:1145)
        at kotlin.collections.CollectionsKt___CollectionsKt.toMutableList(_Collections.kt:1178)
        at kotlin.collections.CollectionsKt___CollectionsKt.toList(_Collections.kt:1169)
        at kotlinx.coroutines.internal.MainDispatcherLoader.loadMainDispatcher(MainDispatchers.kt:15)
        at kotlinx.coroutines.internal.MainDispatcherLoader.<clinit>(MainDispatchers.kt:10)
        at kotlinx.coroutines.Dispatchers.getMain(Dispatchers.kt:55)
        at androidx.lifecycle.ViewModelKt.getViewModelScope(ViewModel.kt:41)
        at com.example.myapplication.MyViewModel.foo(MainActivity.kt:35)
        at com.example.myapplication.MainActivity.onCreate(MainActivity.kt:28)
        at android.app.Activity.performCreate(Activity.java:7136)
        at android.app.Activity.performCreate(Activity.java:7127)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2893)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193) 
        at android.app.ActivityThread.main(ActivityThread.java:6669) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858

根据堆栈跟踪,存在磁盘读取违规,但是那段代码中没有任何内容应该访问磁盘。
感兴趣的代码行为:

   at com.example.myapplication.MyViewModel.foo(MainActivity.kt:35)
        at com.example.myapplication.MainActivity.onCreate(MainActivity.kt:28)

第35行:viewModelScope.launch(Dispatchers.IO){ }

第28行:viewModel.foo()

此外,如果我删除penaltyLog(),应用程序就不会崩溃。

我的问题是:

如何使用上述严格模式配置避免崩溃?

问题出在协程还是严格模式本身?

更新:这似乎是协程的已知问题。仍未解决 - 请参见此处的讨论


如果您尝试使用Dispatchers.MAIN,它是否仍会崩溃? - Jeel Vankhede
是的,它也会与“Main”调度程序崩溃。相同的堆栈跟踪。 - Naveed
6个回答

4
解决方案是使用自己的调度器,在主线程上不进行I/O初始化。
这有点棘手,因为为了避免由于Handler默认启用vsync(可以延迟16ms的代码根本不需要vsync),导致应用程序变慢,您必须使用一个API 28+的构造函数,并对早期版本的Android使用反射。在这样做之后,您可以使用HandlerasCoroutineDispatcher()扩展函数,并使用生成的调度程序。
为了使我和其他人更简单,我编写了一个(小)库,提供了一个Dispatchers.MainAndroid扩展,它是惰性初始化的,并且没有任何I/O,可以代替Dispatchers.Main。它还将Lifecycle与协程范围集成在一起。
这是链接,您可以在链接中查看如何获取依赖项(可在jcenter上获得),以及如何实现:https://github.com/LouisCAD/Splitties/tree/master/modules/lifecycle-coroutines

闭环并选择这个答案。其他建议,如取消惩罚死亡,仍然有效,因为协程实现超出了应用程序开发人员的范围。 - Naveed

2
问题在于,对于 Kotlin 协程,初始化 Dispatchers.Main 会使用大量磁盘时间来读取和校验您的 JAR。这不应该发生。
这个 Kotlin 协程的问题 已经通过更快的 ServiceLoader 解决了。有一个 更新版本 的 Kotlin 协程可供使用,它提供了一种不需要对磁盘上的 JAR 进行校验的 ServiceLoader 解决方案。
Google Android 团队正在开发 R8 优化器,他们正在创建一个更好的解决方案,如果您已经启用了足够新的 R8,并完全启用了 ProGuard 优化,则可以在 ProGuard 步骤中完全优化掉 ServiceLoader 读取。当与 R8 一起使用时,这个修复将在 Android Gradle 插件 3.5.0 中发布。

1
你的堆栈跟踪表明,你的代码正在访问磁盘,因为它是第一次运行,并且触发了某些类加载。这会涉及到 DexClassLoader 并接触磁盘。
在执行完所有代码路径后,尝试启用严格模式。

我在应用程序级别上初始化了严格模式。如果我之后再初始化它,问题不会传播到其他活动吗? - Naveed
1
除非您可以在所有初始化之后启动它,否则您必须适应错误的情况。在类加载期间在UI线程上触摸文件系统是您不可能避免的事情。 - Marko Topolnik
经过进一步的研究,似乎这不是一个误报。目前协程实现存在已知问题。https://github.com/Kotlin/kotlinx.coroutines/issues/878 但他们似乎没有任何好的解决计划。 - Naveed
@Naveed 严格模式记录所有违规行为,不具有最小的排除方式。这通常意味着协程在性能上设计上略有劣势……甚至可能无法避免。可以过度工程化一切,但这并不一定使其更好。 - Martin Zeitler
1
@MartinZeitler getResources() 调用是使用服务发现 API 解析实现 Dispatchers.Main 的类的结果。它必须检查 META-INF/services 目录,这在 Android 上没有预加载,因此会访问磁盘并触发 JAR 验证机制,进而导致整个 JAR 文件被加载并通过计算密集型的加密哈希算法运行。解决方案将是“过度工程化”并针对 Android 的特殊情况进行优化。 - Marko Topolnik
显示剩余6条评论

0

如果有人偶然发现这个问题,那么请注意,这个问题已经被修复了。

所以,如果你遇到了严格模式违规的情况,你可能只需要更新协程库。

当前版本可以在 github 上找到:

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2")

0
我会移除.penaltyDeath()以防止它崩溃 - 并忽略那个性能惩罚 - 因为它基本上是“责任范围之外”,除非是自己造成的。

0

并不是真正解决问题,而是一种绕过StrictMode的方法,以便您可以在应用程序的其余部分继续启用StrictMode:

fun <T> permitDiskReads(func: () -> T): T {
    return if (BuildConfig.DEBUG) {
        val oldThreadPolicy = StrictMode.getThreadPolicy()
        StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder(oldThreadPolicy).permitDiskReads().build())

        val value = func()

        StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder(oldThreadPolicy).build())

        value
    } else {
        func()
    }
}

这样你就可以做到了

class MyViewModel : ViewModel() {
    fun foo() {
        permitDiskReads { viewModelScope.launch(Dispatchers.IO) { } }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接