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

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

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

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

שימוש ב-Termination Handling API

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

דרישות חשובות לשחזור

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

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

דוגמה להטמעה

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

הערה: בדוגמה הבאה תמיד מוחזר הערך true כדי שהאפליקציה תמשיך לפעול. אם רוצים שהאפליקציה תסתיים כשתהליך הרינדור של WebView יסתיים, אפשר להחזיר false מהקריאה החוזרת הזו. עם זאת, מומלץ לטפל ביצירה מחדש ולהחזיר את 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, חשוב לזכור את הנקודות הבאות כדי לוודא שהאפליקציה יציבה ושיש לכם גישה לאירועי סיום:

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