WebView の終了を処理する

WebView は、ウェブ コンテンツを別のレンダラ プロセスで実行します。このプロセスは終了することがあります。これは、システムがメモリを再利用するためにレンダラを強制終了した場合や、プロセス自体がクラッシュした場合に発生します。これらの終了を処理しないと、アプリも終了します。

プロセス終了の処理が重要な理由

以前のバージョンの Android では、アプリがレンダラ プロセスの終了を処理できなかった場合でも、ユーザーが問題に気づくことはあまりありませんでした。ただし、新しいバージョンの Android では、より複雑なメカニズムを使用してバックグラウンド プロセスのリソース消費を管理しています。そのため、アプリに onRenderProcessGone のハンドラが実装されていない場合、ユーザーがアプリに戻ったときに、フォアグラウンドの終了が目に見える形で発生する可能性が高くなります。アプリを適切に復元し、安定したユーザー エクスペリエンスを維持するには、Termination Handling API を実装する必要があります。

Termination Handling API を使用する

このような状況に対処するには、WebViewClient で onRenderProcessGone をオーバーライドします。

復元の主な要件

レンダラ プロセスの終了後もアプリの実行を継続するには、アプリ内の各 WebView が次のルールに従う必要があります。

  • WebView インスタンスを再利用しないでください。レンダラがなくなると、その特定の WebView オブジェクトは役に立たなくなります。ビュー階層から WebView オブジェクトを削除して破棄し、アクティビティまたはフラグメント内の WebView オブジェクトへの参照をすべてクリアする必要があります。これは、破棄された WebView インスタンスでメソッドが誤って呼び出され続けるのを防ぐためです。
  • 新しいインスタンスを作成する。ウェブ コンテンツの表示を継続する場合は、新しい WebView インスタンスが必要です。
  • true を返します。実装では、true を返して、状況を処理したことを WebView コードに伝える必要があります。そのレンダラ プロセスを使用している WebView が true を返さない場合、WebView はレンダラ プロセスの終了理由に応じてアプリを強制終了またはクラッシュさせます。

実装例

次の例は、完全な Activity 内でこれらの終了を処理する方法を示しています。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 を実装する際は、アプリの安定性を確保し、終了イベントの可視性を維持するために、次の点を考慮してください。

  • クラッシュの繰り返し: 特定のページの読み込み中にレンダラがクラッシュした場合は、同じページをすぐに再読み込みしないでください。新しい WebView が再びクラッシュする可能性があります。
  • 可視性: このコールバックがないためにアプリが強制終了されたことは、Google Play Console のクラッシュ レポートにまだ反映されていません。今後のアップデートでこのギャップが解消される可能性はありますが、アプリの安定性を確保するには、今すぐこれらの問題を解決する必要があります。このハンドラを実装すると、アプリが終了した理由をすぐに確認できます。
  • レンダラの終了とアプリプロセスの終了を区別する: onRenderProcessGone は、アプリプロセスがアクティブなままレンダラ プロセスが終了する状況を処理します。アプリのプロセス全体がバックグラウンドでシステムによって強制終了された場合は、onSaveInstanceState() を WebView.saveState() および WebView.restoreState() とともに使用して、ナビゲーション履歴を保持して復元します。トランザクションの上限に達することなく、保存された状態を安全に管理するには、WebView の状態を効率的に管理するをご覧ください。