Ottimizzare l'avvio di WebView

Quando la tua app utilizza per la prima volta un componente WebView, il sistema esegue attività di avvio specifiche. Questa procedura di avvio è pesante. Per impostazione predefinita, si verifica implicitamente sul thread UI la prima volta che l'applicazione chiama molte API all'interno dei pacchetti android.webkit o androidx.webkit o gonfia un layout che contiene un tag WebView.

Perché è importante

Poiché questo avvio implicito avviene interamente sul thread principale, impedisce all'app di elaborare l'input dell'utente e aumenta drasticamente il rischio di errori ANR (L'applicazione non risponde). Per saperne di più su come Android gestisce il modello di esecuzione a thread singolo, consulta la Panoramica di processi e thread.

Trigger per l'avvio implicito

L'avvio implicito può essere attivato nei seguenti modi:

  • A livello di programmazione: chiamando API come WebSettings.getUserAgentString().
  • Utilizzo dei layout: chiamata di setContentView() o layoutInflater.inflate() su una risorsa XML che include un <WebView>.

L'avvio implicito può anche influire negativamente sulle metriche aziendali, come il tempo di avvio dell'app e il tempo al primo display. Se l'inizializzazione implicita non è ottimale per la tua app, utilizza startUpWebView.

Questa pagina illustra come ottimizzare le prestazioni di avvio di WebView utilizzando l'API startUpWebView.

Controllare l'avvio di WebView

Per migliorare le prestazioni e ridurre al minimo gli errori ANR, utilizza l'API startUpWebView disponibile nella libreria Jetpack Webkit. Questa API ti offre il controllo esplicito sull'avvio di WebView. Sposta una quantità significativa del carico di lavoro di avvio su un thread in background e consente di eseguire qualsiasi lavoro che deve essere eseguito sul thread dell'interfaccia utente in blocchi, anziché in un unico blocco monolitico di grandi dimensioni. In questo modo, il thread della UI può gestire in parallelo altre attività critiche dell'app, riducendo la possibilità di bloccare l'esperienza utente.

L'API utilizza il callback androidx.webkit.WebViewOutcomeReceiver, che ti consente di monitorare le inizializzazioni riuscite.

Per utilizzare questa API, aggiungi la libreria Jetpack Webkit al file build.gradle. Assicurati di utilizzare la versione 1.16.0 o successive:

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

Utilizzare l'API startUpWebView

Il modo in cui ottimizzi il flusso di avvio dipende dal momento in cui l'app deve effettivamente visualizzare la WebView.

Quando WebView non si trova nel percorso critico

Se la tua app non deve caricare immediatamente un componente WebView, puoi nascondere completamente il costo di inizializzazione. Chiama startUpWebView all'inizio del ciclo di vita della tua app e attendi l'attivazione del callback di successo.

Idealmente, dovresti attendere il callback prima di chiamare altre API WebView. Se attivi startUpWebView ma non aspetti che termini prima di toccare altri componenti WebView, il sistema blocca il thread dell'interfaccia utente in attesa del completamento dell'inizializzazione. La tua app potrebbe ottenere alcuni vantaggi in termini di prestazioni dal lavoro in background già completato, ma non il massimo vantaggio.

Quando WebView si trova nel percorso critico

Se il percorso utente principale della tua app richiede immediatamente un componente WebView, probabilmente non puoi permetterti di attendere il completamento dell'avvio di WebView. In questo scenario, devi comunque chiamare startUpWebView il prima possibile nel ciclo di vita dell'app (ad esempio in Application.onCreate), ma non aspettare che venga attivato il callback. Utilizza invece direttamente le API WebView quando sono necessarie.

Per ottenere il massimo vantaggio dall'avvio asincrono, rimanda l'istanza di una WebView o la chiamata alle API WebView finché non sono rimaste altre operazioni del thread dell'interfaccia utente del percorso critico da eseguire (come l'esecuzione dell'inflate delle gerarchie di layout, l'inizializzazione di altri SDK o il disegno del frame iniziale).

Se chiami startUpWebView e invochi immediatamente le API WebView sul thread principale, il thread dell'interfaccia utente si blocca in attesa del completamento dell'inizializzazione. In questo scenario, non ci sono vantaggi in termini di rendimento.

Se l'utilizzo di WebView può diventare un percorso critico, ma non vuoi avviare WebView completamente, puoi scegliere di eseguire selettivamente le attività di avvio di WebView che possono essere eseguite su un thread in background, liberando il thread dell'interfaccia utente per altre attività critiche dell'app. Per farlo, puoi utilizzare shouldRunUiThreadStartUpTasks(false).

In un secondo momento del ciclo di vita dell'app, puoi chiamare di nuovo startUpWebView con shouldRunUiThreadStartUpTasks(true) per completare le attività di avvio rimanenti sul thread dell'interfaccia utente. Se attendere il callback a questo punto dipende dal fatto che l'utilizzo di WebView si trovi o meno nel percorso critico.

Esempio di implementazione

L'API utilizza il callback androidx.webkit.WebViewOutcomeReceiver, che ti consente di monitorare le inizializzazioni riuscite o gestire gli errori di diagnostica.

È sicuro chiamare startUpWebView più volte da diverse parti dell'app. Ti consigliamo di evitare di implementare un ciclo di nuovi tentativi ingenuo.

Il seguente esempio di codice mostra come utilizzare l'API WebViewCompat.startUpWebView per l'inizializzazione asincrona.

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

Eseguire il debug dei problemi di avvio asincrono

Se startUpWebView non produce i vantaggi in termini di rendimento previsti, spesso è perché WebView viene inizializzato implicitamente altrove nell'app prima dell'esecuzione della chiamata. Le cause potrebbero essere le seguenti:

  • Librerie di terze parti o SDK inizializzati all'inizio del ciclo di vita dell'app.

  • ContentProviders inserito nel tuo APK che attiva le API WebView durante l'avvio dell'app.

  • Gonfiature del layout o chiamate programmatiche (come il recupero di stringhe user agent) che si verificano inaspettatamente in anticipo.

Per aiutarti a diagnosticare dove e perché si verificano queste inizializzazioni impreviste, l'oggetto WebViewStartUpResult fornisce funzionalità di controllo integrate:

  • getUiThreadBlockingStartUpLocations(): restituisce un elenco di oggetti StartUpLocation che rappresentano le posizioni in cui le attività di avvio di WebView hanno bloccato il thread dell'interfaccia utente principale.

  • getNonUiThreadBlockingStartUpLocations(): restituisce siti di chiamate specifici in cui l'esecuzione di attività di avvio ha bloccato i thread in background.

Ogni StartUpLocation contiene un'analisi dello stack che puoi registrare o ispezionare per trovare la classe e il metodo esatti che hanno attivato l'inizializzazione.

Esempio di implementazione

Puoi esaminare queste posizioni all'interno del callback onResult per controllare il percorso di avvio:

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

Come utilizzare questi dati durante un audit

Quando esegui l'audit dell'avvio di WebView della tua app, utilizza le seguenti strategie per analizzare i dati di diagnostica e risolvere i colli di bottiglia delle prestazioni:

  • Cerca stack trace imprevisti:se getUiThreadBlockingStartUpLocations() non è vuoto, esamina gli stack trace stampati. Se visualizzi classi appartenenti a SDK di terze parti o componenti imprevisti, hai trovato un collo di bottiglia di inizializzazione implicita.

  • Verifica l'ordine di chiamata:se gli output dei log mostrano che si è verificata un'inizializzazione implicita prima della chiamata startUpWebView manuale, devi spostare l'inizializzazione startUpWebView in un punto precedente dell'app o configurare l'SDK incriminato in modo da ritardare le attività dipendenti da WebView.

Migrazione dalle soluzioni alternative precedenti

In passato, potresti aver utilizzato soluzioni alternative esplicite per forzare l'inizializzazione di WebView su un thread in background, ad esempio recuperando la stringa user agent.

Queste soluzioni alternative sono considerate pratiche non supportate e il loro comportamento sottostante può cambiare nelle release future. Se la tua app si basa su soluzioni alternative esplicite e non documentate per attivare o gestire l'avvio di WebView, ti consigliamo di utilizzare l'API startUpWebView. L'API startUpWebView funziona su tutte le versioni di Android e WebView supportate dalla libreria Jetpack Webkit.

L'utilizzo dell'implementazione di Jetpack Webkit contribuisce a garantire un comportamento coerente in tutto l'ecosistema Android. Un vantaggio fondamentale di questa API è la sua resilienza: sui dispositivi legacy in cui non sono disponibili ottimizzazioni più recenti, l'API mantiene la parità di prestazioni con le soluzioni alternative manuali. In questo modo, puoi adottare i vantaggi moderni delle startup sui dispositivi più recenti senza incorrere in una penalità di rendimento su quelli meno recenti.

Se riscontri problemi o hai feedback sull'API startUpWebView, segnala un bug nel public issue tracker.