כשהאפליקציה משתמשת ב-WebView בפעם הראשונה, המערכת מבצעת משימות הפעלה ספציפיות.
תהליך ההפעלה הזה כבד. כברירת מחדל, הפעולה הזו מתבצעת באופן מרומז בשרשור ממשק המשתמש בפעם הראשונה שהאפליקציה קוראת להרבה ממשקי API בחבילות android.webkit או androidx.webkit, או כשהיא מרחיבה פריסה שמכילה תג WebView.
למה זה חשוב
מכיוון שההפעלה הסמויה הזו מתרחשת כולה ב-Thread הראשי, היא חוסמת את האפליקציה שלכם מעיבוד קלט של משתמשים ומגדילה באופן משמעותי את הסיכון לשגיאות מסוג 'האפליקציה לא מגיבה' (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 שומר על ביצועים שווים לפתרונות עקיפים ידניים. כך תוכלו ליהנות מיתרונות מודרניים של הפעלה במכשירים חדשים יותר, בלי לפגוע בביצועים במכשירים ישנים יותר.
אם נתקלתם בבעיות או שיש לכם משוב לגבי startUpWebView API, אתם יכולים לדווח על באג בכלי הציבורי למעקב אחר בעיות.