Cómo controlar la finalización de WebView

WebView ejecuta su contenido web en un proceso de renderizador independiente. A veces, este proceso se puede finalizar. Esto sucede si el sistema finaliza el procesador para recuperar memoria o si el proceso falla. Si no controlas estas finalizaciones, tu app también se cerrará.

Por qué es fundamental controlar la finalización de los procesos

En versiones anteriores de Android, era menos probable que los usuarios notaran un problema cuando una app no podía controlar la finalización del proceso de renderización. Sin embargo, las versiones más recientes de Android usan mecanismos más complejos para administrar el consumo de recursos de los procesos en segundo plano. Por lo tanto, si tu app no implementa un controlador para onRenderProcessGone, ahora es más probable que los usuarios observen finalizaciones visibles en primer plano cuando regresen a la app. Es necesario implementar la API de Termination Handling para garantizar que tu app se recupere correctamente y mantenga una experiencia del usuario estable.

Usa la API de Termination Handling

Para controlar estas situaciones, anula onRenderProcessGone en tu WebViewClient.

Requisitos clave para la recuperación

Para que tu app siga funcionando después de que finalice un proceso de renderizador, cada WebView de tu app debe seguir estas reglas:

  • No reutilices la instancia de WebView. Una vez que el renderizador desaparece, ese objeto WebView específico deja de ser útil. Debes quitar el objeto WebView de tu jerarquía de vistas, destruirlo y borrar cualquier referencia a él en tu Activity o Fragment para evitar que se sigan llamando accidentalmente métodos en la instancia de WebView destruida.
  • Crea una instancia nueva. Si quieres seguir mostrando contenido web, necesitas una nueva instancia de WebView.
  • Devuelve verdadero. Tu implementación debe mostrar true para indicarle al código de WebView que controlaste la situación. Si algún WebView que use ese proceso de renderizador no devuelve true, WebView cerrará o bloqueará tu app según el motivo de salida del proceso de renderizador.

Ejemplo de implementación

En el siguiente ejemplo, se muestra cómo controlar estas finalizaciones dentro de una actividad completa. Usa detail.didCrash() para registrar si el proceso falló o si el sistema finalizó el procesador para recuperar memoria. Independientemente de la causa, el código realiza una limpieza quitando el WebView inactivo del diseño, destruyéndolo y borrando las referencias de la actividad para evitar NullPointerException. Luego, prepara una instancia nueva y devuelve true para que la app no se cierre.

Nota: En el siguiente ejemplo, siempre se devuelve true para mantener la app en funcionamiento. Si quieres específicamente que tu app finalice cuando finalice el renderizador de WebView, puedes devolver false desde esta devolución de llamada. Sin embargo, te recomendamos que controles la recreación y devuelvas true para obtener una experiencia del usuario estable.

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
    }
}

Consideraciones importantes

Cuando implementes onRenderProcessGone, ten en cuenta las siguientes consideraciones para garantizar la estabilidad de la app y mantener la visibilidad de los eventos de finalización:

  • Fallas repetidas: Si un procesador falla mientras carga una página específica, no vuelvas a cargar la misma página de inmediato. Esto podría provocar que el nuevo WebView vuelva a fallar.
  • Visibilidad: Las finalizaciones de apps causadas por la falta de esta devolución de llamada aún no se reflejan en los informes de fallas de Play Console. Si bien es posible que las actualizaciones futuras aborden esta brecha, debes resolver estos problemas ahora para garantizar la estabilidad de la app. La implementación de este controlador proporciona visibilidad inmediata de los motivos por los que se cierra tu app.
  • Distingue la finalización del renderizador del cierre del proceso de la app: onRenderProcessGone controla las situaciones en las que se finaliza el proceso del renderizador mientras el proceso de la app permanece activo. Si el sistema finaliza todo el proceso de la app en segundo plano, usa onSaveInstanceState() con WebView.saveState() y WebView.restoreState() para conservar y recuperar el historial de navegación. Para administrar el estado guardado de forma segura sin alcanzar los límites de transacciones, consulta Administra el estado de WebView de manera eficiente.