web-dev-qa-db-ja.com

WebViewのメモリリーク

WebViewが埋め込まれているxmlレイアウトを使用したアクティビティがあります。私はアクティビティコードでWebViewを使用していません。XMLレイアウトでそこに座って表示されるだけです。

これで、アクティビティを終了したときに、アクティビティがメモリからクリアされていないことがわかりました。 (hprofダンプ経由で確認します)。ただし、XMLレイアウトからWebViewを削除すると、アクティビティは完全にクリアされます。

私はすでに試しました

_webView.destroy();
webView = null;
_

私の活動のonDestroy()で、しかしそれはあまり助けにはなりません。

Hprofダンプでは、アクティビティ(「Browser」という名前)に次の残りのGCルートがあります(destroy()を呼び出した後):

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

別の開発者が同様のことを経験していることがわかりました。フィリペアブランテスの返信を参照してください。 http://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-Android/

実際、非常に興味深い投稿です。最近、Androidアプリでメモリリークのトラブルシューティングを行うのに非常に苦労しました。結局、私のXMLレイアウトには、使用されていなくてもメモリを防止するWebViewコンポーネントが含まれていました画面の回転/アプリの再起動後にg-collectedから…これは現在の実装のバグか、WebViewを使用するときに行う必要がある特定の何かがあります

現在、残念ながら、この質問に関するブログやメーリングリストにはまだ返信がありません。したがって、SDKのバグ(おそらく報告されたMapViewのバグに似ているかもしれません http://code.google.com/p/Android/issues/detail?id=2181 )またはwebviewを埋め込んでメモリから完全にアクティビティを取得する方法

73
Mathias Conradt

上記のコメントとさらなるテストから、問題はSDKのバグであると結論付けました。XMLレイアウトを介してWebViewを作成する場合、アクティビティはアプリケーションコンテキストではなく、WebViewのコンテキストとして渡されます。アクティビティを終了するとき、WebViewはまだアクティビティへの参照を保持するため、アクティビティはメモリから削除されません。そのためのバグレポートを提出しました。上のコメントのリンクをご覧ください。

webView = new WebView(getApplicationContext());

この回避策は特定のユースケースでのみ機能することに注意してください。つまり、hrefリンクやダイアログへのリンクなどを使用せずに、Webビューでhtmlを表示する必要がある場合のみです。以下のコメントを参照してください。

54
Mathias Conradt

私はこの方法でいくらか運がありました:

FrameLayoutをコンテナとしてxmlに配置し、web_containerと呼びます。次に、上記のようにWebViewをプログラムで広告します。 onDestroy、FrameLayoutから削除します。

これがxmlレイアウトファイルのどこかにあるとします。 layout/your_layout.xml

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

次に、ビューをインフレートした後、アプリケーションコンテキストでインスタンス化されたWebViewをFrameLayoutに追加します。 onDestroy、webviewのdestroyメソッドを呼び出してビュー階層から削除しないと、リークします。

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と、layout_widthおよびlayout_heightは、動作する既存のプロジェクトから任意にコピーされました。別のViewGroupが機能すると想定しており、他のレイアウトディメンションも機能すると確信しています。

このソリューションは、FrameLayoutの代わりにRelativeLayoutでも機能します。

32
caller9

上記のハックを使用してシームレスにメモリリークを回避するWebViewのサブクラスを次に示します。

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

使用するには、レイアウトでWebViewをNonLeakingWebViewに置き換えるだけです

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

次に、アクティビティのonDestroyメソッドからNonLeakingWebView.destroy()を呼び出してください。

このWebクライアントは一般的なケースを処理する必要がありますが、通常のWebクライアントほどフル機能ではない場合があります。たとえば、フラッシュなどのテストは行っていません。

10
emmby

この投稿に対するuser1668939の回答に基づいて( https://stackoverflow.com/a/12408703/1369016 )、これがフラグメント内のWebViewリークを修正した方法です:

@Override
public void onDetach(){

    super.onDetach();

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

User1668939の答えとの違いは、プレースホルダーを使用していないことです。 WebvView参照自体でremoveAllViews()を呼び出すだけでうまくいきました。

##更新##

あなたが私のようで、いくつかのフラグメント内にWebViewがある場合(そして、すべてのフラグメントにわたって上記のコードを繰り返したくない場合)、リフレクションを使用してそれを解決できます。フラグメントにこれを拡張させるだけです。

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

私はあなたがあなたのWebViewフィールドを「webView」と呼んでいると仮定しています(そして、はい、残念ながらあなたのWebView参照はフィールドでなければなりません)。フィールドの名前とは独立した別の方法を見つけていません(すべてのフィールドをループして、各フィールドがパフォーマンスの問題のためにしたくないWebViewクラスからのものであるかどうかを確認しない限り)。

7
Tiago

このようなイライラするWebviewのメモリリークの問題を修正しました。

(これが多くの人に役立つことを願っています)

基本

  • Webビューを作成するには、参照(アクティビティなど)が必要です。
  • プロセスを強制終了するには:

Android.os.Process.killProcess(Android.os.Process.myPid());を呼び出すことができます。

ターニングポイント:

デフォルトでは、すべてのアクティビティは1つのアプリケーションで同じプロセスで実行されます。 (プロセスはパッケージ名で定義されます)。しかし:

同じアプリケーション内で異なるプロセスを作成できます。

解決策:アクティビティに別のプロセスが作成された場合、そのコンテキストを使用してWebビューを作成できます。このプロセスが強制終了されると、このアクティビティ(この場合はwebview)への参照を持つすべてのコンポーネントが強制終了され、主な望ましい部分は次のとおりです。

GCは、このガベージを収集するために強制的に呼び出されます(Webビュー)。

ヘルプのコード:(1つの単純なケース)

合計2つのアクティビティ:AとBを言う

マニフェストファイル:

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

Aを開始してからB

A> B

Bは、webviewが埋め込まれた状態で作成されます。

アクティビティBでbackKeyが押されると、onDestroyが呼び出されます。

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

これにより、現在のプロセス、つまりcom.processkill.p3が強制終了されます

そしてそれが参照するウェブビューを取り除きます

注:このkillコマンドを使用するときは、特に注意してください。 (明らかな理由により推奨されません)。アクティビティ(この場合はアクティビティB)に静的メソッドを実装しないでください。他からのこのアクティビティへの参照を使用しないでください(強制終了されて使用できなくなるため)。

3
vipulfb

http://code.google.com/p/Android/issues/detail?id=9375 を読んだ後、リフレクションを使用して、Activity.onDestroyでConfigCallback.mWindowManagerをnullに設定し、復元することができます。 Activity.onCreateで。ただし、何らかの許可が必要なのか、ポリシーに違反するのかはわかりません。これはAndroid.webkitの実装に依存しており、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) {
    }
}

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

マルチプロセスの処理があなたにとって大きな努力ではない場合、Webアクティビティを別のプロセスに入れて、アクティビティが破棄されたときに終了することができます。

2
Fanny

WebView.destroy()を呼び出す前に、親ビューからWebViewを削除する必要があります。

WebViewのdestroy()コメント-「このメソッドは、このWebViewがビューシステムから削除された後に呼び出す必要があります。」

2
qjinee

「アプリコンテキスト」の回避策には問題があります。WebViewがダイアログを表示しようとするとクラッシュします。たとえば、ログイン/パスフォーム送信時の「パスワードを記憶する」ダイアログ(他の場合)。

「パスワードを記憶する」場合は、WebView settings 'setSavePassword(false)で修正できます。

1
Denis Gladkiy