處理 WebView 終止

WebView 會在獨立的轉譯器程序中執行網路內容。有時,這項程序可能會終止。如果系統終止轉譯器以回收記憶體,或程序本身異常終止,就會發生這種情況。如果您不處理這些終止作業,應用程式也會結束。

處理程序終止作業的重要性

在較低版本的 Android 上,當應用程式無法處理轉譯器程序終止時,使用者較不容易注意到問題。不過,新版 Android 會使用更複雜的機制,管理背景程序的資源消耗量。因此,如果應用程式未針對 onRenderProcessGone 實作處理常式,使用者返回應用程式時,現在更有可能看到前景終止。您必須實作 Termination Handling API,確保應用程式能順利復原,並維持穩定的使用者體驗。

使用 Termination Handling API

如要處理這類情況,請覆寫 WebViewClient 中的 onRenderProcessGone。

復原的重要條件

如要在轉譯器程序終止後繼續執行應用程式,應用程式中的每個 WebView 都必須遵循下列規則:

  • 請勿重複使用 WebView 執行個體。轉譯器消失後,該特定 WebView 物件就無法使用。您必須從檢視區塊階層中移除 WebView 物件、將其刪除,並清除 Activity 或 Fragment 中對該物件的所有參照,以免不小心繼續對已刪除的 WebView 執行個體呼叫方法。
  • 建立新的執行個體。如要繼續顯示網路內容,您需要新的 WebView 執行個體。
  • 傳回 true。實作項目必須傳回 true,告知 WebView 程式碼您已處理該情況。如果使用該轉譯器程序的任何 WebView 未傳回 true,WebView 會根據轉譯器程序結束原因,終止或停止應用程式。

導入範例

以下範例說明如何在完整的 Activity 中處理這些終止作業。它會使用 detail.didCrash() 記錄程序是否異常終止,或系統是否終止轉譯器以回收記憶體。無論原因為何,程式碼都會清除版面配置中失效的 WebView、將其刪除,並清除 Activity 的參照,以防止 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 時,請注意下列事項,確保應用程式穩定,並掌握終止事件:

  • 重複當機:如果轉譯器在載入特定網頁時當機,請勿立即重新載入同一個網頁。這可能會導致新的 WebView 再次當機。
  • 可見度:因缺少這個回呼而導致的應用程式終止,尚未反映在 Play 管理中心的當機報告中。雖然日後的更新可能會解決這個問題,但您現在必須解決這些問題,確保應用程式穩定。實作這個處理常式,即可立即瞭解應用程式退出的原因。
  • 區分轉譯器終止和應用程式程序終止: onRenderProcessGone 會處理轉譯器程序終止,但應用程式程序仍保持運作的情況。如果整個應用程式程序在背景遭到系統終止,請搭配使用 onSaveInstanceState() 與 WebView.saveState() 和 WebView.restoreState(),以保留及復原導覽記錄。如要安全管理已儲存的狀態,且不超過交易限制,請參閱「有效管理 WebView 狀態」。