Android WebView 中的内存泄漏

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/3130654/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-20 08:50:15  来源:igfitidea点击:

Memory leak in WebView

androidandroid-webview

提问by Mathias Conradt

I have an activity using an xml layout where a WebView is embedded. I am not using the WebView in my activity code at all, all it does is sitting there in my xml layout and being visible.

我有一个使用 xml 布局的活动,其中嵌入了 WebView。我根本没有在我的活动代码中使用 WebView,它所做的只是坐在我的 xml 布局中并且可见。

Now, when I finish the activity, I find that my activity is not being cleared from memory. (I check via hprof dump). The activity is entirely cleared though if I remove the WebView from the xml layout.

现在,当我完成活动时,我发现我的活动没有从内存中清除。(我通过 hprof 转储检查)。但是,如果我从 xml 布局中删除 WebView,则该活动将被完全清除。

I already tried a

我已经试过了

webView.destroy();
webView = null;

in onDestroy() of my activity, but that doesn't help much.

在我的活动的 onDestroy() 中,但这并没有多大帮助。

In my hprof dump, my activity (named 'Browser') has the following remaining GC roots (after having called destroy()on it):

在我的 hprof 转储中,我的活动(名为“浏览器”)具有以下剩余的 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]  

I found that another developer has experienced similar thing, see the reply of Filipe Abrantes on: http://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-android/

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

Indeed a very interesting post. Recently I had a very hard time troubleshooting a memory leak on my Android app. In the end it turned out that my xml layout included a WebView component that, even if not used, was preventing the memory from being g-collected after screen rotations/app restart… is this a bug of the current implementation, or is there something specific that one needs to do when using WebViews

确实是一个非常有趣的帖子。最近,我很难解决我的 Android 应用程序上的内存泄漏问题。最后发现我的 xml 布局包含一个 WebView 组件,即使没有使用,它也会阻止内存在屏幕旋转/应用程序重启后被 g-collected ......这是当前实现的错误,还是有什么使用 WebViews 时需要做的特定事情

Now, unfortunately there has been no reply on the blog or the mailing list about this question yet. Therefore I am wondering, is that a bug in the SDK (maybe similar to the MapView bug as reported http://code.google.com/p/android/issues/detail?id=2181) or how to get the activity entirely off the memory with a webview embedded?

现在,不幸的是,博客或邮件列表上还没有关于这个问题的回复。因此,我想知道是 SDK 中的错误(可能类似于http://code.google.com/p/android/issues/detail?id=2181报告的 MapView 错误)或如何完全获取活动关闭嵌入了 webview 的内存

采纳答案by Mathias Conradt

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.

我从上面的评论和进一步的测试得出结论,问题是 SDK 中的一个错误:通过 XML 布局创建 WebView 时,活动作为 WebView 的上下文传递,而不是应用程序上下文。完成活动时,WebView 仍然保留对活动的引用,因此活动不会从内存中删除。我为此提交了错误报告,请参阅上面评论中的链接。

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.

请注意,此解决方法仅适用于某些用例,即如果您只需要在 web 视图中显示 html,而无需任何 href 链接或对话框链接等。请参阅下面的评论。

回答by caller9

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.

将 FrameLayout 作为容器放入 xml 中,我们称之为 web_container。然后如上所述以编程方式广告 WebView。onDestroy,将其从 FrameLayout 中删除。

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

说这是在你的 xml 布局文件中的某个地方,例如 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.

然后在您扩充视图后,将使用应用程序上下文实例化的 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();
    }
}

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.

此外,FrameLayout 以及 layout_width 和 layout_height 是从其工作的现有项目中任意复制的。我假设另一个 ViewGroup 可以工作,并且我确定其他布局尺寸也可以工作。

This solution also works with RelativeLayout in place of FrameLayout.

此解决方案也适用于 RelativeLayout 代替 FrameLayout。

回答by emmby

Here's a subclass of WebView that uses the above hack to seamlessly avoid memory leaks:

这是 WebView 的一个子类,它使用上述 hack 来无缝避免内存泄漏:

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

要使用它,只需在布局中用 NonLeakingWebView 替换 WebView

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

然后确保NonLeakingWebView.destroy()从您的活动的 onDestroy 方法调用。

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.

请注意,此 Web 客户端应处理常见情况,但它的功能可能不如常规 Web 客户端完整。例如,我还没有针对闪存之类的东西测试过它。

回答by Tiago

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:

基于 user1668939 在这篇文章 ( https://stackoverflow.com/a/12408703/1369016)上的回答,这是我修复片段内 WebView 泄漏的方法:

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

与 user1668939 的答案不同的是,我没有使用任何占位符。只需在 WebvView 引用本身上调用 removeAllViews() 就可以了。

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

如果你和我一样,在几个片段中都有 WebViews(并且你不想在所有片段中重复上面的代码),你可以使用反射来解决它。只需让您的 Fragments 扩展这个:

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

我假设您将 WebView 字段称为“webView”(是的,不幸的是,您的 WebView 引用必须是一个字段)。我还没有找到另一种独立于字段名称的方法(除非我遍历所有字段并检查每个字段是否来自 WebView 类,我不想因为性能问题而这样做)。

回答by vipulfb

I fixed memory leak issue of frustrating Webview like this:

我修复了令人沮丧的 Webview 的内存泄漏问题,如下所示:

(I hope this may help many)

(我希望这可以帮助很多人)

Basics:

基础知识

  • To create a webview, a reference (say an activity) is needed.
  • To kill a process:
  • 要创建一个 webview,需要一个引用(比如一个活动)。
  • 杀死一个进程:

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

android.os.Process.killProcess(android.os.Process.myPid());可以调用。

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 :

解决方案:如果为活动创建了不同的流程,则可以使用其上下文来创建 webview。当这个进程被终止时,所有引用这个活动的组件(在这种情况下是 webview)都会被终止,主要的可取部分是:

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

GC 被强制调用以收集这些垃圾(webview)。

Code for help:(one simple case)

帮助代码:(一个简单的案例)

Total two activities: say A & B

总共两个活动:说 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

A > B

A > B

B is created with webview embedded.

B 是用嵌入的 webview 创建的。

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

当在活动 B 上按下 backKey 时,onDestroy 被调用:

@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

这会杀死当前进程,即 com.processkill.p3

and takes away the webview referenced to it

并带走引用它的 webview

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

注意:使用此 kill 命令时要格外小心。(由于明显的原因不推荐)。不要在活动(本例中为活动 B)中实现任何静态方法。不要使用任何其他人对此活动的任何引用(因为它将被杀死并且不再可用)。

回答by raijintatsu

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.

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

Calling the above method in Activity

在Activity中调用上述方法

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

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

回答by Fanny

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.

如果多进程处理对您来说不是很大的努力,您可以尝试将 Web 活动放在一个单独的进程中并在活动被破坏时退出。

回答by qjinee

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

您需要在调用之前从父视图中删除 WebView WebView.destroy()

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

WebView 的 destroy() 注释 - “在从视图系统中删除此 WebView 后,应调用此方法。”

回答by Denis Gladkiy

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

“应用程序上下文”解决方法存在问题:WebView尝试显示任何对话框时崩溃。例如,登录/传递表单提交时的“记住密码”对话框(任何其他情况?)。

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

它可以通过“记住密码”案例的WebView设置来修复setSavePassword(false)