Optymalizacja uruchamiania WebView

Gdy aplikacja po raz pierwszy używa WebView, system wykonuje określone zadania uruchamiania. Ten proces uruchamiania jest złożony. Domyślnie odbywa się on niejawnie w wątku interfejsu użytkownika, gdy aplikacja po raz pierwszy wywoła wiele interfejsów API w pakietach android.webkit lub androidx.webkit albo rozwinie układ zawierający tag WebView.

Dlaczego to jest ważne

Ponieważ to niejawne uruchamianie odbywa się w całości w wątku głównym, blokuje ono aplikację przed przetwarzaniem danych wejściowych użytkownika i znacznie zwiększa ryzyko wystąpienia błędów typu „Aplikacja nie odpowiada” (ANR). Więcej informacji o tym, jak Android obsługuje model wykonywania w jednym wątku, znajdziesz w artykule Omówienie procesów i wątków overview.

Przyczyny niejawnego uruchamiania

Niejawne uruchamianie może być wywoływane w te sposoby:

  • Programowo: wywoływanie interfejsów API, takich jak WebSettings.getUserAgentString().
  • Za pomocą układów: wywoływanie setContentView() lub layoutInflater.inflate() w zasobie XML, który zawiera <WebView>.

Niejawne uruchamianie może też negatywnie wpływać na dane biznesowe, takie jak czas uruchamiania aplikacji i czas do pierwszego wyświetlenia. Jeśli niejawna inicjalizacja nie jest optymalna dla Twojej aplikacji, użyj startUpWebView zamiast niej.

Na tej stronie dowiesz się, jak zoptymalizować wydajność uruchamiania WebView za pomocą interfejsu startUpWebView API.

Przejmij kontrolę nad uruchamianiem WebView

Aby zwiększyć wydajność i zminimalizować liczbę błędów ANR, użyj interfejsu startUpWebView API dostępnego w bibliotece Jetpack Webkit. Ten interfejs API daje Ci pełną kontrolę nad tym, kiedy uruchamia się WebView. Przenosi on znaczną część obciążenia związanego z uruchamianiem do wątku w tle i umożliwia wykonywanie w wątku UI zadań w częściach, a nie w jednym dużym bloku. Dzięki temu wątek interfejsu użytkownika może równolegle obsługiwać inne krytyczne zadania aplikacji, co zmniejsza ryzyko zablokowania interfejsu.

Interfejs API używa wywołania zwrotnego androidx.webkit.WebViewOutcomeReceiver, co pozwala śledzić udane inicjalizacje.

Aby używać tego interfejsu API, dodaj bibliotekę Jetpack Webkit do pliku build.gradle. Upewnij się, że używasz wersji 1.16.0 lub nowszej:

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

Korzystanie z interfejsu startUpWebView API

Sposób optymalizacji procesu uruchamiania zależy od tego, kiedy aplikacja musi wyświetlić WebView.

Gdy WebView nie znajduje się na ścieżce krytycznej

Jeśli aplikacja nie musi od razu wczytywać WebView, możesz całkowicie ukryć koszt inicjalizacji. Wywołaj startUpWebView na wczesnym etapie cyklu życia aplikacji i poczekaj na wywołanie zwrotne.

Najlepiej poczekać na wywołanie zwrotne przed wywołaniem innych interfejsów WebView API. Jeśli wywołasz startUpWebView, ale nie poczekasz na jego zakończenie przed użyciem innych komponentów WebView, system zablokuje wątek UI, czekając na zakończenie inicjalizacji. Aplikacja może uzyskać pewne korzyści z pracy w tle, ale nie maksymalne.

Gdy WebView znajduje się na ścieżce krytycznej

Jeśli podstawowa ścieżka użytkownika w aplikacji wymaga natychmiastowego użycia WebView, prawdopodobnie nie możesz sobie pozwolić na czekanie na zakończenie uruchamiania WebView. W takim przypadku nadal należy wywołać startUpWebView jak najwcześniej w cyklu życia aplikacji (np. w Application.onCreate), ale nie należy czekać na wywołanie zwrotne . Zamiast tego używaj interfejsów WebView API bezpośrednio, gdy są potrzebne.

Aby uzyskać maksymalne korzyści z asynchronicznego uruchamiania, odłóż tworzenie instancji komponentu WebView lub wywoływanie interfejsów WebView API do momentu, gdy nie będzie już żadnych innych operacji w wątku UI na ścieżce krytycznej (takich jak rozszerzanie hierarchii układów, inicjowanie innych pakietów SDK czy rysowanie pierwszej klatki).

Jeśli wywołasz startUpWebView i natychmiast potem wywołasz interfejsy WebView API w wątku głównym, wątek UI zostanie zablokowany, czekając na zakończenie inicjalizacji. W takim przypadku nie ma żadnych korzyści z wydajności.

Jeśli użycie WebView może znaleźć się na ścieżce krytycznej, ale nie chcesz uruchamiać WebView w całości, możesz selektywnie uruchamiać zadania uruchamiania WebView, które mogą być wykonywane w wątku w tle, co zwalnia wątek UI na potrzeby innych krytycznych zadań aplikacji. W tym celu możesz użyć shouldRunUiThreadStartUpTasks(false).

Na późniejszym etapie cyklu życia aplikacji możesz ponownie wywołać startUpWebView z shouldRunUiThreadStartUpTasks(true), aby dokończyć pozostałe zadania uruchamiania w wątku UI. To, czy w tym momencie poczekasz na wywołanie zwrotne, zależy od tego, czy użycie WebView znajduje się na ścieżce krytycznej.

Przykład wdrożenia

Interfejs API używa wywołania zwrotnego androidx.webkit.WebViewOutcomeReceiver, co pozwala śledzić udane inicjalizacje lub obsługiwać błędy diagnostyczne.

Możesz bezpiecznie wywoływać startUpWebView wiele razy z różnych części aplikacji. Zalecamy unikanie implementowania prostego cyklu ponawiania.

Poniższy przykładowy kod pokazuje, jak używać interfejsu WebViewCompat.startUpWebView API do asynchronicznej inicjalizacji.

Kotlin

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);
                }
            }
    );
}

Debugowanie problemów z asynchronicznym uruchamianiem

Jeśli startUpWebView nie przynosi oczekiwanych korzyści z wydajności, często dzieje się tak, ponieważ WebView jest inicjowany niejawnie w innym miejscu aplikacji przed wykonaniem wywołania. Może to być spowodowane tymi przyczynami:

  • Biblioteki innych firm lub pakiety SDK inicjowane na wczesnym etapie cyklu życia aplikacji.

  • ContentProviders wstrzykiwane do pliku APK, które wywołują interfejsy WebView API podczas uruchamiania aplikacji.

  • Rozwijanie układów lub wywołania programowe (np. pobieranie ciągów znaków agenta użytkownika), które występują niespodziewanie wcześnie.

Aby pomóc Ci zdiagnozować, gdzie i dlaczego dochodzi do tych nieoczekiwanych inicjalizacji, obiekt WebViewStartUpResult udostępnia wbudowane funkcje audytu:

  • getUiThreadBlockingStartUpLocations(): zwraca listę obiektów StartUpLocation reprezentujących miejsca, w których zadania uruchamiania WebView blokowały główny wątek interfejsu użytkownika.

  • getNonUiThreadBlockingStartUpLocations(): zwraca konkretne miejsca wywołań, w których uruchamianie zadań blokowało wątki w tle.

Każdy StartUpLocation zawiera zrzut stosu, który możesz zarejestrować lub sprawdzić, aby znaleźć dokładną klasę i metodę, które wywołały inicjalizację.

Przykład wdrożenia

Możesz sprawdzić te lokalizacje w wywołaniu zwrotnym onResult, aby przeprowadzić audyt ścieżki uruchamiania:

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()
}

Jak korzystać z tych danych podczas audytu

Podczas audytu uruchamiania WebView w aplikacji użyj tych strategii, aby analizować dane diagnostyczne i eliminować wąskie gardła wydajności:

  • Szukaj nieoczekiwanych śladów stosu: jeśli getUiThreadBlockingStartUpLocations() nie jest puste, sprawdź wydrukowane ślady stosu. Jeśli widzisz klasy należące do pakietów SDK innych firm lub nieoczekiwane komponenty, oznacza to, że znalazłeś wąskie gardło niejawnej inicjalizacji.

  • Sprawdź kolejność wywołań: jeśli dane wyjściowe logu wskazują, że niejawna inicjalizacja nastąpiła przed ręcznym wywołaniem startUpWebView, przenieś inicjalizację startUpWebView na wcześniejszy etap aplikacji lub skonfiguruj pakiet SDK, który powoduje problem, tak aby opóźnić jego zadania zależne od WebView.

Migracja z poprzednich obejść

W przeszłości mogłeś używać jawnych obejść, aby wymusić inicjalizację WebView w wątku w tle, np. pobierając ciąg znaków agenta użytkownika.

Te obejścia są uważane za nieobsługiwane praktyki, a ich podstawowe działanie może się zmienić w przyszłych wersjach. Jeśli Twoja aplikacja korzysta z jawnych, nieudokumentowanych obejść, aby wywoływać uruchamianie WebView lub nim zarządzać, zalecamy używanie zamiast nich interfejsu startUpWebView API. Interfejs startUpWebView API działa we wszystkich wersjach Androida i WebView obsługiwanych przez bibliotekę Jetpack Webkit.

Korzystanie z implementacji Jetpack Webkit pomaga zapewnić spójne działanie w całym ekosystemie Androida. Kluczową zaletą tego interfejsu API jest jego odporność: na starszych urządzeniach, na których nie są dostępne nowsze optymalizacje, interfejs API zachowuje równą wydajność w porównaniu z ręcznymi obejściami. Dzięki temu możesz korzystać z nowoczesnych funkcji uruchamiania na nowszych urządzeniach bez utraty wydajności na starszych.

Jeśli napotkasz problemy lub masz opinie na temat interfejsu startUpWebView API, zgłoś błąd w publicznym narzędziu do śledzenia problemów.