Управление и диагностика памяти WebView

WebView выполняет нативный код в нескольких процессах для отображения веб-контента в вашем Android-приложении. Оставление экземпляров WebView без управления может привести к утечкам памяти, сбоям из-за нехватки памяти (OOM) и снижению производительности приложения.

В этом документе объясняется модель многопроцессной работы с памятью 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), механизм Android Low Memory Killer (LMK) начинает завершать фоновые процессы для освобождения памяти. Это ухудшает общую многозадачность устройства, прежде чем в конечном итоге завершить работу приложения на переднем плане.

Управление жизненным циклом WebView

Правильное управление жизненным циклом имеет решающее значение для предотвращения утечек памяти. Распространенная ошибка заключается в предположении, что удаление WebView из макета или завершение Activity автоматически освобождает его память.

Очистка экземпляров WebView

Для обеспечения корректного завершения работы и освобождения ресурсов при уничтожении вашего Activity или Fragment :

  1. Удалите WebView из родительского контейнера ( ViewGroup ).
  2. Остановить активную загрузку и очистить историю навигации.
  3. Вызовите метод destroy() .
  4. Удалите ссылку на null .

Следующий пример демонстрирует, как правильно выполнить очистку WebView :

Котлин

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): общий объем физической оперативной памяти, выделенной для процесса, включая общий код и библиотеки (обозначается как Total в профилировщике Android Studio).

  • Анонимный RSS (RssAnon): Память, выделяемая непосредственно процессом и не связанная с файлом на диске (например, собственная куча и выделение памяти во время выполнения JavaScript). Это основная часть памяти, используемой для веб-контента (обозначается как «Выделено» в профилировщике Android Studio).

  • Объем используемой частной памяти (PMF): сумма объема анонимной оперативной памяти (RSS) и памяти подкачки (zRAM). PMF отражает фактическую неизменяемую нагрузку на память, которую ваше приложение оказывает на систему.

  • Сравнение PMF браузера и PMF рендерера: объем памяти, используемый основным процессом приложения, и объем памяти, используемый изолированным процессом рендеринга. Интенсивный веб-контент вызывает всплески нагрузки, в основном, в процессе рендеринга.

  • Количество активных объектов ( WebViews , Activities , Views ): число активных экземпляров пользовательского интерфейса, контекста и WebView , хранящихся в памяти. Отслеживание этих показателей позволяет определить, вызван ли рост объема памяти сохранением ссылок на Java или выделением памяти только для нативных приложений.

  • Разделы "Частная память, прочее" и "Нативная куча": В dumpsys meminfo нативные выделения памяти C/C++ и пользовательские отображения памяти (например, Chromium PartitionAlloc или кучи встроенной среды выполнения JavaScript) отображаются в разделах "Нативная куча" и "Частная память, прочее", а не в разделе "Куча Java".

Для получения дополнительной информации о счетчиках памяти процессов и их категориях см. глоссарий терминов, относящихся к памяти процессов .

Практические диагностические алгоритмы

Поскольку WebView работает в многопроцессной среде и выделяет собственную память, для анализа его потребления памяти используйте следующие инструменты и методы:

Инструменты профилирования и диагностики

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

  • Профилировщик памяти Android Studio: используйте профилировщик памяти для визуализации выделения памяти в нативных приложениях, отслеживания категорий памяти с течением времени и обнаружения утечек Activity при переходах между экранами.

  • Отслеживание использования памяти с помощью Perfetto: Используйте Perfetto для записи системных счетчиков памяти (таких как RSS и Anonymous 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

Повторите целевое взаимодействие с пользователем (например, открытие и закрытие веб-экрана) несколько раз и сравните полученные значения:

  • Утечка экземпляра: если WebViews или Activities увеличиваются при каждой навигации и не возвращаются к базовому значению, в вашем приложении происходит утечка Java-обертки или хост- Activity (например, отсутствует ViewGroup.removeView() или ссылки на слушатели). Поскольку утечка Activity фиксирует все дерево представлений и декодированные ресурсы изображений в памяти, повторные посещения быстро исчерпают Java-кучу и приведут к сбоям OutOfMemoryError .

  • Утечка нативных ресурсов или DOM: Если количество WebViews и Activities остается неизменным, в то время как общий RSS процесса и Private Other продолжают расти, утечка возникает из-за невысвобожденных нативных ресурсов, элементов DOM или привязок движка JavaScript. Поскольку эти выделения находятся в нативной памяти и обходят сборщик мусора ART, они остаются невидимыми для стандартных инструментов обнаружения утечек Java и продолжают накапливаться до тех пор, пока операционная система не завершит работу приложения.

Проанализируйте изолированный процесс рендеринга с помощью командной строки.

Запуск dumpsys meminfo с указанием имени пакета вашего приложения выводит информацию только об объеме памяти основного процесса хоста. Чтобы проверить изолированный процесс рендеринга, в котором отображаются веб-страницы:

  1. Найдите идентификатор процесса (PID) изолированной службы рендеринга:

    adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"

    В выходных данных отображается запись об изолированном процессе и его PID (например, 22155 ):

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. Проанализируйте распределение памяти процесса рендеринга, используя его PID:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. Проанализируйте процесс основного приложения, чтобы оценить объем данных, предоставляемых браузером:

    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 Profiler.

Распределение ресурсов, выделяемых на высокопроизводительные решения, в гибридном приложении

При диагностике необъяснимого роста объема памяти во время повторяющихся взаимодействий WebView (например, при открытии веб-ссылок или навигации по веб-лентам) используйте следующий алгоритм анализа, чтобы определить, находится ли утечка памяти на уровне Java или в нативном движке:

  1. Определите тип утечки памяти (Java или нативная версия): выполните команду dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" до и после многократных переходов пользователя (например, открытия и закрытия веб-статей или пролистывания ленты).

    • Наблюдение: Если количество Activities и WebViews остается стабильным (например, 1–2 активных экземпляра), приложение не допускает утечки контекстов Activity или Java WebView оберток.
  2. Измерение разницы в объеме памяти между взаимодействиями (отслеживание временных рядов): Получение снимков памяти dumpsys meminfo для нескольких взаимодействий пользователей с целью расчета скорости выделения памяти при каждом переходе:

    • Наблюдение: объем кучи Java остается ограниченным и стабильным (скачки происходят во время использования, а сброс — после сборки мусора), но объем частной и нативной кучи неуклонно увеличивается на несколько мегабайт за каждый переход. Это доказывает, что утечка полностью происходит в нативной памяти вне среды выполнения ART. Стандартные дампы кучи Java ( .hprof ) не показывают никаких проблем.
  3. Проверка анонимных карт памяти: изучите карты памяти процесса с помощью ADB (см. Проверка карт памяти и выделений ):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • Наблюдение: Рост объема памяти сосредоточен в [anon:partition_alloc] или кучах встроенного скриптового движка, сопровождаемый медленным увеличением глобальных ссылок JNI. Это указывает на то, что, хотя представления Java были заменены, базовые нативные объекты страниц или привязки JavaScript не были освобождены.
  4. Очистка:

    • Убедитесь, что каждый повторно используемый или удаленный WebView явно останавливает активные скрипты ( stopLoading() ), очищает историю и вызывает destroy() .
    • Удалите пользовательские коллбэки JavaScript-моста или глобальные ссылки JNI, связанные с закрытыми представлениями.
    • Подтвердите, что Private Other и процесс RSS стабилизируются после переходов между навигационными окнами.

Дополнительные ресурсы

Для получения дополнительной информации об отладке и профилировании памяти и производительности WebView см. следующие ресурсы: