Сопрограмма Android Kotlin аварийно завершает работу в строгом режиме

Ниже я создал очень упрощенную версию моей проблемы.
Строгий режим настроен со следующими политиками:

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

Активность вызывает viewModel.foo() в onCreate, что приводит к сбою приложения со следующей трассировкой.

   --------- 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 09.04.2019 06:34

Да, с Main Dispatcher тоже вылетает. Та же трассировка стека.

Naveed 09.04.2019 07:06
8
2
1 843
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Ваша трассировка стека делает очевидным, что ваш код обращается к диску, потому что он запускается в первый раз и вызывает некоторую загрузку классов. Это идет к DexClassLoader и касается диска.

Попробуйте включить строгий режим после проверки всех путей кода.

Я инициализирую строгий режим на уровне приложения. Не будет ли проблема просто распространяться на другую деятельность, если я ее потом инициализирую.

Naveed 09.04.2019 19:56

Точно, если вы не можете запустить его после всей инициализации, вам придется учитывать ложные срабатывания. Прикосновение к файловой системе в потоке пользовательского интерфейса во время загрузки класса — это то, чего вы не можете избежать.

Marko Topolnik 10.04.2019 08:28

После дальнейших исследований не похоже, что это ложноположительный результат. В настоящее время это известная проблема с реализацией сопрограммы. github.com/Kotlin/kotlinx.coroutines/issues/878, но, похоже, у них нет хороших планов по ее устранению.

Naveed 11.04.2019 01:53

@Naveed строгий режим с записью всех нарушений без малейшего исключения. обычно это означает, что сопрограммы немного неоптимальны с точки зрения производительности, по дизайну ... чего, скорее всего, даже нельзя избежать. можно перепроектировать все, что не обязательно делает его лучше.

Martin Zeitler 11.04.2019 05:19
Операции ввода-вывода в основном потоке кажется чем-то, чего следует избегать. Я могу настроить параметры строгого режима, но тогда я могу столкнуться с проблемами ANR на устройствах с занятыми дисками, как указано здесь.
Naveed 11.04.2019 06:35

@MartinZeitler Я не вижу никакой поддержки вашему утверждению о «чрезмерной инженерии», Kotlin использует там рекомендуемый общедоступный API обнаружения служб. Но бывает так, что платформа Android делает этот подход очень медленным, что является еще одним примером реальной несовместимости между Android и спецификацией Java™. Кроме того, упомянутая медлительность отличается от простого прикосновения к диску во время загрузки классов. Загрузка нескольких КБ с диска не занимает 250 мс.

Marko Topolnik 11.04.2019 08:30

@MarkoTopolnik проблема, похоже, заключается в том, что ViewModel обращается к ClassLoader.getResources(), за которым следуют dalvik.system и java.io (слишком большой стек вызовов, даже если java.io может коснуться диска только один раз для проверки какого-либо файла или каталога).

Martin Zeitler 11.04.2019 08:55

@MartinZeitler Вызов getResources() является следствием использования API обнаружения служб для разрешения класса, реализующего Dispatchers.Main. Он должен проверять каталог META-INF/services, который предварительно не загружен на Android, поэтому он переходит на диск и запускает механизм проверки JAR, что, в свою очередь, вызывает загрузку весь JAR и выполнение дорогостоящего алгоритма криптографического хэширования. И решение для этого будет заключаться в том, чтобы «перепроектировать» его и оптимизировать для особого случая Android.

Marko Topolnik 11.04.2019 09:27

@MarkoTopolnik, тогда Android действительно побеждает сам себя, если только он не проверит META-INF/services в другой ветке. а под переинжинирингом я имел в виду не Kotlin, а скорее логирование в строгом режиме с намерением все это исправить (где .penaltyDeath() тоже своего рода поражение)... в то время как основная цель все еще идентификация возможные узкие места (здесь у меня есть переключатель, чтобы включить / отключить его глобально и использовать его только тогда, когда у меня уже есть подозрение).

Martin Zeitler 11.04.2019 09:32

@MartinZeitler Проблема здесь в том, что даже если вы переместите этот код в другой поток, ваш вызывающий поток все равно не сможет продолжить работу, пока процесс не будет завершен. Во время инициализации всегда возникает эта проблема: вы ничего не можете сделать с графическим интерфейсом до завершения инициализации, поэтому он останется замороженным, независимо от того, блокируете ли вы фактически поток графического интерфейса.

Marko Topolnik 11.04.2019 09:38

@MarkoTopolnik Оптимизация производительности и консервативного энергопотребления и потребления системных ресурсов всегда будет компромиссом ... в конце концов, это все еще мобильное устройство ARM / 64.

Martin Zeitler 11.04.2019 10:06

Я бы удалил .penaltyDeath(), чтобы предотвратить сбой, и проигнорировал бы это снижение производительности, потому что это в основном «вне ответственности», если только кто-то не вызвал это сам.

Ответ принят как подходящий

Решение состоит в том, чтобы использовать собственный диспетчер, который вы инициализируете, не выполняя ввод-вывод в основном потоке.

Это немного сложно реализовать, потому что, чтобы избежать замедления вашего приложения из-за того, что vsync включен по умолчанию на Handler (может задержать до 16 мс код, который вообще не нуждается в vsync), вы должны использовать конструктор API 28+, и используйте отражение для более старых версий Android. После этого вы можете использовать функцию расширения asCoroutineDispatcher() для Handler и использовать полученный диспетчер.

Чтобы упростить себе и другим задачу, я создал (небольшую) библиотеку, которая предоставляет расширение Dispatchers.MainAndroid, которое лениво инициализируется без каких-либо операций ввода-вывода и может использоваться вместо Dispatchers.Main. Он также интегрировал Lifecycle с областями действия сопрограмм.

Вот ссылка, по которой вы можете увидеть, как получить зависимость (доступно на jcenter) и как она реализована: https://github.com/LouisCAD/Splitties/tree/master/modules/lifecycle-coroutines

Замыкаем цикл и выбираем этот ответ. Другие предложения, такие как удаление штрафной смерти, по-прежнему актуальны, поскольку реализация сопрограммы выходит за рамки с точки зрения разработчиков приложений.

Naveed 12.04.2019 02:31

Проблема в том, что инициализация Dispatchers.Main для Kotlin Coroutines использует много дискового времени для чтения и проверки контрольной суммы вашего JAR. Этого не должно случиться.

Проблема Эта проблема в Kotlin Coroutines была устранена с помощью более быстрого обходного пути ServiceLoader. Вы должны использовать более новая версия Kotlin Coroutines, который предлагает обходной путь ServiceLoader, который не проверяет контрольную сумму JAR на диске.

Команда Google Android, работающая над оптимизатором R8, также создает еще лучшее решение, которое полностью оптимизирует чтение ServiceLoader на этапе ProGuard, если у вас полностью включена оптимизация ProGuard с достаточно новым R8. Это исправление будет в Android Gradle Plugin 3.5.0 при использовании с R8.

На самом деле это не решение проблемы, но обходной путь для игнорирования 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) { } }
    }
}

Другие вопросы по теме