WebView中的内存泄漏

77
我有一个使用xml布局的活动,其中嵌入了一个WebView。我在活动代码中根本没有使用WebView,它只是坐在我的xml布局中并可见。
现在,当我完成活动时,我发现我的活动没有从内存中清除。(我通过hprof dump进行检查)。但是,如果我从xml布局中删除WebView,活动就完全清除了。
我已经尝试过...
webView.destroy();
webView = null;

虽然我已在我的活动的 onDestroy() 方法中释放了资源,但效果不是很好。

在我的 hprof 文件中,我的活动(名为“Browser”)在调用 destroy() 后仍具有以下未释放的 GC 根:

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在此处的回答。

这确实是一个非常有趣的帖子。最近我在调试我的Android应用程序时遇到了很大的麻烦,最终发现我的xml布局包含一个WebView组件,即使未被使用,在屏幕旋转/应用程序重新启动后仍会防止内存被垃圾回收...这是当前实现的一个错误吗?还是在使用WebViews时需要特别注意什么?

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


如果您动态创建WebView,是否会出现这种情况? - ktingle
1
我刚刚测试了一下,但没有任何区别。 - Mathias Conradt
1
同时,我在http://code.google.com/p/android/issues/detail?id=9375提交了一个错误报告,但也许有人有解决方法,请发帖分享。 - Mathias Conradt
3
好的。稍作修改后再进行另一个测试:当我通过编程方式创建WebView并将“this”(即活动)设置为上下文时,该活动仍会保留在内存中。但是,当我使用getApplicationContext()时,一切都正常,活动被移除而没有任何保留的引用。 - Mathias Conradt
9个回答

57

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

webView = new WebView(getApplicationContext());

请注意,这种解决方法仅适用于特定的用例,即如果您只需要在Web视图中显示HTML,而没有任何href链接或链接到对话框等。请参见下面的评论。


1
谢谢你,getApplicationContext() 在创建 WebView 时解决了我的内存泄漏问题。但是当我将 WebView 添加到另一个 ViewGroup 时,内存泄漏问题再次出现。我猜测是因为它采用了父级的 baseContext。有没有什么解决方法?我也是使用 getApplicationContext() 创建父级的...所以我想我已经没有其他理论了。 - weakwire
28
请注意,使用应用程序上下文意味着您将无法在Web视图中点击链接,因为这样做会导致崩溃:"从Activity上下文之外调用startActivity()需要FLAG_ACTIVITY_NEW_TASK标志。这真的是您想要的吗?"请注意不要改变原文的含义。 - emmby
5
每当Webview尝试创建对话框(例如,记住密码等),它都会崩溃,因为它需要一个活动上下文。同时,任何时候Webview尝试创建对话框,它都会崩溃,因为它期望有一个活动上下文。 - markshiz
1
当webView尝试显示对话框(例如询问“是否要保存密码”)时,应用程序会崩溃:( - Gelldur
2
这个错误在安卓5.0棒棒糖版本仍然存在。因此我们需要记住,我们仍然需要为这个错误问题找到解决方法。 - Soma Hesk
显示剩余3条评论

36

我已经通过以下方法取得了一些成功:

在你的XML文件中放一个FrameLayout作为容器,我们称之为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也可以工作,我确定其他布局尺寸也可以工作。

这个解决方案同样适用于RelativeLayout替代FrameLayout。


1
这对我绝对有效。非常感谢!我唯一的改进是使用活动上下文而不是应用程序上下文,我希望这将使我免受在Webview中出现Flash或对话框时发生的崩溃的影响。 - SilithCrowe
很遗憾,它似乎对我不起作用。sConfigCallback仍然存在并保持引用 :/ - Surya Wijaya Madjid

10

这是一个 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 https://dev59.com/pnA75IYBdhLWcg3wv7wj 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()

请注意,这个WebClient应该处理常见情况,但它可能不像普通的WebClient那样功能全面。例如,我没有测试过它是否支持flash等功能。


已更新于2013年2月1日,以解决BrowserFrame.sConfigCallback中的额外泄漏问题。 - emmby
对我来说,内存仍然没有释放。 - Tarun Tak
请确保在您的Activity的onDestroy()方法中调用NonLeakingWebView.destroy()。 - emmby
这并不是一个很好的解决方案,我必须说。一旦你销毁了一个BrowserFrame.sConfigCallback,然后打开另一个使用NonLeakingWebView的Activity,那么一个新的ConfigCallback就会被创建,并由ViewRoot或ViewRootImpl中的静态ArrayList引用,另一个Activity也将被泄漏。请检查Android 2.3.5r1的代码: 第209-215行[android.webkit.BrowserFrame.ConfigCallback] 第298-301行[android.view.ViewRoot.addConfigCallback] - qiuping345
在我的手机上,在静态块代码中的一行中,我每次都会得到例外:“sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback");”,但所有东西都正常运行,你能解释一下吗?“ java.lang.ClassNotFoundException:android.webkit.BrowserFrame”。 - mac229
显示剩余2条评论

9

根据用户 user1668939 在此帖子 (https://dev59.com/9Wct5IYBdhLWcg3wkuLE#12408703) 上的回答,这是我在片段中修复 WebView 泄漏的方法:

@Override
public void onDetach(){

    super.onDetach();

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

与user1668939的答案不同之处在于,我没有使用任何占位符。只需在WebView引用本身上调用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类,但出于性能问题,我不想这样做)。

实际上,我发现这是最好的解决方案(至少对我来说是这样)。我正在使用Xamarin Android,并且每次关闭带有WebView的Activity时会损失约1 MB。 - Tobias81
我不理解这里的反射。为什么需要反射一个你已经创建的类来找到这个“webView”字段? - android developer

4

在调用WebView.destroy()之前,您需要将WebView从父视图中移除。

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


这是解决我的问题的方法。将它添加到FrameLayout中,然后在活动的onDestroy()中从该FrameLayout中删除Web视图。 - Carl B
1
也适用于我 - 请参阅https://www.alibabacloud.com/forum/read-520以获取详细分析。 - Paul W

3
我解决了令人沮丧的Webview内存泄漏问题,方法如下:(希望能对很多人有所帮助)
基本情况:
- 创建webview需要一个引用(比如说一个活动)。 - 要终止一个进程,可以调用 android.os.Process.killProcess(android.os.Process.myPid()); 方法。
转折点:
默认情况下,一个应用程序中的所有活动都在同一个进程中运行(该进程由包名定义)。 但是:
- 可以在同一应用程序中创建不同的进程。
解决方案:
如果为活动创建了一个不同的进程,则可以使用其上下文来创建webview。当终止此进程时,所有具有对此活动的引用(在此情况下为 webview)的组件都会被终止,最重要的部分是:
强制调用GC来收集此垃圾(即webview)
帮助代码:(一个简单的例子)
总共两个活动: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活动中按下返回键时,会调用onDestroy:

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

这会终止当前进程,即com.processkill.p3。

并且会带走与其相关的webview。

注意:在使用此终止命令时要特别小心(由于明显的原因不建议使用)。不要在活动中实现任何静态方法(在本例中为B活动)。不要从任何其他活动引用此活动(因为它将被终止并且不再可用)。


如何使用此方法处理Webview内的导航(例如,重定向、登录、链接、表单等)? - Megakoresh
你还应该指出,运行在不同进程中的活动不能同时访问相同的静态值、共享首选项或写入相同的sqlite数据库。 - CamHart

3

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

没有违反政策(关于隐藏API或其他方面),只是您无法保证底层系统不会在更新中更改。潜在地,您可以将此代码包装在API级别检查中,并在测试后仅允许其用于新的SDK修订版。 - powerj1984
我认为这是一个不错的解决方法,当第一个创建的带有WebView实例的Activity即将被销毁时。我曾经因为这个问题而苦恼了很久。但是,当你尝试启动另一个带有WebView的Activity时,它可能会引起麻烦。 - qiuping345

2
您可以尝试将Web活动放在单独的进程中,并在活动被销毁时退出,如果多进程处理对您来说不是太费力的话。

1

与“应用程序上下文”解决方案有问题:当WebView尝试显示任何对话框时崩溃。例如,在登录/密码表单提交时出现“记住密码”对话框(还有其他情况吗?)。

可以通过WebView设置的setSavePassword(false)来修复“记住密码”情况。


另一个案例(在Galaxy Nexus,HTC Hero上复制,但不在Galaxy Ace上):从下拉列表中选择(这也会触发WebView显示对话框)。 - Denis Gladkiy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接