Android Kotlin 协程在严格模式下崩溃

Android Kotlin coroutine crashing on strict mode

我在下面创建了一个非常简化的问题版本。
严格模式设置了以下策略:

   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){  }
    }
}

activity 在 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[=13=](StrictMode.java:1619)
        at android.os.-$$Lambda$StrictMode$AndroidBlockGuardPolicynBulCQKaMajrWr41SB7f7YRT1I.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.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(),则应用程序不会崩溃。

所以我的问题是:

如何使用上面的严格模式配置来防止崩溃?

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

更新: 这似乎是协程的一个已知问题。仍未解决 - 查看对话 here

您的堆栈跟踪表明您的代码正在访问磁盘,因为这是第一次 运行 并且它正在触发一些类加载。这会转到 DexClassLoader 并触及磁盘。

在执行所有代码路径后尝试启用严格模式。

我会删除 .penaltyDeath() 以防止它崩溃 - 并忽略性能损失 - 因为它基本上是 "outside of responsibility",除非它是由你自己造成的。

解决方案是使用您自己初始化的调度程序,而无需在主线程上执行 I/O。

实现起来有点棘手,因为要避免因为在 Handler 上默认启用垂直同步而减慢您的应用程序(可能会延迟多达 16 毫秒的代码,根本不需要垂直同步),您有使用 API 28+ 构造函数,并为旧版本的 Android 使用反射。这样做之后,您可以为 Handler 使用 asCoroutineDispatcher() 扩展函数,并使用生成的调度程序。

为了让我和其他人更简单,我制作了一个(小型)库,它提供了一个 Dispatchers.MainAndroid 扩展,它在没有任何 I/O 的情况下进行了延迟初始化,并且可以用来代替 Dispatchers.Main。它还将 Lifecycle 与协程作用域集成在一起。

这里是 link,您可以在其中了解如何获取依赖项(在 jcenter 上可用)以及它是如何实现的:https://github.com/LouisCAD/Splitties/tree/master/modules/lifecycle-coroutines

问题在于,为 Kotlin 协同程序初始化 Dispatchers.Main 会占用大量磁盘时间来读取 JAR 并对其进行校验和。这不应该发生。

This issue in Kotlin Coroutines was resolved with a workaround faster ServiceLoader. There's a newer version 您应该使用的 Kotlin 协同程序提供了一种解决方法 ServiceLoader,它不会对磁盘​​上​​的 JAR 进行校验和。

致力于 R8 优化器的 Google Android 团队也在创建一个更好的解决方案,如果您已通过新的足够的R8。当与 R8 一起使用时,该修复将在 Android Gradle 插件 3.5.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) { } }
    }
}