Эффективно управляйте состоянием WebView

При управлении жизненным циклом приложения Android сохранение состояния пользователя во время восстановления фоновых ресурсов является основным компонентом бесперебойной работы пользователя. Для приложений, включающих веб-процессы, WebView.saveState(Bundle) позволяет сериализовать историю навигации и состояние WebView в Bundle. Эти данные можно восстановить с помощью WebView.restoreState(Bundle).

Однако при стандартной реализации могут возникнуть ограничения на размер транзакции во время сеансов с высокой активностью. На этой странице описываются эти архитектурные ограничения и предлагаются стратегии предотвращения исключений, связанных с памятью, при сохранении истории навигации.

Ограничение на размер транзакции в 1 МБ и удаление состояния

В Android действует строгое ограничение на общий объем данных, которые можно хранить в savedInstanceState, – 1 МБ. Этот лимит в 1 МБ распространяется на весь процесс приложения. Если в приложении используется несколько экземпляров WebView, их общее состояние навигации и история должны помещаться в этом общем выделенном объеме памяти. Если этот лимит превышен, возникает ошибка TransactionTooLargeException, которая приводит к сбою приложения.

Распространенная, но проблематичная стратегия смягчения последствий включает мониторинг размера пакета состояния WebView и полную очистку истории WebView, если она пересекает произвольный порог безопасности (например, 300 КБ). Хотя это предотвращает сбой, это вносит серьезные регрессии в пользовательский опыт:

  • Потеря навигации назад. Android часто завершает фоновые процессы приложений, чтобы освободить память для других задач. Чтобы сохранить историю навигации, используйте saveState(Bundle) в обратном вызове жизненного цикла onSaveInstanceState(). Если вы очистите эту историю, чтобы избежать ограничения на транзакции в 1 МБ, весь стек навигации будет потерян. Когда пользователь возвращается в приложение, системная кнопка "Назад" сразу закрывает компонент или приложение, поскольку контекст истории не сохраняется и навигация назад невозможна, независимо от того, был ли перезапущен процесс.

  • Аннулирование кеша BFCache. Очистка истории не позволяет приложению использовать кеш BFCache, что лишает его возможности мгновенно отображать ранее посещенные страницы.

  • Увеличение задержки. Пользователи теряют текущее состояние в WebView, что требует полной повторной навигации и повторной инициализации. Это значительно увеличивает нагрузку на сеть и задержку транзакций.

Стратегии устранения уязвимостей в архитектуре

Чтобы предотвратить сбои TransactionTooLargeException, не ухудшая удобство работы пользователей из-за полного удаления истории, необходимо поддерживать строгий баланс между сохранением состояния и эффективностью памяти. Реализовав следующие стратегии оптимизации, вы сможете безопасно управлять бюджетом транзакций в 1 МБ, сохраняя при этом важную историю навигации и целостность сеанса.

Принудительно ограничить размер сериализации состояния

Вместо того чтобы полностью очищать стек навигации, когда он становится слишком большим, более эффективным решением будет усечение исторических данных:

  • Целевое удаление. Используйте WebViewCompat.saveState(), чтобы сериализовать состояние и применить ограничение на размер в байтах (например, WebViewCompat.saveState(webView, outState, maxSizeBytes)). Этот API автоматически удаляет старые записи навигации, пока общий размер полезной нагрузки не станет соответствовать заданному ограничению. Важно, что при этом усекается только сериализованный объект Bundle, а текущая история активного объекта WebView не изменяется и не очищается. Это гарантирует, что немедленный переход назад будет работать без проблем.

  • Удаление записей о переходе вперед. Если в интерфейсе приложения есть кнопка "Назад", но нет кнопки "Вперед", вы можете удалить все записи о переходе вперед, задав для параметра includeForwardState в API saveState значение false. Это значительно уменьшает размер полезной нагрузки, не влияя на доступные пользователю пути навигации.

Как управлять задержкой ресурсов с помощью HTTP Cache Quota API

saveState управляет ограничением в 1 МБ для Bundle в истории навигации, а HTTP Cache Quota API позволяет вручную управлять сохраненными веб-ресурсами (кешем диска) для каждого профиля. Это позволяет четко разграничить краткосрочный контекст навигации и долгосрочные кэшированные ресурсы.

Выбор подходящей квоты включает в себя компромисс в отношении производительности:

  • Увеличенные квоты повышают доступность в офлайн-режиме и сокращают задержку при загрузке ресурсов, поскольку на диске хранится больше объектов.
  • Меньшие квоты минимизируют занимаемое приложением место на диске и предотвращают удаление операционной системой кеша других важных данных приложения.

Эти настройки сохраняются при перезапуске приложения и должны быть заданы в основном потоке.

В примере ниже показано, как настроить квоту кеша диска для профиля по умолчанию.

Kotlin

if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) &&
    WebViewFeature.isFeatureSupported(WebViewFeature.HTTP_CACHE)) {
    val defaultProfile = ProfileStore.getInstance()
        .getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME)
    val httpCache = defaultProfile.httpCache

    // Set explicit cache size to 50MB (50 * 1024 * 1024 bytes)
    httpCache.setQuotaBytes(50L * 1024 * 1024)
}

Java

if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) &&
    WebViewFeature.isFeatureSupported(WebViewFeature.HTTP_CACHE)) {
    Profile defaultProfile = ProfileStore.getInstance()
        .getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME);
    HttpCache httpCache = defaultProfile.getHttpCache();

    // Set explicit cache size to 50MB (50 * 1024 * 1024 bytes)
    httpCache.setQuotaBytes(50L * 1024 * 1024);
}

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

Ключевые факторы эффективности

Ниже перечислены технические ограничения и особенности обработки внутренних данных, которые определяют поведение состояния WebView.

  • Непрозрачные BLOB-объекты PageState. Около 70% данных, хранящихся в saveState, – это внутренние BLOB-объекты PageState из механизма рендеринга. Эти данные содержат подробные сведения о состоянии сеанса, включая входные данные формы и позиции прокрутки iframe. Не пытайтесь вручную анализировать или удалять отдельные сегменты из этих объектов, так как это создает серьезные риски для безопасности и нарушает целостность восстановления сеанса.

  • Точное управление историей. Стандартный API WebBackForwardList не поддерживает произвольное удаление отдельных элементов истории. Для строгого управления состоянием необходимо реализовать стратегии усечения с помощью параметров maxSizeBytes и includeForwardState в WebViewCompat.saveState(), чтобы обеспечить безопасность архитектуры.