ניהול אובייקטים של WebView

מערכת Android מספקת כמה ממשקי API שיעזרו לכם לנהל את WebView האובייקטים שמציגים תוכן מהאינטרנט באפליקציה שלכם.

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

Version API

החל מ-Android 7.0 (רמת API‏ 24), המשתמשים יכולים לבחור מבין כמה חבילות שונות להצגת תוכן אינטרנט באובייקט WebView. הספרייה Jetpack Webkit כוללת את השיטה getCurrentWebViewPackage() לאחזור מידע שקשור לחבילה שמציגה תוכן אינטרנט באפליקציה. השיטה הזו שימושית כשמנתחים שגיאות שמתרחשות רק כשהאפליקציה מנסה להציג תוכן אינטרנט באמצעות הטמעה של WebView בחבילה מסוימת.

כדי להשתמש בשיטה הזו, צריך להוסיף את הלוגיקה שמוצגת בקטע הקוד הבא:

Kotlin

val webViewPackageInfo = WebViewCompat.getCurrentWebViewPackage(appContext)
Log.d("MY_APP_TAG", "WebView version: ${webViewPackageInfo.versionName}")

Java

PackageInfo webViewPackageInfo = WebViewCompat.getCurrentWebViewPackage(appContext);
Log.d("MY_APP_TAG", "WebView version: " + webViewPackageInfo.versionName);

שירות הגלישה הבטוחה של Google

כדי לספק למשתמשים חוויית גלישה בטוחה יותר, WebViewאובייקטים מאמתים כתובות URL באמצעות הגלישה הבטוחה של Google, וכך האפליקציה יכולה להציג למשתמשים אזהרה כשהם מנסים לעבור לאתר שעלול להיות לא בטוח.

למרות שערך ברירת המחדל של EnableSafeBrowsing הוא true, יש מקרים שבהם כדאי להפעיל את הגלישה הבטוחה רק בתנאים מסוימים או להשבית אותה. ב-Android מגרסה 8.0 (רמת API‏ 26) ואילך, אפשר להשתמש ב-setSafeBrowsingEnabled() כדי להפעיל או להשבית את הגלישה הבטוחה לאובייקט WebView ספציפי.

אם רוצים שכל אובייקטי WebView לא ייבדקו על ידי הגלישה הבטוחה, צריך להוסיף את רכיב <meta-data> הבא לקובץ המניפסט של האפליקציה:

<manifest>
    <application>
        <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
                   android:value="false" />
        ...
    </application>
</manifest>

הגדרת פעולות פרוגרמטיות

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

אם אתם מטרגטים ל-Android 8.1 (רמת API‏ 27) או לגרסה מתקדמת יותר, אתם יכולים להגדיר באופן פרוגרמטי איך האפליקציה שלכם מגיבה לאיום מוכר באופנים הבאים:

  • אתם יכולים להחליט אם האפליקציה תדווח על איומים מוכרים לגלישה בטוחה.
  • אתם יכולים להגדיר שהאפליקציה תבצע באופן אוטומטי פעולה מסוימת – כמו חזרה למצב בטוח – בכל פעם שהיא נתקלת בכתובת URL שמסווגת כאיום מוכר.

קטעי הקוד הבאים מראים איך להנחות את המופעים של WebView באפליקציה לחזור תמיד למצב בטוח אחרי שנתקלים באיום מוכר:

MyWebActivity.java

Kotlin

private lateinit var superSafeWebView: WebView
private var safeBrowsingIsInitialized: Boolean = false

// ...

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    superSafeWebView = WebView(this)
    superSafeWebView.webViewClient = MyWebViewClient()
    safeBrowsingIsInitialized = false

    if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
        WebViewCompat.startSafeBrowsing(this, ValueCallback<Boolean> { success ->
            safeBrowsingIsInitialized = true
            if (!success) {
                Log.e("MY_APP_TAG", "Unable to initialize Safe Browsing!")
            }
        })
    }
}

Java

private WebView superSafeWebView;
private boolean safeBrowsingIsInitialized;

// ...

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    superSafeWebView = new WebView(this);
    superSafeWebView.setWebViewClient(new MyWebViewClient());
    safeBrowsingIsInitialized = false;

    if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
        WebViewCompat.startSafeBrowsing(this, new ValueCallback<Boolean>() {
            @Override
            public void onReceiveValue(Boolean success) {
                safeBrowsingIsInitialized = true;
                if (!success) {
                    Log.e("MY_APP_TAG", "Unable to initialize Safe Browsing!");
                }
            }
        });
    }
}

MyWebViewClient.java

Kotlin

class MyWebViewClient : WebViewClientCompat() {
    // Automatically go "back to safety" when attempting to load a website that
    // Google identifies as a known threat. An instance of WebView calls this
    // method only after Safe Browsing is initialized, so there's no conditional
    // logic needed here.
    override fun onSafeBrowsingHit(
            view: WebView,
            request: WebResourceRequest,
            threatType: Int,
            callback: SafeBrowsingResponseCompat
    ) {
        // The "true" argument indicates that your app reports incidents like
        // this one to Safe Browsing.
        if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY)) {
            callback.backToSafety(true)
            Toast.makeText(view.context, "Unsafe web page blocked.", Toast.LENGTH_LONG).show()
        }
    }
}

Java

public class MyWebViewClient extends WebViewClientCompat {
    // Automatically go "back to safety" when attempting to load a website that
    // Google identifies as a known threat. An instance of WebView calls this
    // method only after Safe Browsing is initialized, so there's no conditional
    // logic needed here.
    @Override
    public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
            int threatType, SafeBrowsingResponseCompat callback) {
        // The "true" argument indicates that your app reports incidents like
        // this one to Safe Browsing.
        if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY)) {
            callback.backToSafety(true);
            Toast.makeText(view.getContext(), "Unsafe web page blocked.",
                    Toast.LENGTH_LONG).show();
        }
    }
}

HTML5 Geolocation API

באפליקציות שמטרגטות Android 6.0 (רמת API‏ 23) ואילך, ה-API של מיקום גיאוגרפי נתמך רק במקורות מאובטחים, כמו HTTPS. כל בקשה ל-Geolocation API ממקורות לא מאובטחים נדחית באופן אוטומטי בלי להפעיל את השיטה המתאימה onGeolocationPermissionsShowPrompt().

ביטול ההסכמה לאיסוף מדדים

ל-WebView יש אפשרות להעלות נתוני אבחון אנונימיים ל-Google כשהמשתמש נותן את הסכמתו. הנתונים נאספים ברמת האפליקציה לכל אפליקציה שיוצרת מופע של WebView. כדי להשבית את התכונה הזו, צריך ליצור את התג הבא ברכיב <application> של קובץ המניפסט:

<manifest>
    <application>
    ...
    <meta-data android:name="android.webkit.WebView.MetricsOptOut"
               android:value="true" />
    </application>
</manifest>

הנתונים מועלים מאפליקציה רק אם המשתמש מביע הסכמה וגם האפליקציה לא מבטלת את ההסכמה. מידע נוסף על ביטול ההסכמה לשיתוף נתוני אבחון זמין במאמר פרטיות המשתמשים בדוחות של WebView.

Termination Handling API

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

אם רכיב ה-Renderer קורס בזמן טעינה של דף אינטרנט מסוים, ניסיון לטעון את אותו דף שוב עלול לגרום לאובייקט WebView חדש להציג את אותה התנהגות של קריסת ה-Rendering.

בקטע הקוד הבא אפשר לראות איך משתמשים ב-API הזה בתוך Activity:

Kotlin

    
inner class MyRendererTrackingWebViewClient : WebViewClient() {
    private var mWebView: WebView? = null

    override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {
        if (!detail.didCrash()) {
            // Renderer is killed because the system ran out of memory. The app
            // can recover gracefully by creating a new WebView instance in the
            // foreground.
            Log.e("MY_APP_TAG", ("System killed the WebView rendering process " +
                "to reclaim memory. Recreating..."))

            mWebView?.also { webView ->
                val webViewContainer: ViewGroup = findViewById(R.id.my_web_view_container)
                webViewContainer.removeView(webView)
                webView.destroy()
                mWebView = null
            }

            // By this point, the instance variable "mWebView" is guaranteed to
            // be null, so it's safe to reinitialize it.

            return true // The app continues executing.
        }

        // Renderer crashes because of an internal error, such as a memory
        // access violation.
        Log.e("MY_APP_TAG", "The WebView rendering process crashed!")

        // In this example, the app itself crashes after detecting that the
        // renderer crashed. If you handle the crash more gracefully and let
        // your app continue executing, you must destroy the current WebView
        // instance, specify logic for how the app continues executing, and
        // return "true" instead.
        return false
    }
}

Java

public class MyRendererTrackingWebViewClient extends WebViewClient {
    private WebView mWebView;

    @Override
    public boolean onRenderProcessGone(WebView view,
            RenderProcessGoneDetail detail) {
        if (!detail.didCrash()) {
            // Renderer is killed because the system ran out of memory. The app
            // can recover gracefully by creating a new WebView instance in the
            // foreground.
            Log.e("MY_APP_TAG", "System killed the WebView rendering process " +
                    "to reclaim memory. Recreating...");

            if (mWebView != null) {
                ViewGroup webViewContainer =
                        (ViewGroup) findViewById(R.id.my_web_view_container);
                webViewContainer.removeView(mWebView);
                mWebView.destroy();
                mWebView = null;
            }

            // By this point, the instance variable "mWebView" is guaranteed to
            // be null, so it's safe to reinitialize it.

            return true; // The app continues executing.
        }

        // Renderer crashes because of an internal error, such as a memory
        // access violation.
        Log.e("MY_APP_TAG", "The WebView rendering process crashed!");

        // In this example, the app itself crashes after detecting that the
        // renderer crashed. If you handle the crash more gracefully and let
        // your app continue executing, you must destroy the current WebView
        // instance, specify logic for how the app continues executing, and
        // return "true" instead.
        return false;
    }
}

Renderer Importance API

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

בקטע הקוד הבא אפשר לראות איך מקצים עדיפות לתהליך העיבוד שמשויך לאובייקטים WebView של האפליקציה:

Kotlin

val myWebView: WebView = ...
myWebView.setRendererPriorityPolicy(RENDERER_PRIORITY_BOUND, true)

Java

WebView myWebView;
myWebView.setRendererPriorityPolicy(RENDERER_PRIORITY_BOUND, true);

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

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

שימור המצב והיסטוריית הניווט

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

מכיוון שמערכת Android אוכפת מגבלה של 1MB לכל התהליך של savedInstanceState, סדרת פעולות (serialization) של סשנים גדולים של גלישה באמצעות API של framework רגיל עלולה לגרום לTransactionTooLargeException. כדי לאכוף מגבלות גודל או להסיר בבטחה רשומות קודמות בהיסטוריה, משתמשים בשיטה WebViewCompat.saveState() של Jetpack Webkit.

אסטרטגיות מפורטות ושיטות מומלצות לניהול יעיל של מצב מוצגות במאמר ניהול יעיל של מצב WebView.