web-dev-qa-db-fra.com

Fuite de mémoire dans WebView

J'ai une activité utilisant une mise en page XML dans laquelle une WebView est intégrée. Je n'utilise pas du tout WebView dans mon code d'activité, tout ce qu'il fait est assis là dans ma disposition xml et être visible.

Maintenant, lorsque je termine l'activité, je constate que mon activité n'est pas effacée de la mémoire. (Je vérifie via hprof dump). L'activité est entièrement effacée si je supprime le WebView de la mise en page XML.

J'ai déjà essayé un

webView.destroy();
webView = null;

dans onDestroy () de mon activité, mais cela n'aide pas beaucoup.

Dans mon vidage hprof, mon activité (nommée 'Browser') a les racines GC restantes suivantes (après avoir appelé destroy() dessus):

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]  

J'ai trouvé qu'un autre développeur a connu une chose similaire, voir la réponse de Filipe Abrantes sur: http://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-Android/

En effet, un poste très intéressant. Récemment, j'ai eu beaucoup de mal à résoudre une fuite de mémoire sur mon Android. Au final, il s'est avéré que ma disposition xml comprenait un composant WebView qui, même s'il n'était pas utilisé, empêchait la mémoire d'être g-collecté après les rotations d'écran/le redémarrage de l'application… est-ce un bogue de l'implémentation actuelle, ou y a-t-il quelque chose de spécifique à faire lors de l'utilisation de WebViews

Maintenant, malheureusement, il n'y a pas encore eu de réponse sur le blog ou la liste de diffusion sur cette question. Par conséquent, je me demande, est-ce qu'un bogue dans le SDK (peut-être similaire au bogue MapView tel que rapporté http://code.google.com/p/Android/issues/detail?id=2181 ) ou comment retirer complètement l'activité de la mémoire avec une vue Web intégrée ?

73
Mathias Conradt

Je conclus des commentaires et des tests ci-dessus que le problème est un bogue dans le SDK: lors de la création d'une WebView via une disposition XML, l'activité est transmise en tant que contexte pour la WebView, pas le contexte de l'application. Lorsque vous terminez l'activité, WebView conserve toujours les références à l'activité, par conséquent, l'activité n'est pas supprimée de la mémoire. J'ai déposé un rapport de bogue pour cela, voir le lien dans le commentaire ci-dessus.

webView = new WebView(getApplicationContext());

Notez que cette solution de contournement ne fonctionne que pour certains cas d'utilisation, c'est-à-dire si vous avez juste besoin d'afficher du HTML dans une vue Web, sans liens href ni liens vers des boîtes de dialogue, etc. Voir les commentaires ci-dessous.

54
Mathias Conradt

J'ai eu de la chance avec cette méthode:

Mettez un FrameLayout dans votre xml comme conteneur, appelons-le web_container. Annoncez ensuite WebView par programmation comme mentionné ci-dessus. onDestroy, supprimez-le du FrameLayout.

Dites que c'est quelque part dans votre fichier de mise en page XML, par exemple layout/your_layout.xml

<FrameLayout
    Android:id="@+id/web_container"
    Android:layout_width="fill_parent"
    Android:layout_height="wrap_content"/>

Ensuite, après avoir gonflé la vue, ajoutez la WebView instanciée avec le contexte de l'application à votre FrameLayout. onDestroy, appelez la méthode destroy de la vue Web et supprimez-la de la hiérarchie des vues ou vous fuierez.

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();
    }
}

FrameLayout ainsi que layout_width et layout_height ont également été copiés arbitrairement à partir d'un projet existant où il fonctionne. Je suppose qu'un autre ViewGroup fonctionnerait et je suis certain que d'autres dimensions de mise en page fonctionneront.

Cette solution fonctionne également avec RelativeLayout à la place de FrameLayout.

32
caller9

Voici une sous-classe de WebView qui utilise le hack ci-dessus pour éviter de manière transparente les fuites de mémoire:

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<Activity> activityRef;

        public MyWebViewClient( Activity activity ) {
            this.activityRef = new WeakReference<Activity>(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;
        }
    }
}

Pour l'utiliser, remplacez simplement WebView par NonLeakingWebView dans vos mises en page

                    <com.mycompany.view.NonLeakingWebView
                            Android:layout_width="fill_parent"
                            Android:layout_height="wrap_content"
                            ...
                            />

Assurez-vous ensuite d'appeler NonLeakingWebView.destroy() à partir de la méthode onDestroy de votre activité.

Notez que ce client Web doit gérer les cas courants, mais il peut ne pas être aussi complet qu'un client Web classique. Je ne l'ai pas testé pour des choses comme le flash, par exemple.

10
emmby

Sur la base de la réponse de user1668939 sur ce post ( https://stackoverflow.com/a/12408703/1369016 ), voici comment j'ai corrigé ma fuite WebView dans un fragment:

@Override
public void onDetach(){

    super.onDetach();

    webView.removeAllViews();
    webView.destroy();
}

La différence avec la réponse de user1668939 est que je n'ai utilisé aucun espace réservé. L'appel de removeAllViews () sur la référence WebvView elle-même a fait l'affaire.

## MISE À JOUR ##

Si vous êtes comme moi et que vous avez des vues Web dans plusieurs fragments (et que vous ne voulez pas répéter le code ci-dessus dans tous vos fragments), vous pouvez utiliser la réflexion pour le résoudre. Faites simplement étendre vos fragments à celui-ci:

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();
        }
    }
}

Je suppose que vous appelez votre champ WebView "webView" (et oui, votre référence WebView doit être malheureusement un champ). Je n'ai pas trouvé d'autre moyen de le faire qui serait indépendant du nom du champ (sauf si je fais une boucle dans tous les champs et vérifie si chacun est d'une classe WebView, ce que je ne veux pas faire pour des problèmes de performances).

7
Tiago

J'ai résolu le problème de fuite de mémoire de frustrant Webview comme ceci:

(J'espère que cela peut aider beaucoup)

Bases:

  • Pour créer une vue Web, une référence (disons une activité) est nécessaire.
  • Pour tuer un processus:

Android.os.Process.killProcess(Android.os.Process.myPid()); peut être appelée.

Point tournant:

Par défaut, toutes les activités s'exécutent dans le même processus dans une seule application. (le processus est défini par le nom du package). Mais:

Différents processus peuvent être créés au sein d'une même application.

Solution: Si un processus différent est créé pour une activité, son contexte peut être utilisé pour créer une vue Web. Et lorsque ce processus est tué, tous les composants ayant des références à cette activité (la vue Web dans ce cas) sont tués et la partie souhaitable principale est:

GC est appelé avec force pour collecter ces ordures (vue Web).

Code d'aide: (un cas simple)

Total de deux activités: dites A et B

Fichier manifeste:

<application
        Android:allowBackup="true"
        Android:icon="@drawable/ic_launcher"
        Android:label="@string/app_name"
        Android:process="com.processkill.p1" // can be given any name 
        Android:theme="@style/AppTheme" >
        <activity
            Android:name="com.processkill.A"
            Android:process="com.processkill.p2"
            Android:label="@string/app_name" >
            <intent-filter>
                <action Android:name="Android.intent.action.MAIN" />
                <category Android:name="Android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            Android:name="com.processkill.B"
            Android:process="com.processkill.p3"
            Android:label="@string/app_name" >
        </activity>
    </application>

Démarrer A puis B

A> B

B est créé avec une vue Web intégrée.

Lorsque backKey est appuyé sur l'activité B, onDestroy est appelé:

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

et cela tue le processus actuel, c'est-à-dire com.processkill.p3

et enlève la vue Web qui y est référencée

REMARQUE: Faites très attention lorsque vous utilisez cette commande kill. (non recommandé pour des raisons évidentes). N'implémentez aucune méthode statique dans l'activité (activité B dans ce cas). N'utilisez aucune référence à cette activité d'une autre (car elle sera tuée et ne sera plus disponible).

3
vipulfb

Après avoir lu http://code.google.com/p/Android/issues/detail?id=9375 , nous pourrions peut-être utiliser la réflexion pour définir ConfigCallback.mWindowManager sur null sur Activity.onDestroy et le restaurer sur Activity.onCreate. Je ne sais pas si cela nécessite des autorisations ou viole une politique. Cela dépend de l'implémentation d'Android.webkit et peut échouer sur les versions ultérieures d'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) {
    }
}

Appel de la méthode ci-dessus dans Activity

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

public void onDestroy() {
    setConfigCallback(null);
    super.onDestroy();
}
3
raijintatsu

Vous pouvez essayer de placer l'activité Web dans un processus distinct et de quitter lorsque l'activité est détruite, si la gestion de processus multiples n'est pas un gros effort pour vous.

2
Fanny

Vous devez supprimer WebView de la vue parent avant d'appeler WebView.destroy().

Commentaire destroy () de WebView - "Cette méthode doit être appelée après que cette WebView a été supprimée du système de vue."

2
qjinee

Il y a un problème avec la solution de contournement "contexte d'application": plantez lorsque WebView essaie d'afficher une boîte de dialogue. Par exemple, la boîte de dialogue "mémoriser le mot de passe" lors de la soumission des formulaires de connexion/passe (dans tous les autres cas?).

Il pourrait être corrigé avec WebView settings 'setSavePassword(false) pour le cas "se souvenir du mot de passe".

1
Denis Gladkiy