webentwicklung-frage-antwort-db.com.de

Speicherverlust in WebView

Ich habe eine Aktivität mit einem XML-Layout, in das ein WebView eingebettet ist. Ich verwende das WebView in meinem Aktivitätscode überhaupt nicht, es befindet sich nur in meinem XML-Layout und ist sichtbar.

Wenn ich die Aktivität beende, wird meine Aktivität nicht aus dem Speicher gelöscht. (Ich überprüfe per hprof dump). Die Aktivität wird jedoch vollständig gelöscht, wenn ich das WebView aus dem XML-Layout entferne.

Ich habe es schon versucht

webView.destroy();
webView = null;

in onDestroy () von meiner Aktivität, aber das hilft nicht viel.

In meinem hprof-Dump hat meine Aktivität (mit dem Namen 'Browser') die folgenden verbleibenden GC-Wurzeln (nachdem destroy() darauf aufgerufen wurde):

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]  

Ich habe festgestellt, dass ein anderer Entwickler Ähnliches erlebt hat, siehe die Antwort von Filipe Abrantes auf: http://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-Android/

In der Tat ein sehr interessanter Beitrag. Vor kurzem fiel es mir sehr schwer, einen Speicherverlust in meiner Android= App zu beheben. Am Ende stellte sich heraus, dass mein XML-Layout eine WebView-Komponente enthielt, die den Speicher sperrte, auch wenn sie nicht verwendet wurde Nach Bildschirmrotationen/App-Neustart nicht mehr erfasst werden ... Dies ist ein Fehler der aktuellen Implementierung, oder muss bei der Verwendung von WebViews eine bestimmte Aktion ausgeführt werden

Leider wurde auf dem Blog oder der Mailingliste noch keine Antwort zu dieser Frage gegeben. Daher wundere ich mich, dass es sich um einen Fehler im SDK handelt (möglicherweise ähnlich wie der gemeldete MapView-Fehler http://code.google.com/p/Android/issues/detail?id=2181 ) oder wie man die Aktivität mit einer eingebetteten Webansicht komplett aus dem Speicher holt ?

73
Mathias Conradt

Ich schließe aus den obigen Kommentaren und weiteren Tests, dass das Problem ein Fehler im SDK ist: Beim Erstellen eines WebView über ein XML-Layout wird die Aktivität als Kontext für das WebView übergeben, nicht als Anwendungskontext. Nach Abschluss der Aktivität behält das WebView weiterhin Verweise auf die Aktivität bei, sodass die Aktivität nicht aus dem Speicher entfernt wird. Ich habe dafür einen Fehlerbericht eingereicht, siehe den Link im Kommentar oben.

webView = new WebView(getApplicationContext());

Beachten Sie, dass diese Problemumgehung nur für bestimmte Anwendungsfälle funktioniert, d. H., Wenn Sie nur HTML in einer Webansicht anzeigen müssen, ohne href-Links oder Links zu Dialogen usw. Siehe die nachstehenden Kommentare.

54
Mathias Conradt

Ich hatte etwas Glück mit dieser Methode:

Fügen Sie ein FrameLayout als Container in Ihre XML ein und nennen Sie es web_container. Anschließend können Sie das WebView wie oben beschrieben programmgesteuert hinzufügen. onDestroy, entfernen Sie es aus dem FrameLayout.

Angenommen, dies befindet sich irgendwo in Ihrer XML-Layoutdatei, z. layout/your_layout.xml

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

Fügen Sie nach dem Aufblasen der Ansicht die mit dem Anwendungskontext instanziierte WebView zu Ihrem FrameLayout hinzu. onDestroy, rufen Sie die Destroy-Methode der Webansicht auf und entfernen Sie sie aus der Ansichtshierarchie, da sonst ein Leck auftritt.

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

Auch FrameLayout sowie layout_width und layout_height wurden willkürlich aus einem vorhandenen Projekt kopiert, in dem es funktioniert. Ich gehe davon aus, dass eine andere ViewGroup funktionieren würde und ich bin sicher, dass andere Layout-Dimensionen funktionieren werden.

Diese Lösung funktioniert auch mit RelativeLayout anstelle von FrameLayout.

32
caller9

Hier ist eine Unterklasse von WebView, die den obigen Hack verwendet, um Speicherlecks nahtlos zu vermeiden:

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

Ersetzen Sie dazu in Ihren Layouts einfach WebView durch NonLeakingWebView

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

Stellen Sie dann sicher, dass Sie NonLeakingWebView.destroy() in der onDestroy-Methode Ihrer Aktivität aufrufen.

Beachten Sie, dass dieser Webclient die gängigen Fälle behandeln sollte, aber möglicherweise nicht so umfassend ist wie ein normaler Webclient. Ich habe es zum Beispiel nicht für Flash getestet.

10
emmby

Basierend auf der Antwort von user1668939 auf diesen Beitrag ( https://stackoverflow.com/a/12408703/1369016 ) habe ich mein WebView-Leck in einem Fragment folgendermaßen behoben:

@Override
public void onDetach(){

    super.onDetach();

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

Der Unterschied zur Antwort von user1668939 besteht darin, dass ich keine Platzhalter verwendet habe. Das Aufrufen von removeAllViews () in der WebvView-Referenz selbst hat den Trick getan.

## UPDATE ##

Wenn Sie wie ich WebViews in mehreren Fragmenten haben (und den obigen Code nicht in allen Fragmenten wiederholen möchten), können Sie ihn mithilfe von Reflection lösen. Lassen Sie einfach Ihre Fragmente dieses erweitern:

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

Ich gehe davon aus, dass Sie Ihr WebView-Feld "webView" nennen (und ja, Ihre WebView-Referenz muss leider ein Feld sein). Ich habe keine andere Möglichkeit gefunden, die vom Namen des Feldes unabhängig ist (es sei denn, ich durchlaufe alle Felder und überprüfe, ob sie aus einer WebView-Klasse stammen, was ich bei Leistungsproblemen nicht tun möchte).

7
Tiago

Ich habe das Problem des Speicherverlusts bei frustrierendem Webview folgendermaßen behoben:

(Ich hoffe das kann vielen helfen)

Grundlagen:

  • Zum Erstellen einer Webansicht wird eine Referenz (beispielsweise eine Aktivität) benötigt.
  • So beenden Sie einen Prozess:

Android.os.Process.killProcess(Android.os.Process.myPid()); kann aufgerufen werden.

Wendepunkt:

Standardmäßig werden alle Aktivitäten in einer Anwendung im selben Prozess ausgeführt. (Der Prozess wird durch den Paketnamen definiert). Aber:

Innerhalb einer Anwendung können unterschiedliche Prozesse erstellt werden.

Lösung: Wenn für eine Aktivität ein anderer Prozess erstellt wird, kann dessen Kontext zum Erstellen einer Webansicht verwendet werden. Und wenn dieser Prozess beendet wird, werden alle Komponenten mit Verweisen auf diese Aktivität (in diesem Fall die Webansicht) beendet, und der wichtigste wünschenswerte Teil ist:

GC wird mit Nachdruck aufgerufen, um diesen Müll zu sammeln (Webview).

Code für Hilfe: (ein einfacher Fall)

Insgesamt zwei Aktivitäten: Sagen Sie A & B

Manifestdatei:

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

Starten Sie A und dann B

A> B

B wird mit eingebettetem Webview erstellt.

Wenn bei Aktivität B die Rücktaste gedrückt wird, wird onDestroy aufgerufen:

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

und dies beendet den aktuellen Prozess, d. h. com.processkill.p3

und entfernt die darauf bezogene Webansicht

HINWEIS: Seien Sie besonders vorsichtig, wenn Sie diesen Kill-Befehl verwenden. (aus offensichtlichen Gründen nicht zu empfehlen). Implementieren Sie keine statische Methode in der Aktivität (in diesem Fall Aktivität B). Verwenden Sie keine anderen Verweise auf diese Aktivität (da sie getötet werden und nicht mehr verfügbar sind).

3
vipulfb

Nach dem Lesen von http://code.google.com/p/Android/issues/detail?id=9375 könnten wir möglicherweise Reflection verwenden, um ConfigCallback.mWindowManager für Activity.onDestroy auf null zu setzen und wiederherzustellen auf Activity.onCreate. Ich bin mir jedoch nicht sicher, ob es einige Berechtigungen erfordert oder gegen eine Richtlinie verstößt. Dies hängt von der Implementierung von Android.webkit ab und kann in späteren Versionen von Android fehlschlagen.

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

Aufruf der obigen Methode in 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

Sie können versuchen, die Webaktivität in einem separaten Prozess zu platzieren und zu beenden, wenn die Aktivität zerstört wird, wenn die Verarbeitung mehrerer Prozesse für Sie kein großer Aufwand ist.

2
Fanny

Sie müssen das WebView aus der übergeordneten Ansicht entfernen, bevor Sie WebView.destroy() aufrufen.

WebView's destroy () -Kommentar - "Diese Methode sollte aufgerufen werden, nachdem diese WebView aus dem View-System entfernt wurde."

2
qjinee

Es gibt ein Problem mit der Problemumgehung "Anwendungskontext": Absturz, wenn WebView versucht, einen beliebigen Dialog anzuzeigen. Beispiel: Dialogfeld "Kennwort speichern" beim Senden von Anmelde-/Übergabeformularen (in anderen Fällen?).

Dies könnte mit WebView settings 'setSavePassword(false) für den Fall "Remember the Password" behoben werden.

1
Denis Gladkiy