WebView выполняет нативный код в нескольких процессах, чтобы отображать веб-контент в приложении Android. Если не управлять экземплярами WebView, это может привести к утечкам памяти, сбоям из-за ее нехватки и снижению производительности приложения.
В этом документе описана многопроцессная модель памяти WebView, а также рассказывается, как правильно управлять ее жизненным циклом, чтобы предотвратить утечки, и как диагностировать проблемы с памятью.
Как устроена память WebView
Чтобы эффективно управлять памятью WebView, необходимо понимать, как Android выделяет ресурсы для веб-контента:
Многопроцессное выполнение. В Android 8.0 (уровень API 26) и более поздних версиях
WebViewотделяет веб-контент от основных функций приложения, используя несколько процессов (на устройствах с небольшим объемом ОЗУ может использоваться один процесс):- Процесс хоста (браузера). Основной процесс приложения, в котором выполняется код
Activityи Java или Kotlin. - Изолированный процесс отрисовки. Отдельный процесс в изолированной среде (
SandboxedProcessService), который анализирует HTML и CSS, выполняет код JavaScript и отрисовывает веб-страницы.
- Процесс хоста (браузера). Основной процесс приложения, в котором выполняется код
Занимаемый объем нативной памяти. Большая часть памяти
WebView, включая отрисованную графику, дерево DOM и память среды выполнения JavaScript, выделяется в нативной памяти, а не в куче Java. Дамп кучи Java (.hprof) показывает только легковесный объект-оболочку Java и не отражает фактический объем памяти, используемый веб-контентом.Влияние на систему. В отличие от выделения памяти в куче Java, которое ограничено
maxHeapи быстро завершается с ошибкойOutOfMemoryError, нативная память может незаметно увеличиваться до гигабайтов. Когда невысвобожденная собственная память заполняет физическую оперативную память и пространство подкачки (zRAM), Low Memory Killer (LMK) в Android начинает завершать фоновые процессы, чтобы освободить память. Это ухудшает многозадачность устройства, а затем приводит к закрытию активного приложения.
Управление жизненным циклом WebView
Правильное управление жизненным циклом необходимо для предотвращения утечек памяти. Распространенная ошибка – считать, что удаление WebView из макета или автоматическое завершение Activity освобождает память.
Чтобы полностью очистить как ссылки на контекст Java, так и ресурсы для отрисовки нативных объявлений, необходимо явно организовать последовательность удаления в жизненном цикле компонента хоста (например, onDestroy()), остановив выполнение активной страницы, отсоединив представление от контейнера и освободив нативные привязки.
Как очистить экземпляры WebView
Чтобы обеспечить корректное завершение работы и освободить ресурсы при уничтожении Activity или Fragment, выполните следующие действия:
- Удалите контейнер
WebViewиз родительского контейнераViewGroup. - Остановить загрузку и очистить историю навигации.
- Позвоните по номеру
destroy(). - Удалите ссылку на
null.
В следующем примере показано, как правильно очистить WebView:
Kotlin
override fun onDestroy() { myWebView?.let { // Remove the WebView from its parent ViewGroup. (it.parent as? ViewGroup)?.removeView(it) // Stop active loading and clear history. it.stopLoading() it.clearHistory() // Destroy the instance. it.destroy() } myWebView = null super.onDestroy() }
Java
@Override
protected void onDestroy() {
if (myWebView != null) {
// Remove the WebView from its parent ViewGroup.
if (myWebView.getParent() instanceof ViewGroup) {
((ViewGroup) myWebView.getParent()).removeView(myWebView);
}
// Stop active loading and clear history.
myWebView.stopLoading();
myWebView.clearHistory();
// Destroy the instance.
myWebView.destroy();
}
myWebView = null;
super.onDestroy();
}
Что такое память после уничтожения
Когда вы вызываете destroy(), система освобождает контекст Activity, очищает иерархии представлений и останавливает фоновую работу в интернете. Однако вы можете заметить, что физическая память процесса (Resident Set Size) не сразу возвращается к базовому значению, которое было до использования WebView.
Это нормальное поведение. Встроенные кеши времени выполнения, общие библиотеки и выделенные страницы памяти остаются в процессе до тех пор, пока операционная система не освободит их или процесс не завершится. Основная цель destroy() – предотвратить Activity утечки памяти при переходе пользователей между экранами, созданными на основе веб-технологий.
Основные показатели для отладки
При анализе потребления памяти WebView обращайте внимание на следующие показатели:
Размер резидентного набора (RSS). Общий объем физической ОЗУ, сопоставленной с процессом, включая общий код и библиотеки (в профилировщике Android Studio этот показатель называется Всего).
Анонимный RSS (RssAnon). Память, выделенная непосредственно процессом, который не поддерживается файлом на диске (например, выделение нативной кучи и среды выполнения JavaScript). Это основной объем памяти, который занимает ваш веб-контент (в профилировщике Android Studio он обозначен как Выделено).
Потребляемая память приложения (PMF) – сумма анонимных данных о RSS и swap-пространстве (zRAM). Показатель PMF отражает фактическую нагрузку на систему, которую создает ваше приложение, используя память, которую нельзя освободить.
PMF браузера и PMF средства визуализации. Память, используемая основным процессом приложения, и память, используемая изолированным процессом средства визуализации. При обработке тяжелого веб-контента нагрузка в основном приходится на процесс отрисовки.
Количество активных объектов (
WebViews,Activities,Views). Количество активных экземпляров интерфейса, контекста иWebView, хранящихся в памяти. Отслеживание этих данных позволяет определить, вызван ли рост памяти сохраненными ссылками Java или только выделением памяти на уровне ОС.Частная память и нативная куча. В
dumpsys meminfoвыделения нативной памяти C/C++ и пользовательские сопоставления памяти (например, кучи ChromiumPartitionAllocили встроенной среды выполнения JavaScript) отображаются в разделе "Нативная куча" и "Частная память", а не "Куча Java".
Подробнее о счетчиках памяти процессов и их категориях…
Практические рабочие процессы диагностики
Поскольку WebView работает с несколькими процессами и выделяет собственную память, для проверки его использования памяти применяйте следующие инструменты и методы:
Инструменты профилирования и диагностики
Чтобы проверить распределение памяти и выявить утечки, используйте следующие инструменты:
Профилировщик памяти Android Studio. Используйте Профилировщик памяти, чтобы визуализировать выделение нативной памяти, отслеживать категории памяти с течением времени и обнаруживать утечки
Activityпри переходе между экранами.Отслеживание памяти с помощью Perfetto. Используйте Perfetto, чтобы записывать системные счетчики памяти (например, RSS и анонимный RSS) и отслеживать общее увеличение памяти. Обратите внимание, что при распределении ресурсов нативных движков
WebViewне создаются стеки вызовов в инструменте профилирования кучи Perfetto. Используйте инструменты разработчика Chrome, чтобы проверять снимки кучи JavaScript и распределение памяти DOM в веб-контенте.
Как посмотреть количество объектов в реальном времени
Чтобы определить, вызван ли рост памяти сохраненными объектами фреймворка Java (например, компонентами интерфейса) или собственными выделениями, проверьте Objectsраздел dumpsys meminfo:
adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"В выходных данных будет указано количество объектов в реальном времени:
Objects
Views: 142 ViewRootImpl: 1
AppContexts: 3 Activities: 1
Assets: 12 AssetManagers: 0
Local Binders: 32 Proxy Binders: 45
Parcel memory: 15 Parcel count: 30
Death Recipients: 2 WebViews: 1
В этом разделе показано количество активных объектов фреймворка, дескрипторов IPC и распределений Parcel. При диагностике WebView основное внимание уделяйте Activities и WebViews.
Выполните целевое взаимодействие с пользователем (например, откройте и закройте веб-экран) несколько раз и сравните значения:
Утечка экземпляра. Если значения
WebViewsилиActivitiesувеличиваются при каждом переходе и не возвращаются к базовому уровню, в приложении происходит утечка экземпляра JavaWebViewили хостаActivity(например, из-за отсутствияViewGroup.removeView()или сохраненных ссылок на прослушиватель). Поскольку утечка памятиActivityзакрепляет все дерево представлений и декодированные ресурсы изображений в памяти, повторные посещения быстро исчерпают кучу Java и приведут к сбоямOutOfMemoryError.Утечка нативных ресурсов или элементов DOM. Если значения
WebViewsиActivitiesостаются постоянными, а общий объем памяти процесса и Другие частные данные продолжают расти, утечка происходит из-за невысвобожденных нативных ресурсов, элементов DOM или привязок движка JavaScript. Поскольку эти выделения находятся в собственной памяти и обходят сборщик мусора ART, они остаются невидимыми для стандартных инструментов обнаружения утечек памяти Java и продолжают накапливаться, пока операционная система не завершит работу приложения.
Как профилировать изолированный процесс видеообработки с помощью интерфейса командной строки
При запуске dumpsys meminfo с названием пакета приложения выводится только информация о памяти для основного процесса хоста. Чтобы проверить изолированный процесс отрисовки, в котором обрабатываются веб-страницы:
Найдите идентификатор процесса (PID) изолированного сервиса отрисовки:
adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"В выходных данных будет показана запись изолированного процесса и его PID RENDERER_PID (например,
22155):Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}Чтобы проверить, как память распределяется в процессе отрисовки, используйте его PID:
adb shell dumpsys meminfo <var>RENDERER_PID</var>Чтобы оценить влияние на браузер, проверьте процесс хост-приложения:
adb shell dumpsys meminfo <var>PACKAGE_NAME</var>
Проверка карт памяти и распределения памяти
Чтобы узнать, какие нативные подсистемы или распределители занимают анонимную память, проверьте карты памяти процесса:
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"В таблице ниже перечислены распространенные теги анонимной памяти и их связь с ростом потребления памяти.
| Тег воспоминания | Подсистема | Релевантность контенту приложения и сайта | Распространенная причина увеличения объема памяти |
|---|---|---|---|
[anon:partition_alloc] |
Chromium PartitionAlloc | Распределение ресурсов для деревьев DOM, буферов отрисовки, кучи JavaScript V8 и выполнения WebAssembly в WebView. |
Да (высокая). Этот тег может быть завышен, если загружаются тяжелые веб-страницы, DOM с большим количеством медиаконтента или если не вызывается функция destroy() для удаленных экземпляров WebView. |
[anon:scudo...] или [anon:libc_malloc] |
Выделители нативной памяти Android (Scudo / jemalloc) | Общие нативные выделения памяти C/C++, используемые библиотеками NDK, мостами JNI и нативными графическими конвейерами. | Да (средний или высокий уровень). Рост происходит, когда собственные оболочки JNI или сторонние зависимости C++ сохраняют невысвобожденные выделения при переходах. |
[anon:...] (например, [anon:quickjs_heap...]) |
Специальные скрипты или нативные среды выполнения | Встроенные движки JavaScript, специальные среды выполнения WebAssembly или специальные пулы буферов. | Да (зависит от контекста). Распространено в гибридных приложениях, которые выполняют движки сценариев вместе с собственными представлениями и не очищают привязки времени выполнения. |
Ограничения API для работы с памятью в приложении
API памяти в приложении (например, Debug.getMemoryInfo или ActivityManager.getProcessMemoryInfo) измеряют только вызывающий процесс.
В многопроцессном режиме эти API не могут получить данные об объеме памяти, используемом изолированным процессом отрисовки. Чтобы получить точную оценку общего объема памяти, используйте системные инструменты, такие как dumpsys meminfo, Perfetto или профилировщик Android Studio.
Как устранять проблемы с высоким потреблением памяти в гибридном приложении
При диагностике необъяснимого роста памяти во время повторяющихся взаимодействий WebView (например, при открытии веб-ссылок или переходе по веб-каналам) используйте следующий рабочий процесс, чтобы определить, где возникает утечка: на уровне Java или в собственном движке.
Определите тип утечки (Java или нативный код). Запустите
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"до и после многократных переходов пользователя (например, открытия и закрытия веб-статей или пролистывания фидов).- Наблюдение. Если количество
ActivitiesиWebViewsостается стабильным (например, 1–2 активных экземпляра), значит приложение не допускает утечки контекстовActivityили экземпляровWebViewJava.
- Наблюдение. Если количество
Отслеживание изменений памяти при взаимодействиях (временные ряды). Сделайте
dumpsys meminfoснимков при различных взаимодействиях пользователя, чтобы рассчитать скорость выделения памяти при переходах:- Наблюдение. Куча Java остается ограниченной и в хорошем состоянии (увеличивается во время использования и уменьшается после сборки мусора), но Другие частные данные и Собственная куча постоянно увеличиваются на несколько мегабайт при каждом переходе. Это доказывает, что утечка происходит в собственной памяти за пределами среды выполнения ART.
Стандартные дампы кучи Java (
.hprof) не покажут никаких проблем.
- Наблюдение. Куча Java остается ограниченной и в хорошем состоянии (увеличивается во время использования и уменьшается после сборки мусора), но Другие частные данные и Собственная куча постоянно увеличиваются на несколько мегабайт при каждом переходе. Это доказывает, что утечка происходит в собственной памяти за пределами среды выполнения ART.
Стандартные дампы кучи Java (
Проверьте карты анонимной памяти. Изучите карты памяти процесса с помощью ADB (см. Проверка карт памяти и распределения):
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"- Наблюдение. Рост памяти сосредоточен в
[anon:partition_alloc]или встроенных кучах движка скриптов и сопровождается медленным ростом глобальных ссылок JNI. Это означает, что представления Java были заменены, но базовые объекты нативных страниц или привязки JavaScript не были освобождены.
- Наблюдение. Рост памяти сосредоточен в
Устранение нарушений
- Убедитесь, что каждый утилизированный или выброшенный
WebViewявно останавливает активные скрипты (stopLoading()), очищает историю и вызываетdestroy(). - Удалять обратные вызовы пользовательского моста JavaScript или глобальные ссылки JNI, связанные с закрытыми представлениями.
- Убедитесь, что
Private Otherи процесс RSS стабилизируются после перехода.
- Убедитесь, что каждый утилизированный или выброшенный
Дополнительные ресурсы
Чтобы узнать больше об отладке и профилировании памяти и WebView производительности, ознакомьтесь со следующими ресурсами: