Оптимизация запуска WebView

Когда ваше приложение впервые использует WebView, система выполняет определенные задачи при запуске. Этот процесс запуска является ресурсоемким. По умолчанию он происходит неявно в потоке пользовательского интерфейса при первом вызове приложением многих API из пакетов android.webkit или androidx.webkit , или при создании макета, содержащего тег WebView.

Почему это важно

Because this implicit startup happens entirely on the main thread, it blocks your app from processing user input and drastically increases the risk of Application Not Responding (ANR) errors. For more information about how Android handles the single-threaded execution model, see Processes and threads overview .

Триггеры для неявного запуска

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

  • Программно : Вызов API, таких как WebSettings.getUserAgentString() .
  • Использование макетов : вызов setContentView() или layoutInflater.inflate() для XML-ресурса, содержащего элемент <WebView> .

Неявная инициализация также может негативно повлиять на ваши бизнес-показатели, такие как время запуска приложения и время до первого отображения. Если неявная инициализация не оптимальна для вашего приложения, используйте вместо неё startUpWebView .

На этой странице обсуждается, как оптимизировать производительность запуска WebView с помощью API startUpWebView .

Возьмите под контроль запуск WebView.

Для повышения производительности и минимизации ошибок ANR используйте API startUpWebView , доступный в библиотеке Jetpack Webkit. Этот API предоставляет вам явный контроль над моментом запуска WebView. Он переносит значительную часть нагрузки по запуску в фоновый поток и позволяет выполнять любую работу, которая должна выполняться в потоке пользовательского интерфейса, поэтапно, а не одним большим монолитным блоком. Это освобождает поток пользовательского интерфейса для параллельной обработки других важных задач приложения, снижая вероятность блокировки пользовательского интерфейса.

API использует функцию обратного вызова androidx.webkit.WebViewOutcomeReceiver , позволяющую отслеживать успешную инициализацию.

Для использования этого API добавьте библиотеку Jetpack Webkit в файл build.gradle . Убедитесь, что вы используете версию 1.16.0 или выше:

dependencies {
    implementation("androidx.webkit:webkit:1.16.0")
}

Используйте API startUpWebView

Способ оптимизации процесса запуска зависит от того, когда вашему приложению действительно необходимо отобразить WebView.

Когда WebView не находится на критическом пути

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

В идеале, перед вызовом других API WebView следует дождаться завершения обратного вызова. Если вы запускаете startUpWebView , но не дожидаетсяе его завершения, прежде чем обращаться к другим компонентам WebView, система блокирует поток пользовательского интерфейса, ожидая завершения инициализации. Ваше приложение может получить некоторую выгоду в производительности от уже выполненной фоновой работы, но не максимальную.

Когда WebView находится на критическом пути

Если основной пользовательский сценарий вашего приложения требует немедленного доступа к WebView, вы, вероятно, не можете позволить себе ждать завершения запуска WebView. В этом случае вам все равно следует вызывать startUpWebView как можно раньше в жизненном цикле приложения (например, в Application.onCreate ), но не ждать срабатывания обратного вызова. Вместо этого используйте API WebView напрямую, когда это необходимо.

Чтобы получить максимальную выгоду от асинхронного запуска, крайне важно отложить создание экземпляра WebView или вызов API WebView до тех пор, пока не останется других критически важных операций в потоке пользовательского интерфейса (таких как создание иерархий макета, инициализация других SDK или отрисовка начального кадра).

Если вызвать startUpWebView и сразу же после этого вызвать API WebView в основном потоке, поток пользовательского интерфейса заблокируется, ожидая завершения инициализации. В этом случае повышения производительности не будет.

Если использование WebView становится критически важным, но вы не хотите запускать WebView полностью, вы можете выборочно запускать задачи запуска WebView, которые могут выполняться в фоновом потоке, освобождая поток пользовательского интерфейса для других критически важных задач приложения. Для этого можно использовать shouldRunUiThreadStartUpTasks(false) .

Later in your app's lifecycle, you can call startUpWebView again with shouldRunUiThreadStartUpTasks(true) to finish the remaining startup tasks on the UI thread. Whether you wait for the callback at that point depends on whether WebView usage is on the critical path.

Пример реализации

API использует функцию обратного вызова androidx.webkit.WebViewOutcomeReceiver , позволяющую отслеживать успешную инициализацию или обрабатывать диагностические ошибки.

Вызов функции startUpWebView несколько раз из разных частей приложения безопасен. Мы рекомендуем избегать реализации наивного цикла повторных попыток.

Приведенный ниже пример кода демонстрирует, как использовать API WebViewCompat.startUpWebView для асинхронной инициализации.

Котлин

import android.content.Context
import android.util.Log
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewOutcomeReceiver
import androidx.webkit.WebViewStartUpConfig
import androidx.webkit.WebViewStartUpResult
import androidx.webkit.WebViewStartupException
import java.util.concurrent.Executors

fun initializeWebView(context: Context) {
    // 1. Create a startup configuration specifying the background thread
    // that WebView will use to run its initialization tasks.
    val startUpConfig = WebViewStartUpConfig.Builder(
        Executors.newSingleThreadExecutor()
    ).build()

    // 2. Trigger WebView startup asynchronously
    WebViewCompat.startUpWebView(
        context,
        startUpConfig,
        object : WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException> {

            override fun onResult(result: WebViewStartUpResult) {
                // Success: The WebView has finished its background initialization.
                // This callback is guaranteed to be invoked on the UI thread.
                setupWebView()
            }

            override fun onError(error: WebViewStartupException) {
                // Failure: The initialization encountered a startup exception.
                Log.e("WebViewStartup", "Failed to initialize WebView", error)
            }
        }
    )
}

Java

import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.webkit.WebViewCompat;
import androidx.webkit.WebViewOutcomeReceiver;
import androidx.webkit.WebViewStartUpConfig;
import androidx.webkit.WebViewStartUpResult;
import androidx.webkit.WebViewStartupException;
import java.util.concurrent.Executors;

public void initializeWebView(Context context) {
    // 1. Create the startup configuration specifying the background thread pool
    // to handle internal non-UI initialization processes.
    WebViewStartUpConfig startUpConfig = new WebViewStartUpConfig.Builder(
            Executors.newSingleThreadExecutor()
    ).build();

    // 2. Trigger WebView startup asynchronously
    WebViewCompat.startUpWebView(
            context,
            startUpConfig,
            new WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException>() {

                @Override
                public void onResult(@NonNull WebViewStartUpResult result) {
                    // Success: The WebView has finished its background initialization.
                    // This callback is invoked directly on the UI thread.
                    setupWebView();
                }

                @Override
                public void onError(@NonNull WebViewStartupException error) {
                    // Failure: Handled using the concrete WebViewStartupException
                    Log.e("WebViewStartup", "Failed to initialize WebView", error);
                }
            }
    );
}

Отладка проблем асинхронного запуска

Если startUpWebView не дает ожидаемого повышения производительности, это часто происходит потому, что WebView инициализируется неявно в другом месте вашего приложения до выполнения вашего вызова. Это может быть связано со следующими причинами:

  • Сторонние библиотеки или SDK инициализируются на ранних этапах жизненного цикла приложения.

  • В ваш APK-файл внедряются ContentProviders , которые запускают API WebView во время запуска приложения.

  • Неожиданно рано происходят изменения в макете или программные вызовы (например, получение строк пользовательского агента).

Для того чтобы помочь вам определить, где и почему происходят эти неожиданные инициализации, объект WebViewStartUpResult предоставляет встроенные возможности аудита:

  • getUiThreadBlockingStartUpLocations() : Возвращает список объектов StartUpLocation , представляющих места, где задачи запуска WebView блокировали основной поток пользовательского интерфейса.

  • getNonUiThreadBlockingStartUpLocations() : Возвращает конкретные места вызовов, где выполнение задач запуска блокировало фоновые потоки.

Each StartUpLocation contains a stack trace that you can log or inspect to find the exact class and method that triggered the initialization.

Пример реализации

Вы можете проверить эти места внутри функции обратного вызова onResult , чтобы проанализировать путь запуска:

override fun onResult(result: WebViewStartUpResult) {
    // Check if WebView startup was blocked on the UI thread prior to or during initialization
    val uiBlockingLocations = result.getUiThreadBlockingStartUpLocations()
    if (!uiBlockingLocations.isNullOrEmpty()) {
        for (location in uiBlockingLocations) {
            // Log the stack trace of the call site that triggered the UI-blocking startup
            Log.w("WebViewDebug", "WebView startup blocked the UI thread here:", location.getStack())
        }
    } else {
        Log.i("WebViewDebug", "Excellent! No UI-blocking WebView startup detected.")
    }

    // Check where background initialization tasks were executed
    val backgroundLocations = result.getNonUiThreadBlockingStartUpLocations()
    backgroundLocations?.forEach { location ->
        Log.d("WebViewDebug", "WebView background startup occurred at: ${location.getStack()}")
    }

    setupWebView()
}

Как использовать эти данные во время аудита

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

  • Обратите внимание на неожиданные трассировки стека: если getUiThreadBlockingStartUpLocations() не пуст, посмотрите на выведенные трассировки стека. Если вы видите классы, принадлежащие сторонним SDK, или неожиданные компоненты, значит, вы обнаружили неявное узкое место инициализации.

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

Перейдите на другую платформу, используя предыдущие обходные решения.

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

Эти обходные пути считаются неподдерживаемыми методами, и их поведение может измениться в будущих релизах. Если ваше приложение использует какие-либо явные, недокументированные обходные пути для запуска или управления запуском WebView, мы рекомендуем использовать API startUpWebView . API startUpWebView работает со всеми версиями Android и WebView, поддерживаемыми библиотекой Jetpack Webkit.

Использование реализации Jetpack Webkit помогает обеспечить согласованное поведение во всей экосистеме Android. Ключевым преимуществом этого API является его отказоустойчивость: на устаревших устройствах, где новые оптимизации недоступны, API поддерживает паритет производительности с ручными обходными путями. Это позволяет использовать преимущества современных функций автозапуска на новых устройствах без снижения производительности на старых.

Если у вас возникли проблемы или есть замечания по API startUpWebView , сообщите об ошибке в общедоступном трекере проблем .