אופטימיזציה של הפעלת WebView

כשהאפליקציה משתמשת ב-WebView בפעם הראשונה, המערכת מבצעת משימות הפעלה ספציפיות. תהליך ההפעלה הזה כבד. כברירת מחדל, הפעולה הזו מתבצעת באופן מרומז בשרשור ממשק המשתמש בפעם הראשונה שהאפליקציה קוראת ל-API רבים בחבילות android.webkit או androidx.webkit, או כשהיא מרחיבה פריסה שמכילה תג WebView.

למה זה חשוב

מכיוון שההפעלה הסמויה הזו מתרחשת כולה בשרשור הראשי, היא חוסמת את האפליקציה שלכם מעיבוד קלט של משתמשים ומגדילה באופן משמעותי את הסיכון לשגיאות מסוג 'האפליקציה לא מגיבה' (ANR). מידע נוסף על האופן שבו Android מטפל במודל של ביצוע עם שרשור יחיד זמין במאמר סקירה כללית של תהליכים ושרשורים.

טריגרים להפעלה מרומזת

הפעלה מרומזת יכולה להיות מופעלת בדרכים הבאות:

  • באמצעות תכנות: קריאה לממשקי API כמו WebSettings.getUserAgentString().
  • שימוש בפריסות: קריאה ל-setContentView() או ל-layoutInflater.inflate() במשאב XML שכולל <WebView>.

הפעלה מרומזת יכולה גם להשפיע לרעה על המדדים העסקיים, כמו זמן ההפעלה של האפליקציה והזמן עד להצגת המסך הראשון. אם אתם רוצים להשתמש בשיטה אחרת לאתחול של האפליקציה, תוכלו להשתמש ב-startUpWebView.

בדף הזה מוסבר איך לבצע אופטימיזציה של ביצועי ההפעלה של WebView באמצעות startUpWebViewAPI.

שליטה בהפעלה של WebView

כדי לשפר את הביצועים ולצמצם את מספר ה-ANR, אפשר להשתמש ב-startUpWebView API שזמין בספריית Jetpack Webkit. ‫API שמאפשר לכם לשלוט באופן מפורש במועד ההפעלה של WebView. הוא מעביר כמות משמעותית של עומס עבודה בהפעלה לשרשור ברקע, ומאפשר לבצע כל עבודה שצריכה להתבצע בשרשור UI במנות, במקום בבלוק גדול אחד. כך משחררים את ה-UI thread כדי לטפל במקביל במשימות קריטיות אחרות באפליקציה, ומקטינים את הסיכוי לחסימה של חוויית המשתמש.

ממשק ה-API משתמש בקריאה החוזרת androidx.webkit.WebViewOutcomeReceiver, שמאפשרת לכם לעקוב אחרי אתחולים מוצלחים.

כדי להשתמש ב-API הזה, צריך להוסיף את ספריית Jetpack Webkit לקובץ build.gradle. מוודאים שאתם משתמשים בגרסה 1.16.0 ואילך:

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

שימוש ב-startUpWebView API

אופן האופטימיזציה של תהליך ההפעלה תלוי במועד שבו האפליקציה צריכה להציג את WebView.

כש-WebView לא נמצא בנתיב הקריטי

אם האפליקציה לא צריכה לטעון WebView באופן מיידי, אפשר להסתיר לחלוטין את עלות האתחול. מומלץ להתקשר אל startUpWebView בשלב מוקדם במחזור החיים של האפליקציה ולהמתין להפעלת קריאת החזרה (callback) של ההצלחה.

מומלץ להמתין לקריאה החוזרת לפני שמפעילים ממשקי API אחרים של WebView. אם מפעילים את startUpWebView אבל לא מחכים לסיום הפעולה לפני שנוגעים ברכיבים אחרים של WebView, המערכת חוסמת את שרשור ה-UI בזמן ההמתנה לסיום האתחול. יכול להיות שהאפליקציה תהנה משיפור מסוים בביצועים בעקבות העבודה ברקע שכבר הושלמה, אבל לא משיפור מקסימלי.

כאשר WebView נמצא בנתיב הקריטי

אם התהליך המרכזי שהמשתמש עובר באפליקציה שלכם דורש WebView באופן מיידי, כנראה שלא תוכלו להרשות לעצמכם לחכות עד לסיום ההפעלה של WebView. במקרה כזה, עדיין צריך להתקשר אל startUpWebView מוקדם ככל האפשר במחזור החיים של האפליקציה (למשל ב-Application.onCreate), אבל לא צריך לחכות להפעלת הקריאה החוזרת. במקום זאת, השתמשו ישירות בממשקי WebView API כשנדרש.

כדי להפיק את המרב מההפעלה האסינכרונית, חשוב לדחות את יצירת המופע של WebView או את הקריאה לממשקי ה-API של WebView עד שלא יישארו עוד פעולות קריטיות בשרשור UI להרצה (כמו יצירת אוביקט תצוגה של היררכיות של פריסות, הפעלה של SDK-ים אחרים או ציור של המסגרת הראשונית).

אם קוראים ל-startUpWebView ומפעילים מיד לאחר מכן את ממשקי ה-API של WebView ב-thread הראשי, שרשור ה-UI נחסם בהמתנה לאתחול. במקרה כזה, לא יהיה שיפור בביצועים.

אם השימוש ב-WebView יכול להפוך לחלק מהנתיב הקריטי, אבל אתם לא רוצים להפעיל את WebView באופן מלא, אתם יכולים לבחור להריץ באופן סלקטיבי את משימות ההפעלה של WebView שיכולות לפעול בשרשור רקע, וכך לפנות את שרשור UI למשימות קריטיות אחרות של האפליקציה. לשם כך, אפשר להשתמש ב-shouldRunUiThreadStartUpTasks(false).

בשלב מאוחר יותר במחזור החיים של האפליקציה, אפשר לקרוא שוב ל-startUpWebView עם shouldRunUiThreadStartUpTasks(true) כדי לסיים את משימות ההפעלה שנותרו ב-UI thread. ההחלטה אם להמתין לקריאה החוזרת בשלב הזה תלויה בשאלה אם השימוש ב-WebView נמצא בנתיב הקריטי.

דוגמה להטמעה

ממשק ה-API משתמש בandroidx.webkit.WebViewOutcomeReceiver callback, שמאפשר לכם לעקוב אחרי הפעלות מוצלחות או לטפל בכשלים באבחון.

אפשר לקרוא לפונקציה startUpWebView כמה פעמים מחלקים שונים באפליקציה. מומלץ להימנע מהטמעה של לולאת ניסיון חוזר פשוטה.

בדוגמת הקוד הבאה אפשר לראות איך משתמשים ב-API‏ WebViewCompat.startUpWebView לאתחול אסינכרוני.

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

ניפוי באגים בבעיות בהפעלה אסינכרונית

אם השימוש ב-startUpWebView לא מניב את שיפורי הביצועים הצפויים, לרוב זה קורה כי WebView מאותחל באופן מרומז במקום אחר באפליקציה לפני שהקריאה מבוצעת. יכולות להיות לכך כמה סיבות:

  • ספריות של צד שלישי או ערכות SDK שהופעלו בשלב מוקדם במחזור החיים של האפליקציה.

  • ContentProviders שמוזרק ל-APK ומפעיל את WebView APIs במהלך הפעלת האפליקציה.

  • הגדלת פריסה או קריאות תוכנה (למשל אחזור של מחרוזות של סוכן משתמש) שמתרחשות מוקדם מהצפוי.

כדי לעזור לכם לאבחן איפה ולמה מתרחשות האתחולים הלא צפויים האלה, אובייקט WebViewStartUpResult מספק יכולות מובנות של ביקורת:

  • getUiThreadBlockingStartUpLocations(): מחזירה רשימה של StartUpLocation אובייקטים שמייצגים מיקומים שבהם משימות ההפעלה של WebView חסמו את השרשור הראשי של ממשק המשתמש.

  • getNonUiThreadBlockingStartUpLocations(): מחזירה אתרים ספציפיים של קריאות שבהם הפעלת משימות הפעלה חסמה שרשורים ברקע.

כל StartUpLocation מכיל דוח קריסות שאפשר לרשום ביומן או לבדוק כדי למצוא את המחלקה והמתודה המדויקות שהפעילו את האתחול.

דוגמה להטמעה

אתם יכולים לבדוק את המיקומים האלה בתוך הקריאה החוזרת (callback) של 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, מומלץ להשתמש במקום זאת ב-startUpWebView API. ‫startUpWebView API פועל בכל הגרסאות של Android ו-WebView שנתמכות על ידי ספריית Jetpack Webkit.

איך מוודאים שהאפליקציה עמידה ויציבה

השימוש בהטמעה של Jetpack Webkit עוזר לשמור על התנהגות עקבית בכל הסביבה העסקית של Android. יתרון מרכזי של ה-API הזה הוא העמידות שלו: במכשירים מדור קודם שבהם אין אופטימיזציות חדשות יותר, ה-API שומר על ביצועים שווים לפתרונות עקיפים ידניים. כך תוכלו ליהנות מיתרונות מודרניים של הפעלה במכשירים חדשים יותר, בלי לפגוע בביצועים במכשירים ישנים יותר.

אופטימיזציה של הפעלת WebView מצמצמת את הסיכון לשגיאות ANR במהלך הפעלת האפליקציה, אבל חשוב גם להגן על האפליקציה מפני קריסות של רכיבי עיבוד בזמן ריצה ומפני שחזור של זיכרון המערכת. כדי לשמור על יציבות מקיפה של האפליקציה אחרי ש-WebView פועל, כדאי לעיין במאמר בנושא טיפול בסגירה של WebView. בנוסף, כדי לשמור את מצב המשתמש במקרה של סגירת תהליך ברקע, צריך להשתמש ב-WebView.saveState() ולפעול לפי שיטות בטוחות לניהול הזיכרון שמוסברות במאמר ניהול יעיל של מצב WebView.

אם נתקלתם בבעיות או שיש לכם משוב לגבי startUpWebView API, אתם יכולים לדווח על באג בכלי הציבורי למעקב אחר בעיות.