Ниже я создал очень упрощенную версию моей проблемы.
Строгий режим настроен со следующими политиками:
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()
, то приложение не вылетит.
Итак, мой вопрос (ы):
Как я могу предотвратить сбой с указанными выше конфигурациями строгого режима?
Проблема с сопрограммой или самим строгим режимом?
Обновлять: Кажется, это известная проблема с сопрограммами. Еще не решено - см. беседу здесь
Да, с Main
Dispatcher тоже вылетает. Та же трассировка стека.
Ваша трассировка стека делает очевидным, что ваш код обращается к диску, потому что он запускается в первый раз и вызывает некоторую загрузку классов. Это идет к DexClassLoader
и касается диска.
Попробуйте включить строгий режим после проверки всех путей кода.
Я инициализирую строгий режим на уровне приложения. Не будет ли проблема просто распространяться на другую деятельность, если я ее потом инициализирую.
Точно, если вы не можете запустить его после всей инициализации, вам придется учитывать ложные срабатывания. Прикосновение к файловой системе в потоке пользовательского интерфейса во время загрузки класса — это то, чего вы не можете избежать.
После дальнейших исследований не похоже, что это ложноположительный результат. В настоящее время это известная проблема с реализацией сопрограммы. github.com/Kotlin/kotlinx.coroutines/issues/878, но, похоже, у них нет хороших планов по ее устранению.
@Naveed строгий режим с записью всех нарушений без малейшего исключения. обычно это означает, что сопрограммы немного неоптимальны с точки зрения производительности, по дизайну ... чего, скорее всего, даже нельзя избежать. можно перепроектировать все, что не обязательно делает его лучше.
@MartinZeitler Я не вижу никакой поддержки вашему утверждению о «чрезмерной инженерии», Kotlin использует там рекомендуемый общедоступный API обнаружения служб. Но бывает так, что платформа Android делает этот подход очень медленным, что является еще одним примером реальной несовместимости между Android и спецификацией Java™. Кроме того, упомянутая медлительность отличается от простого прикосновения к диску во время загрузки классов. Загрузка нескольких КБ с диска не занимает 250 мс.
@MarkoTopolnik проблема, похоже, заключается в том, что ViewModel обращается к ClassLoader.getResources()
, за которым следуют dalvik.system
и java.io
(слишком большой стек вызовов, даже если java.io
может коснуться диска только один раз для проверки какого-либо файла или каталога).
@MartinZeitler Вызов getResources()
является следствием использования API обнаружения служб для разрешения класса, реализующего Dispatchers.Main
. Он должен проверять каталог META-INF/services
, который предварительно не загружен на Android, поэтому он переходит на диск и запускает механизм проверки JAR, что, в свою очередь, вызывает загрузку весь JAR и выполнение дорогостоящего алгоритма криптографического хэширования. И решение для этого будет заключаться в том, чтобы «перепроектировать» его и оптимизировать для особого случая Android.
@MarkoTopolnik, тогда Android действительно побеждает сам себя, если только он не проверит META-INF/services
в другой ветке. а под переинжинирингом я имел в виду не Kotlin, а скорее логирование в строгом режиме с намерением все это исправить (где .penaltyDeath()
тоже своего рода поражение)... в то время как основная цель все еще идентификация возможные узкие места (здесь у меня есть переключатель, чтобы включить / отключить его глобально и использовать его только тогда, когда у меня уже есть подозрение).
@MartinZeitler Проблема здесь в том, что даже если вы переместите этот код в другой поток, ваш вызывающий поток все равно не сможет продолжить работу, пока процесс не будет завершен. Во время инициализации всегда возникает эта проблема: вы ничего не можете сделать с графическим интерфейсом до завершения инициализации, поэтому он останется замороженным, независимо от того, блокируете ли вы фактически поток графического интерфейса.
@MarkoTopolnik Оптимизация производительности и консервативного энергопотребления и потребления системных ресурсов всегда будет компромиссом ... в конце концов, это все еще мобильное устройство ARM / 64.
Я бы удалил .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
Замыкаем цикл и выбираем этот ответ. Другие предложения, такие как удаление штрафной смерти, по-прежнему актуальны, поскольку реализация сопрограммы выходит за рамки с точки зрения разработчиков приложений.
Проблема в том, что инициализация 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) { } }
}
}
Что, если вы попробуете с
Dispatchers.MAIN
, он все равно вылетит?