10

I have created a very simplified version of my issue below.
The strict mode is set up with the following policies:

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

The view model has only one function which crashes the application when invoked. The function does nothing (it has an empty body)

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

The activity invokes viewModel.foo() in onCreate which crashes the application with the following trace.

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

According to the stack trace there is a disk read violation but nothing in that code should be accessing disk.
The lines of interest are:

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

line 35: viewModelScope.launch(Dispatchers.IO){ }

line 28: viewModel.foo()

Further more if I remove penaltyLog() then the application does not crash.

So my question(s):

How can I prevent the crash with the strict mode configurations above?

Is the problem with coroutine or strict mode itself?

Update: This seems to be a known issue with coroutines. Still unresolved - see the conversation here

Naveed
  • 2,942
  • 2
  • 25
  • 58

6 Answers6

4

The solution is to use your own dispatcher that you initialize without doing I/O on the main thread.

It's a bit tricky to implement because to avoid slowing down your app because of vsync enabled by default on Handler (could delay by up to 16ms code that doesn't need vsync at all), you have to use an API 28+ constructor, and use reflection for older versions of Android. After doing that, you can use the asCoroutineDispatcher() extension function for Handler, and use the resulting dispatcher.

To make it simpler for me, and others, I made a (small) library that provides a Dispatchers.MainAndroid extension that is lazily initialized without any I/O and can be used in place of Dispatchers.Main. It also integrated Lifecycle with coroutine scopes.

Here's the link where you can see how to get the dependency (available on jcenter) and how it is implemented: https://github.com/LouisCAD/Splitties/tree/master/modules/lifecycle-coroutines

Louis CAD
  • 10,965
  • 2
  • 39
  • 58
  • Closing the loop and picking this answer. Other suggestions such as removing penalty death are still valid since coroutine implementation is outside the scope from an application developers perspective. – Naveed Apr 12 '19 at 00:31
2

The problem is that initializing Dispatchers.Main for Kotlin Coroutines is using a lot of disk time to read and checksum your JAR. This shouldn't happen.

This issue in Kotlin Coroutines was resolved with a workaround faster ServiceLoader. There's a newer version of Kotlin Coroutines you should use which offers a workaround ServiceLoader that does not checksum the JAR on disk.

The Google Android team working on the R8 optimizer is also creating an even better solution that will optimize out ServiceLoader reads entirely at the ProGuard step if you have ProGuard optimizations fully enabled with a new enough R8. That fix will be in Android Gradle Plugin 3.5.0 when used with R8.

colintheshots
  • 1,992
  • 19
  • 37
1

Your stacktrace makes it obvious that your code is accessing the disk because it's being run for the first time and it's triggering some classloading. This goes to the DexClassLoader and touches the disk.

Try to enable the strict mode after exercising all your code paths.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • I initialize strict mode on application level. Wont the problem just propagate to a different activity if I initialize it afterwards. – Naveed Apr 09 '19 at 17:56
  • 1
    Exactly, unless you can start it after all the initialization, you'll have to accomodate for false positives. Touching the filesystem on the UI thread during class loading is something you can't possibly avoid. – Marko Topolnik Apr 10 '19 at 06:28
  • Upon further research, doesn't seem like its a false positive. This is a known issue with coroutine implementation currently. https://github.com/Kotlin/kotlinx.coroutines/issues/878 but they don't seem to have any good plans to resolve it. – Naveed Apr 10 '19 at 23:53
  • @Naveed strict-mode with log all violations, without the least way of exclusion. this generally means that co-routines are slightly sub-optimal performance-wise, by design... which most likely cannot even be avoided. one can over-engineer everything, which does not necessarily makes it any better. – Martin Zeitler Apr 11 '19 at 03:19
  • [IO operations on main thread](https://github.com/Kotlin/kotlinx.coroutines/issues/878#issuecomment-479885021) seem like something that should be avoided. I can adjust the strict mode options but then I may run into ANR issues on devices with busy disks as mentioned [here](https://github.com/Kotlin/kotlinx.coroutines/issues/1038) – Naveed Apr 11 '19 at 04:35
  • @MartinZeitler I don't see any support for your "overengineered" claim, Kotlin uses the recommended public Service Discovery API there. But it happens that the Android platform makes that approach really slow, another example of the real-world incompatibility between Android and the Java™ specification. Also, the mentioned slowness is a separate thing from just touching the disk during classloading. Loading a few KB from the disk does not take 250 ms. – Marko Topolnik Apr 11 '19 at 06:30
  • @MarkoTopolnik the issue seems to be when the [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) is accessing `ClassLoader.getResources()`, followed by `dalvik.system` and `java.io` (too much of a call-stack, even if `java.io` might touch the disk only once for checking some file or directory). – Martin Zeitler Apr 11 '19 at 06:55
  • 1
    @MartinZeitler The `getResources()` call is the consequence of using the Service Discovery API to resolve the class that implements `Dispatchers.Main`. It must inspect the `META-INF/services` directory, which isn't pre-loaded on Android, so it goes to the disk and triggers the JAR verification mechanism, which in turn causes the _entire JAR_ to be loaded and run through the computationally expensive cryptographic hashing algorithm. And the solution for that will be to "over-engineer" it and optimize for the special case of Android. – Marko Topolnik Apr 11 '19 at 07:27
  • @MarkoTopolnik it's indeed Android defeating itself then, unless it would inspect `META-INF/services` on another thread. and with over-engineering I wasn't referring to Kotlin, but rather to the strict-mode logging with the intention to fix it all (where `.penaltyDeath()` is also kind of a self-defeat)... while the main purpose is still identifying possible bottle-necks (here I have a switch to enable/disable it globally and only use it when I already have a suspicion). – Martin Zeitler Apr 11 '19 at 07:32
  • @MartinZeitler The issue here is that, even if you move this code to another thread, your calling thread will still not be able to proceed until the process is done. There is always this problem during init: you can't do anything with the GUI before the init is complete, so it will remain frozen whether or not you actually block the GUI thread. – Marko Topolnik Apr 11 '19 at 07:38
  • @MarkoTopolnik optimizing for performance and conservative power & system resource consumption will always be a trade-off... after all, it's still a mobile ARM/64 device. – Martin Zeitler Apr 11 '19 at 08:06
0

I'd remove .penaltyDeath() in order to prevent it from crashing - and ignore that performance penalty - because it is basically "outside of responsibility", unless one has caused it by oneself.

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
0

Not really fixing the issue, but a workaround to ignore StrictMode for a block so that you can continue having StrictMode enabled in the rest of your app:

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

so you can do

class MyViewModel : ViewModel() {
    fun foo() {
        permitDiskReads { viewModelScope.launch(Dispatchers.IO) { } }
    }
}
Rule
  • 629
  • 1
  • 9
  • 19
0

If anyone stumbles upon this, this issue has already been fixed.

So if you are getting the strict mode violations, you probably just need to update the coroutines library.

The current version cab be found on github:

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2")
Zaffy
  • 16,801
  • 8
  • 50
  • 77