Pérdida de memoria en WebView

Tengo una actividad usando un diseño xml donde está incrustado un WebView. No estoy usando WebView en mi código de actividad, todo lo que hace es estar sentado en mi diseño xml y ser visible.

Ahora, cuando termino la actividad, descubro que mi actividad no se borra de la memoria. (Lo compruebo a través de hprof dump). Sin embargo, la actividad se borrará por completo si elimino WebView del diseño xml.

Ya probé una

webView.destroy(); webView = null; 

en onDestroy () de mi actividad, pero eso no ayuda mucho.

En mi volcado de hprof, mi actividad (llamada ‘Navegador’) tiene las siguientes raíces de GC restantes (después de haber llamado a destroy() sobre ella):

 com.myapp.android.activity.browser.Browser - mContext of android.webkit.JWebCoreJavaBridge - sJavaBridge of android.webkit.BrowserFrame [Class] - mContext of android.webkit.PluginManager - mInstance of android.webkit.PluginManager [Class] 

Descubrí que otro desarrollador ha experimentado algo similar, consulte la respuesta de Filipe Abrantes en: http://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-android/

De hecho, una publicación muy interesante. Recientemente me costó mucho solucionar problemas de pérdida de memoria en mi aplicación de Android. Al final resultó que mi diseño xml incluía un componente WebView que, incluso si no se usaba, impedía que la memoria se recolectara g después de la rotación de la pantalla / reinicio de la aplicación … ¿es esto un error de la implementación actual, o hay algo específico que hay que hacer al usar WebViews

Ahora, desafortunadamente, todavía no ha habido respuesta en el blog o la lista de correo sobre esta pregunta. Por lo tanto, me pregunto si es un error en el SDK (tal vez similar al error de MapView que se informa http://code.google.com/p/android/issues/detail?id=2181 ) o cómo obtener la actividad por completo. fuera de la memoria con una vista web incrustado ?

Concluyo de los comentarios anteriores y otras pruebas, que el problema es un error en el SDK: al crear un diseño WebView a través de XML, la actividad se pasa como el contexto para el WebView, no como el contexto de la aplicación. Al finalizar la actividad, WebView aún conserva referencias a la actividad, por lo tanto, la actividad no se elimina de la memoria. Archivé un informe de error para eso, vea el enlace en el comentario anterior.

 webView = new WebView(getApplicationContext()); 

Tenga en cuenta que esta solución alternativa solo funciona para ciertos casos de uso, es decir, si solo necesita mostrar html en una vista web, sin enlaces href ni enlaces a cuadros de diálogo, etc. Consulte los comentarios a continuación.

He tenido algo de suerte con este método:

Ponga un FrameLayout en su xml como un contenedor, vamos a llamarlo web_container. A continuación, programáticamente y el WebView como se mencionó anteriormente. onDestroy, elimínelo de FrameLayout.

Diga que esto está en algún lugar de su archivo de diseño xml, por ejemplo, layout / your_layout.xml

  

Luego, después de inflar la vista, agregue la WebView instanciada con el contexto de la aplicación a su FrameLayout. onDestroy, llame al método de destrucción de la vista web y elimínelo de la jerarquía de vistas o se producirá una fuga.

 public class TestActivity extends Activity { private FrameLayout mWebContainer; private WebView mWebView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.your_layout); mWebContainer = (FrameLayout) findViewById(R.id.web_container); mWebView = new WebView(getApplicationContext()); mWebContainer.addView(mWebView); } @Override protected void onDestroy() { super.onDestroy(); mWebContainer.removeAllViews(); mWebView.destroy(); } } 

También FrameLayout así como también layout_width y layout_height fueron copiados arbitrariamente de un proyecto existente donde funciona. Supongo que otro ViewGroup funcionaría y estoy seguro de que otras dimensiones de diseño funcionarán.

Esta solución también funciona con RelativeLayout en lugar de FrameLayout.

Aquí hay una subclase de WebView que usa el truco anterior para evitar memory leaks:

 package com.mycompany.view; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.util.AttributeSet; import android.webkit.WebView; import android.webkit.WebViewClient; /** * see http://stackoverflow.com/questions/3130654/memory-leak-in-webview and http://code.google.com/p/android/issues/detail?id=9375 * Note that the bug does NOT appear to be fixed in android 2.2 as romain claims * * Also, you must call {@link #destroy()} from your activity's onDestroy method. */ public class NonLeakingWebView extends WebView { private static Field sConfigCallback; static { try { sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback"); sConfigCallback.setAccessible(true); } catch (Exception e) { // ignored } } public NonLeakingWebView(Context context) { super(context.getApplicationContext()); setWebViewClient( new MyWebViewClient((Activity)context) ); } public NonLeakingWebView(Context context, AttributeSet attrs) { super(context.getApplicationContext(), attrs); setWebViewClient(new MyWebViewClient((Activity)context)); } public NonLeakingWebView(Context context, AttributeSet attrs, int defStyle) { super(context.getApplicationContext(), attrs, defStyle); setWebViewClient(new MyWebViewClient((Activity)context)); } @Override public void destroy() { super.destroy(); try { if( sConfigCallback!=null ) sConfigCallback.set(null, null); } catch (Exception e) { throw new RuntimeException(e); } } protected static class MyWebViewClient extends WebViewClient { protected WeakReference activityRef; public MyWebViewClient( Activity activity ) { this.activityRef = new WeakReference(activity); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { try { final Activity activity = activityRef.get(); if( activity!=null ) activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); }catch( RuntimeException ignored ) { // ignore any url parsing exceptions } return true; } } } 

Para usarlo, simplemente reemplace WebView con NonLeakingWebView en sus diseños

   

Luego, asegúrese de llamar a NonLeakingWebView.destroy() desde el método onDestroy de su actividad.

Tenga en cuenta que este cliente web debe manejar los casos comunes, pero puede que no tenga todas las características de un cliente web normal. No lo he probado para cosas como flash, por ejemplo.

En función de la respuesta de user1668939 en esta publicación ( https://stackoverflow.com/a/12408703/1369016 ), así es como arreglé mi fuga de WebView dentro de un fragmento:

 @Override public void onDetach(){ super.onDetach(); webView.removeAllViews(); webView.destroy(); } 

La diferencia con la respuesta del usuario1668939 es que no he usado ningún marcador de posición. Solo llamar a removeAllViews () en la referencia WebvView en sí misma fue el truco.

## ACTUALIZACIÓN ##

Si eres como yo y tienes WebViews dentro de varios fragmentos (y no quieres repetir el código anterior en todos tus fragmentos), puedes usar el reflection para resolverlo. Solo haz que tus Fragmentos extiendan este:

 public class FragmentWebViewLeakFree extends Fragment{ @Override public void onDetach(){ super.onDetach(); try { Field fieldWebView = this.getClass().getDeclaredField("webView"); fieldWebView.setAccessible(true); WebView webView = (WebView) fieldWebView.get(this); webView.removeAllViews(); webView.destroy(); }catch (NoSuchFieldException e) { e.printStackTrace(); }catch (IllegalArgumentException e) { e.printStackTrace(); }catch (IllegalAccessException e) { e.printStackTrace(); }catch(Exception e){ e.printStackTrace(); } } } 

Supongo que está llamando a su campo WebView “webView” (y sí, su referencia WebView debe ser un campo desafortunadamente). No he encontrado otra forma de hacerlo que sea independiente del nombre del campo (a menos que recorra todos los campos y verifique si cada uno es de una clase WebView, lo que no quiero hacer por problemas de rendimiento).

Después de leer http://code.google.com/p/android/issues/detail?id=9375 , tal vez podríamos usar la reflexión para configurar ConfigCallback.mWindowManager para anular en Activity.onDestroy y restaurarlo en Activity.onCreate. No estoy seguro si requiere algunos permisos o infringe alguna política. Esto depende de la implementación de android.webkit y puede fallar en versiones posteriores de Android.

 public void setConfigCallback(WindowManager windowManager) { try { Field field = WebView.class.getDeclaredField("mWebViewCore"); field = field.getType().getDeclaredField("mBrowserFrame"); field = field.getType().getDeclaredField("sConfigCallback"); field.setAccessible(true); Object configCallback = field.get(null); if (null == configCallback) { return; } field = field.getType().getDeclaredField("mWindowManager"); field.setAccessible(true); field.set(configCallback, windowManager); } catch(Exception e) { } } 

Llamar al método anterior en Activity

 public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setConfigCallback((WindowManager)getApplicationContext().getSystemService(Context.WINDOW_SERVICE)); } public void onDestroy() { setConfigCallback(null); super.onDestroy(); } 

Solucioné el problema de pérdida de memoria de Webview frustrante de esta manera:

(Espero que esto pueda ayudar a muchos)

Conceptos básicos :

  • Para crear una vista web, se necesita una referencia (por ejemplo, una actividad).
  • Para matar un proceso:

android.os.Process.killProcess(android.os.Process.myPid()); puede ser llamado.

Punto de retorno:

Por defecto, todas las actividades se ejecutan en el mismo proceso en una aplicación. (el proceso está definido por el nombre del paquete). Pero:

Se pueden crear diferentes procesos dentro de la misma aplicación.

Solución: si se crea un proceso diferente para una actividad, su contexto se puede usar para crear una vista web. Y cuando este proceso se cancela, todos los componentes que tienen referencias a esta actividad (vista web en este caso) se eliminan y la principal parte deseable es:

GC es llamado con fuerza para recolectar esta basura (vista web).

Código de ayuda: (un caso simple)

Total de dos actividades: decir A y B

Archivo manifiesto

           

Comience A luego B

A> B

B se crea con webview incrustado.

Cuando se presiona backKey en la actividad B, se llama a onDestroy:

 @Override public void onDestroy() { android.os.Process.killProcess(android.os.Process.myPid()); super.onDestroy(); } 

y esto mata el proceso actual, es decir, com.processkill.p3

y quita la vista web a la que se hace referencia

NOTA: tenga especial cuidado al usar este comando de matar. (no recomendado debido a razones obvias). No implemente ningún método estático en la actividad (actividad B en este caso). No utilice ninguna referencia a esta actividad de ninguna otra (ya que se eliminará y ya no estará disponible).

Puedes intentar ubicar la actividad web en un proceso separado y salir cuando se destruya la actividad, si el manejo de multiprocesos no es un gran esfuerzo para ti.

Existe un problema con la solución temporal del “contexto de la aplicación”: locking cuando WebView intenta mostrar cualquier diálogo. Por ejemplo, el cuadro de diálogo “recordar la contraseña” en la submición de inicio de sesión / paso de formularios (¿en otros casos?).

Podría setSavePassword(false) con la configuración de WebViewsetSavePassword(false) para el caso “recordar la contraseña”.

WebView.destroy() eliminar WebView de la vista principal antes de llamar a WebView.destroy() .

Comentario de destroy () de WebView: “Este método debe invocarse después de que este WebView se haya eliminado del sistema de visualización”.