Speicherverlust in WebView

74

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

Wenn ich die Aktivität beende, stelle ich fest, dass meine Aktivität nicht aus dem Speicher gelöscht wird. (Ich überprüfe über 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 () 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 ich sie aufgerufen destroy()habe):

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 ähnliche Erfahrungen gemacht hat. Siehe die Antwort von Filipe Abrantes unter: 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, selbst wenn sie nicht verwendet wurde, verhinderte, dass der Speicher nach Bildschirmrotationen / App-Neustart gesammelt wird. Ist dies ein Fehler der aktuellen Implementierung oder gibt es etwas? spezifisch, dass man tun muss, wenn man WebViews verwendet

Leider gibt es im Blog oder in der Mailingliste noch keine Antwort auf diese Frage. Daher frage ich mich, ob dies ein Fehler im SDK ist (möglicherweise ähnlich dem MapView-Fehler, wie unter http://code.google.com/p/android/issues/detail?id=2181 gemeldet ) oder wie die Aktivität vollständig abgerufen werden kann aus dem Speicher mit einer eingebetteten Webansicht ?

Mathias Conradt
quelle
Tritt dies auf, wenn Sie das WebView dynamisch erstellen?
Ktingle
1
Ich habe das gerade getestet, aber es macht keinen Unterschied.
Mathias Conradt
1
In der Zwischenzeit habe ich einen Fehlerbericht unter code.google.com/p/android/issues/detail?id=9375 eingereicht, aber vielleicht hat jemand eine Problemumgehung dafür. dann poste es bitte.
Mathias Conradt
3
OK. Ein weiterer Test mit nur geringen Änderungen: Wenn ich das WebView programmgesteuert erstelle und 'this' (die Aktivität) als Kontext festlege, bleibt die Aktivität weiterhin im Speicher. Wenn ich jedoch getApplicationContext () verwende, ist dies in Ordnung und die Aktivität wird ohne gespeicherte Referenzen entfernt.
Mathias Conradt

Antworten:

57

Ich schließe aus den obigen Kommentaren und weiteren Tests, dass das Problem ein Fehler im SDK ist: Beim Erstellen einer WebView über ein XML-Layout wird die Aktivität als Kontext für die 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, dh wenn Sie nur HTML in einer Webansicht ohne href-Links oder Links zu Dialogen usw. anzeigen müssen. Siehe die folgenden Kommentare.

Mathias Conradt
quelle
1
getApplicationContext () hat beim Erstellen von WebView tatsächlich an meinem Speicherverlust gearbeitet. Aber wenn ich die Webansicht zu einer anderen ViewGroup hinzufüge, erscheint der Speicherverlust wieder. Meine wilde Vermutung ist, dass der baseContext der Eltern übernommen wird. Gibt es irgendetwas dazu? Ich erstelle das übergeordnete Element auch mit getApplicationContext () ... also denke ich, dass ich
keine
28
Beachten Sie, dass die Verwendung des Anwendungskontexts bedeutet, dass Sie in Ihrer Webansicht nicht auf Links klicken können, da dies zu einem Absturz führt: "Für den Aufruf von startActivity () außerhalb eines Aktivitätskontexts ist das Flag FLAG_ACTIVITY_NEW_TASK erforderlich. Ist dies wirklich der Fall? was willst du
Emmby
5
Jedes Mal, wenn die Webansicht versucht, einen Dialog zu erstellen (z. B. um sich ein Kennwort zu merken usw.), stürzt die Webansicht ab, da sie einen Aktivitätskontext erwartet.
Markshiz
1
Es zerquetscht die App, wenn webView einen Dialog anzeigen möchte, zum Beispiel die Frage "Möchten Sie das Passwort speichern?", Dann wird es zerstört :(
Dawid Drozd
2
Dieser Fehler ist immer noch in Android 5.0 Lilpops aktiv. Wir müssen also berücksichtigen, dass wir dieses Fehlerproblem noch umgehen müssen.
GFxJamal
35

Ich hatte etwas Glück mit dieser Methode:

Fügen Sie ein FrameLayout als Container in Ihre XML-Datei ein und nennen Sie es web_container. Fügen Sie dann programmgesteuert das WebView wie oben erwähnt hinzu. onDestroy, entferne es aus dem FrameLayout.

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

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

Fügen Sie dann nach dem Aufblasen der Ansicht die mit dem Anwendungskontext instanziierte WebView zu Ihrem FrameLayout hinzu. Rufen Sie bei Destroy die Zerstörungsmethode der Webansicht auf und entfernen Sie sie aus der Ansichtshierarchie. Andernfalls tritt ein Leck auf.

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 Layoutabmessungen funktionieren werden.

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

caller9
quelle
1
Das hat bei mir absolut funktioniert. Vielen Dank! Die einzige Verbesserung, die ich vorgenommen habe, war die Verwendung des Aktivitätskontexts anstelle des Anwendungskontexts, von dem ich erwarte, dass er mich vor den an anderer Stelle genannten Abstürzen bewahrt, die auftreten, wenn Flash oder Dialogfelder in der Webansicht angezeigt werden.
SilithCrowe
Schade, dass es bei mir nicht zu funktionieren scheint. Der sConfigCallback ist immer noch da und enthält die Referenz: /
Surya Wijaya Madjid
10

Hier ist eine Unterklasse von WebView, die den oben genannten 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;
        }
    }
}

Um es zu verwenden, ersetzen Sie WebView in Ihren Layouts einfach durch NonLeakingWebView

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

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

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

emmby
quelle
Bei dieser Art der Implementierung wurde nur ein Problem festgestellt: WebView in DialogFragment Ich habe ContextThemeWrapper im Konstruktor anstelle von Activity ans so ClassCastException erhalten.
Sandstar
Wenn Sie Flash-Inhalte in der Webansicht haben, erhalten Sie eine ClassCastException von com.adobe.flashplayer.FlashPaintSurface ... zumindest auf Kindle Fire.
Christopher Perry
Aktualisiert am 1. Februar 2013, um das zusätzliche Leck in BrowserFrame.sConfigCallback zu umgehen
emmby
Für mich wird die Erinnerung immer noch nicht freigegeben
Tarun Tak
Stellen Sie sicher, dass Sie NonLeakingWebView.destroy () in onDestroy () Ihrer Aktivität aufrufen
emmby
8

Basierend auf der Antwort von user1668939 in diesem 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 für alle Ihre Fragmente 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 keinen anderen Weg gefunden, der unabhängig vom Namen des Feldes ist (es sei denn, ich durchlaufe alle Felder und überprüfe, ob jedes aus einer WebView-Klasse stammt, was ich aus Leistungsgründen nicht tun möchte).

Tiago
quelle
Eigentlich fand ich das die beste Lösung (zumindest für mich). Ich verwende Xamarin Android und habe jedes Mal ~ 1 MB verloren, wenn eine Aktivität mit einem WebView geschlossen wurde.
Tobias81
3

Nachdem wir http://code.google.com/p/android/issues/detail?id=9375 gelesen haben , können wir ConfigCallback.mWindowManager mithilfe von Reflection in Activity.onDestroy auf null setzen und in Activity.onCreate wiederherstellen. 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 Aktivität

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

public void onDestroy() {
    setConfigCallback(null);
    super.onDestroy();
}
Raijintatsu
quelle
Es gibt keine Richtlinie, gegen die verstoßen werden muss (in Bezug auf versteckte APIs oder so weiter). Sie haben nur keine Garantie dafür, dass sich das zugrunde liegende System bei einem Update nicht ändert. Möglicherweise können Sie diesen Code in eine Überprüfung auf API-Ebene einschließen und ihn erst nach dem Testen für neue SDK-Revisionen zulassen.
powerj1984
Ich denke, dies ist ein guter Weg, um das Problem zu beheben, wenn die erste erstellte Aktivität mit WebView-Instanz zerstört wird. Ich habe sehr unter diesem Problem gelitten. Es kann jedoch zu Problemen kommen, wenn Sie versuchen, eine andere Aktivität mit WebView zu starten.
Qiuping345
3

Ich habe das Problem mit dem Speicherverlust von frustrierendem Webview wie folgt behoben:

(Ich hoffe das kann vielen helfen)

Grundlagen :

  • Zum Erstellen einer Webansicht wird eine Referenz (z. B. 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 in demselben Prozess ausgeführt. (Der Prozess wird durch den Paketnamen definiert). Aber:

Innerhalb derselben Anwendung können verschiedene 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. Wenn dieser Prozess abgebrochen 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 gewaltsam aufgerufen, um diesen Müll zu sammeln (Webview).

Code für Hilfe: (ein einfacher Fall)

Insgesamt zwei Aktivitäten: sagen 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 eingebetteter Webansicht erstellt.

Wenn bei Aktivität B der BackKey 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, dh com.processkill.p3

und entfernt die darauf referenzierte Webansicht

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

Vipulfb
quelle
Wie würde man mit dieser Methode mit der Navigation in der Webansicht umgehen (dh Weiterleitungen, Anmeldungen, Links, Formulare usw.)?
Megakoresh
Sie sollten auch darauf hinweisen, dass Aktivitäten, die in separaten Prozessen ausgeführt werden, nicht gleichzeitig auf dieselben statischen Werte und gemeinsamen Einstellungen zugreifen oder in dieselbe SQLite-Datenbank schreiben können.
CamHart
2

Sie können versuchen, die Webaktivität in einen separaten Prozess zu versetzen und zu beenden, wenn die Aktivität zerstört wird, wenn die Verarbeitung mehrerer Prozesse für Sie keine große Anstrengung darstellt.

Fanny
quelle
2

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

Destroy () -Kommentar von WebView - "Diese Methode sollte aufgerufen werden, nachdem dieses WebView aus dem Ansichtssystem entfernt wurde."

qjinee
quelle
Dies hat das Problem für mich gelöst. Fügen Sie es einem FrameLayout hinzu und entfernen Sie dann die Webansicht aus diesem FrameLayout in onDestroy () der Aktivität.
Carl B
Arbeitete auch für mich - siehe alibabacloud.com/forum/read-520 für die detaillierte Analyse
Paul W
1

Es gibt ein Problem mit der Problemumgehung "App-Kontext": Absturz, wenn WebViewversucht wird, einen Dialog anzuzeigen. Zum Beispiel das Dialogfeld "Passwort speichern" beim Einreichen von Anmelde- / Passformularen (in anderen Fällen?).

Es könnte mit WebViewEinstellungen setSavePassword(false)für den Fall "Passwort merken" behoben werden.

Denis Gladkiy
quelle
Ein weiterer Fall (reproduziert auf Galaxy Nexus, HTC Hero, aber nicht auf Galaxy Ace): Auswahl aus der Dropdown-Liste (dieser löst auch aus, dass WebView einen Dialog anzeigt).
Denis Gladkiy