«В прошлом, например, было возможно использовать try-catch на случай, если вы декодируете огромный битовый массив, который может вызвать OOM» - я думаю, это недостаток текущей реализации. Теоретически, поскольку хранилище растровых изображений не подвергается непосредственному воздействию Java-кода, должна быть возможность выполнять учетные проверки по требованию в коде, который обращается к растровому памяти. Эти проверки, в свою очередь, могут выбирать, выдавать OOM Error или нет. Обратите внимание, что сверхкоммит всегда позволяет выстрелить себе в ногу с помощью JNI - эти изменения в растровом изображении делают это проще для людей, которые не используют JNI.
Это означает, что чем больше памяти вы используете, особенно путем хранения растровых изображений в памяти, тем ближе вы получаете максимальный объем кучи памяти, которую может использовать ваше приложение. Когда вы достигнете максимума, ваше приложение будет аварийно завершено с исключением OutOfMemory (OOM).
@JvmStatic
fun getHeapMemStats(context: Context): String {
val runtime = Runtime.getRuntime()
val maxMemInBytes = runtime.maxMemory()
val availableMemInBytes = runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory())
val usedMemInBytes = maxMemInBytes - availableMemInBytes
val usedMemInPercentage = usedMemInBytes * 100 / maxMemInBytes
return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
Formatter.formatShortFileSize(context, maxMemInBytes) + " (" + usedMemInPercentage + "%)"
}
Проблема
Я заметил, что на Android O (в моем случае это 8.1, но, вероятно, и на 8.0), вышеприведенный код не подвержен распределению растровых изображений.Продолжая копаться, я заметил в профилировщике Android, что чем больше памяти вы используете (сохраняя большие растровые изображения в моем POC), тем больше используемой памяти.
Чтобы проверить, как это работает, я создал простой цикл как таковой:
В некоторых случаях я сделал это за одну итерацию, а в некоторых я только создал растровое изображение в списке, а не декодировал его (код в комментарии). Подробнее об этом позже ...
val list = ArrayList<Bitmap>()
Log.d("AppLog", "memStats:" + MemHelper.getHeapMemStats(this))
useMoreMemoryButton.setOnClickListener {
AsyncTask.execute {
for (i in 0..1000) {
// list.add(Bitmap.createBitmap(20000, 20000, Bitmap.Config.ARGB_8888))
list.add(BitmapFactory.decodeResource(resources, R.drawable.huge_image))
Log.d("AppLog", "heapMemStats:" + MemHelper.getHeapMemStats(this) + " nativeMemStats:" + MemHelper.getNativeMemStats(this))
}
}
}
Это результат запуска выше:
Как видно из графика, приложение достигло огромного объема памяти, значительно превышающего разрешенный максимальный объем кучи, о котором мне сообщили (который составляет 201 МБ).
Что я нашел
Я обнаружил много странного поведения. Из-за этого я решил сообщить о них,Но, в отличие от проверки кучи памяти, кажется, что максимальная собственная память со временем меняет свое значение, что означает, что я не могу знать, каково ее истинное максимальное значение, и поэтому в реальных приложениях я не могу решить, что размер кеша памяти должен быть. Вот результат кода выше:
@JvmStatic
fun getNativeMemStats(context: Context): String {
val nativeHeapSize = Debug.getNativeHeapSize()
val nativeHeapFreeSize = Debug.getNativeHeapFreeSize()
val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize
val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize
return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
Formatter.formatShortFileSize(context, nativeHeapSize) + " (" + usedMemInPercentage + "%)"
}
Когда я дохожу до того, что устройство не может хранить больше растровых изображений (остановлено на 1,1 ГБ или ~ 850 МБ на моем Nexus 5x), вместо исключения OutOfMemory я получаю ... ничего! Он просто закрывает приложение. Даже без диалога, говорящего, что это потерпело крах.
heapMemStats:used: 2.0 MB / 201 MB (0%) nativeMemStats:used: 3.6 MB / 6.3 MB (57%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 290 MB / 310 MB (93%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 553 MB / 579 MB (95%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 821 MB / 847 MB (96%)
В случае, если я просто создаю новое растровое изображение, а не декодирую его (код доступен выше, просто в комментарии), я получаю странный журнал, в котором говорится, что я использую тонны ГБ и имею в наличии тонны ГБ собственной памяти:
Кроме того, в отличие от того, когда я декодирую растровые изображения, я получаю сбой здесь (включая диалог), но это не OOM. Вместо этого это ... NPE!
01-04 10: 12: 36.936 30598-31301 / com.example.user.myapplication E / AndroidRuntime: ОСНОВНОЕ ИСКЛЮЧЕНИЕ: AsyncTask # 1 Процесс: com.example.user.myapplication, PID: 30598 java.lang.NullPointerException: попытка вызвать виртуальный метод 'void android.graphics.Bitmap.setHasAlpha (boolean)' для пустой ссылки на объект на android.graphics.Bitmap.createBitmap (Bitmap.java:1046) на android.graphics.Bitmap.createBitmap (Bitmap.java:980) ) на android.graphics.Bitmap.createBitmap (Bitmap.java:930) на android.graphics.Bitmap.createBitmap (Bitmap.java:891) на com.example.user.myapplication.MainActivity $ onCreate $ 1 $ 1.run (MainActivity. kt: 21) в android.os.AsyncTask $ SerialExecutor $ 1.run (AsyncTask.java:245) в java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1162) в java.util.concurer.ecread $. выполнить (ThreadPoolExecutor.java:636) в java.lang.Thread.run (Thread.java:764)
Глядя на график профилировщика, он становится еще более странным. Использование памяти, кажется, совсем не увеличивается, а в момент сбоя оно просто падает:
Если вы посмотрите на график, вы увидите много значков GC (мусорное ведро). Я думаю, что это может быть сжатие памяти.
Если я делаю дамп памяти (используя профилировщик), в отличие от предыдущих версий Android, я больше не вижу предварительный просмотр растровых изображений.
ВопросыЭто новое поведение вызывает много вопросов. Это может уменьшить количество сбоев OOM, но также может затруднить их обнаружение, обнаружение утечек памяти и их устранение. Может быть, некоторые из того, что я видел, это просто ошибки, но все же ...Что именно изменилось в использовании памяти на Android O? И почему?
Как растровые изображения обрабатываются?
Можно ли по-прежнему просматривать растровые изображения в отчетах о дампе памяти?
Как правильно получить максимальную собственную память, которую разрешено использовать приложению, и распечатать ее в журналах, и использовать ее как средство для определения максимума?
Есть ли видео / статья на эту тему? Я не говорю об оптимизации памяти, которая была добавлена, но больше о том, как распределяются растровые изображения сейчас, как обрабатывать OOM сейчас и т. Д ...
Я предполагаю, что это новое поведение может повлиять на некоторые библиотеки кэширования, верно? Это потому, что они могут зависеть от размера кучи памяти.
Как могло быть так, что я мог создать так много растровых изображений, каждый размером 20 000 x 20 000 (что означает ~ 1,6 ГБ), но когда я мог создать только несколько из них из реального изображения размером 7 680 x 7 680 (то есть ~ 236 МБ) ? Это действительно делает сжатие памяти, как я уже догадался?
Как собственные функции памяти могут вернуть мне такие огромные значения в случае создания растрового изображения, но более разумные, когда я декодировал растровые изображения? Что они имеют в виду?
Что за странный график профилировщика в случае создания растрового изображения? Он почти не увеличивается в использовании памяти, и все же достиг такой точки, что он не может больше их создавать, в конце концов (после вставки большого количества элементов).
Что за странное поведение исключений? Почему при растровом декодировании я не получил никаких исключений или даже журнал ошибок как часть приложения, и когда я их создал, я получил NPE?
Будет ли Play Store обнаруживать OOM и сообщать о них в случае сбоя приложения из-за этого? Это обнаружит это во всех случаях? Могут ли Crashlytics это обнаружить? Есть ли способ получить информацию о такой вещи, будь то пользователи или во время разработки в офисе?
Похоже, ваше приложение было убито убийцей Linux OOM. Разработчики игр и другие люди, которые активно используют родную память, видят, что это происходит постоянно.