טיפול בסיום של WebView

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

למה חשוב לטפל בסיום התהליך

בגרסאות קודמות של Android, היה פחות סיכוי שהמשתמשים יבחינו בבעיה כשהאפליקציה לא הצליחה לטפל בסיום תהליך הרינדור. עם זאת, בגרסאות חדשות יותר של Android נעשה שימוש במנגנונים מורכבים יותר כדי לנהל את צריכת המשאבים של תהליכים ברקע. לכן, אם לא מטמיעים באפליקציה handler של onRenderProcessGone, סביר יותר שהמשתמשים יבחינו בסגירות גלויות של תהליכים בחזית כשהם יחזרו לאפליקציה. כדי לוודא שהאפליקציה תתאושש בצורה חלקה ותשמור על חוויית משתמש יציבה, צריך להטמיע את Termination Handling API.

שימוש ב-Termination Handling API

כדי לטפל במצבים כאלה, אפשר לשנות את onRenderProcessGone ב-WebViewClient.

הדרישות העיקריות לשחזור

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

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

דוגמה של הטמעה

בדוגמה הבאה אפשר לראות איך לטפל בסגירות האלה בתוך Activity שלם. הוא משתמש ב-detail.didCrash() כדי לתעד אם התהליך קרס או אם המערכת סגרה את תהליך העיבוד כדי לפנות זיכרון. לא משנה מה הסיבה, הקוד מנקה את ה-WebView הלא פעיל על ידי הסרה שלו מהפריסה, השמדה שלו וניקוי ההפניות של הפעילות כדי למנוע NullPointerException. לאחר מכן, המערכת מכינה מופע חדש ומחזירה את הערך true כדי שהאפליקציה לא תיסגר.

הערה: הדוגמה הבאה תמיד מחזירה את הערך true כדי שהאפליקציה תמשיך לפעול. אם אתם רוצים שהאפליקציה תסתיים כשה-WebView renderer מסתיים, אתם יכולים להחזיר את הערך false מהקריאה החוזרת (callback) הזו. עם זאת, מומלץ לטפל ביצירה מחדש ולהחזיר את הערך true כדי שחוויית המשתמש תהיה יציבה.

class MyWebViewActivity : AppCompatActivity() {

    private var webViewContainer: ViewGroup? = null
    private var myWebView: WebView? = null

    private var crashCount = 0
    private val MAX_CRASHES = 3

    // NEW FLAG: Tracks if the WebView crashed while the activity was hidden
    private var isWebViewPendingRecovery = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my_web_view)
        webViewContainer = findViewById(R.id.my_web_view_container)

        initWebView()
    }

    // NEW LIFECYCLE HOOK: Triggers when the user brings the app back to the foreground
    override fun onResume() {
        super.onResume()
        // If a crash happened in the background, rebuild it now that the user can see it
        if (isWebViewPendingRecovery) {
            isWebViewPendingRecovery = false
            Log.i("MY_APP_TAG", "Activity resumed. Recovering pending WebView now.")
            recoverWebView()
        }
    }

    private fun initWebView() {
        val webView = WebView(this)
        myWebView = webView

        webViewContainer?.addView(webView)

        webView.webViewClient = object : WebViewClient() {
            override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {
                if (detail.didCrash()) {
                    Log.e("MY_APP_TAG", "The WebView rendering process crashed!")
                } else {
                    Log.e("MY_APP_TAG", "System killed the WebView rendering process to reclaim memory.")
                }

                // Safely clean up the dead WebView
                webViewContainer?.removeView(webView)
                webView.destroy()
                myWebView = null

                // MODIFIED LOGIC: Check the lifecycle state of the Activity
                // lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) means the activity is visible and active
                if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
                    Log.i("MY_APP_TAG", "Activity is in foreground. Recovering immediately.")
                    recoverWebView()
                } else {
                    // The app is in the background. Mark it for later!
                    Log.w("MY_APP_TAG", "Activity is in background. Postponing recovery until user returns.")
                    isWebViewPendingRecovery = true
                }

                return true
            }
        }
        webView.loadUrl("https://www.example.com")
    }

    // HELPER FUNCTION: Extracted the recovery logic to reuse it
    private fun recoverWebView() {
        if (crashCount < MAX_CRASHES) {
            crashCount++
            Log.w("MY_APP_TAG", "Recovering WebView... Attempt $crashCount")
            initWebView()
        } else {
            Log.e("MY_APP_TAG", "WebView crashed too many times. Showing error UI.")
            showErrorUI()
        }
    }

    private fun showErrorUI() {
        // Example: Inflate an error view
    }

    override fun onDestroy() {
        super.onDestroy()
        myWebView?.let {
            webViewContainer?.removeView(it)
            it.destroy()
        }
        myWebView = null
    }
}

שיקולים חשובים

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

  • קריסות חוזרות: אם רכיב רינדור קורס בזמן טעינה של דף ספציפי, אל תטענו מחדש את אותו דף מיד. זה עלול לגרום ל-WebView החדש לקרוס שוב.
  • חשיפה: סגירות של אפליקציות שנגרמו בגלל היעדר הקריאה החוזרת הזו עדיין לא משתקפות בדוחות הקריסה של Play Console. יכול להיות שעדכונים עתידיים יטפלו בפער הזה, אבל צריך לפתור את הבעיות האלה עכשיו כדי להבטיח את יציבות האפליקציה. הטמעה של ה-handler הזה מספקת תובנות מיידיות לגבי הסיבה ליציאה מהאפליקציה.
  • הבחנה בין סיום תהליך הרינדור לבין השבתת תהליך האפליקציה: onRenderProcessGone מטפל במצבים שבהם תהליך הרינדור מסתיים בזמן שתהליך האפליקציה נשאר פעיל. אם המערכת סוגרת את כל תהליך האפליקציה ברקע, צריך להשתמש ב-onSaveInstanceState() עם WebView.saveState() ו-WebView.restoreState() כדי לשמור את היסטוריית הניווט ולשחזר אותה. כדי לנהל את המצב השמור בצורה בטוחה בלי לחרוג ממגבלות העסקאות, אפשר לעיין במאמר ניהול יעיל של מצב WebView.