WebView 中的内存泄漏

我有一个使用 xml 布局的活动,其中嵌入了 WebView。我根本没有在我的活动代码中使用 WebView,它所做的只是放在 xml 布局中并显示出来。

现在,当我完成这个活动时,我发现我的活动没有从记忆中清除。(我通过 hprof dump 查询)。不过,如果我从 xml 布局中删除 WebView,则该活动将被完全清除。

我已经试过了

webView.destroy();
webView = null;

毁掉我的活动,但这没有多大帮助。

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

我发现另一个开发者也经历过类似的事情,见 Filipe Abrantes 的回复: Http://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-android/

确实是一篇非常有趣的文章。 最近我过得很艰难 上的内存泄漏故障排除 Android 应用程序,最终它被证明是 我的 xml 布局包含了一个 WebView 组件,即使没有使用,是 阻止记忆存在 G-在屏幕旋转/应用程序后收集 重新启动... 这是当前的一个错误 还是有什么东西 特别是当某人需要做某事时 使用网上浏览

现在,很不幸,博客和邮件列表上还没有关于这个问题的回复。因此,我想知道,这是一个错误在 SDK (也许类似于地图视图错误报告 http://code.google.com/p/android/issues/detail?id=2181)或 如何将活动完全从内存中移除,并嵌入一个 webview

68384 次浏览

I conclude from above comments and further tests, that the problem is a bug in the SDK: when creating a WebView via XML layout, the activity is passed as the context for the WebView, not the application context. When finishing the activity, the WebView still keeps references to the activity, therefore the activity doesn't get removed from the memory. I filed a bug report for that , see the link in the comment above.

webView = new WebView(getApplicationContext());

Note that this workaround only works for certain use cases, i.e. if you just need to display html in a webview, without any href-links nor links to dialogs, etc. See the comments below.

I have had some luck with this method:

Put a FrameLayout in your xml as a container, lets call it web_container. Then programmatically ad the WebView as mentioned above. onDestroy, remove it from the FrameLayout.

Say this is somewhere in your xml layout file e.g. layout/your_layout.xml

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

Then after you inflate the view, add the WebView instantiated with the application context to your FrameLayout. onDestroy, call the webview's destroy method and remove it from the view hierarchy or you will leak.

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

Also FrameLayout as well as the layout_width and layout_height were arbitrarily copied from an existing project where it works. I assume another ViewGroup would work and I am certain other layout dimensions will work.

This solution also works with RelativeLayout in place of FrameLayout.

Here's a subclass of WebView that uses the above hack to seamlessly avoid 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<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;
}
}
}

To use it, just replace WebView with NonLeakingWebView in your layouts

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

Then make sure to call NonLeakingWebView.destroy() from your activity's onDestroy method.

Note that this webclient should handle the common cases, but it may not be as full-featured as a regular webclient. I haven't tested it for things like flash, for example.

There is an issue with "app context" workaround: crash when WebView tries to show any dialog. For example "remember the password" dialog on login/pass forms submition (any other cases?).

It could be fixed with WebView settings' setSavePassword(false) for the "remember the password" case.

After reading http://code.google.com/p/android/issues/detail?id=9375, maybe we could use reflection to set ConfigCallback.mWindowManager to null on Activity.onDestroy and restore it on Activity.onCreate. I'm unsure though if it requires some permissions or violates any policy. This is dependent on android.webkit implementation and it may fail on later versions of 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) {
}
}

Calling the above method in Activity

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


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

You can try putting the web activity in a seperate process and exit when the activity is destroyed, if multiprocess handling is not a big effort to you.

Based on user1668939's answer on this post (https://stackoverflow.com/a/12408703/1369016), this is how I fixed my WebView leak inside a fragment:

@Override
public void onDetach(){


super.onDetach();


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

The difference from user1668939's answer is that I have not used any placeholders. Just calling removeAllViews() on the WebvView reference itself did the trick.

## UPDATE ##

If you are like me and have WebViews inside several fragments (and you do not want to repeat the above code across all of your fragments), you can use reflection to solve it. Just make your Fragments extend this one:

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

I am assuming you are calling your WebView field "webView" (and yes, your WebView reference must be a field unfortunately). I have not found another way to do it that would be independent from the name of the field (unless I loop through all the fields and check if each one is from a WebView class, which I do not want to do for performance issues).

I fixed memory leak issue of frustrating Webview like this:

(I hope this may help many)

Basics:

  • To create a webview, a reference (say an activity) is needed.
  • To kill a process:

android.os.Process.killProcess(android.os.Process.myPid()); can be called.

Turning point:

By default, all activities run in same process in one application. (the process is defined by package name). But:

Different processes can be created within same application.

Solution: If a different process is created for an activity, its context can be used to create a webview. And when this process is killed, all components having references to this activity (webview in this case) are killed and the main desirable part is :

GC is called forcefully to collect this garbage (webview).

Code for help: (one simple case)

Total two activities: say A & B

Manifest file:

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

Start A then B

A > B

B is created with webview embedded.

When backKey is pressed on activity B, onDestroy is called:

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

and this kills the current process i.e. com.processkill.p3

and takes away the webview referenced to it

NOTE: Take extra care while using this kill command. (not recommended due to obvious reasons). Don't implement any static method in the activity (activity B in this case). Don't use any reference to this activity from any other (as it will be killed and no longer available).

You need to remove the WebView from the parent view before calling WebView.destroy().

WebView's destroy() comment - "This method should be called after this WebView has been removed from the view system."