WebView中的内存泄漏


75

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

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

我已经尝试过

webView.destroy();
webView = null;

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

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

我发现其他开发人员也经历过类似的事情,请参见Filipe Abrantes的回复:http ://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-android/

确实是一个非常有趣的帖子。最近,我很难对Android应用程序中的内存泄漏进行故障排除。最后,事实证明,我的xml布局包含一个WebView组件,即使不使用它,也阻止了屏幕旋转/应用重启后对内存进行g收集……这是当前实现的错误,还是存在某些问题使用WebView时需要做的特定工作

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


如果您动态创建WebView,会发生这种情况吗?
ktingle 2010年

1
我只是测试了一下,但没有任何区别。
Mathias Conradt 2010年

1
同时,我在code.google.com/p/android/issues/detail?id=9375上提交了一个错误报告,但也许有人可以解决该问题;然后请发布。
Mathias Conradt 2010年

3
好。另一个修改很少的测试:当我以编程方式创建WebView并将“ this”(活动)设置为上下文时,该活动仍将保留在内存中。当我使用getApplicationContext()时,没关系,并且没有任何保留引用的情况下删除了活动get。
Mathias Conradt 2010年

Answers:


57

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

webView = new WebView(getApplicationContext());

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


1
在创建WebView时,该getApplicationContext()确实可以解决我的内存泄漏。但是,当我将webview添加到另一个ViewGroup时,内存泄漏再次出现。我的疯狂猜测是采用父级的baseContext。周围有什么工作吗?我创建getApplicationContext(父)太...所以我想我出的理论
weakwire

28
请注意,使用应用程序上下文意味着您将无法单击Web视图中的链接,因为这样做会导致崩溃:“从Activity上下文外部调用startActivity()需要FLAG_ACTIVITY_NEW_TASK标志。这真的吗?你想要什么?”
emmby 2012年

5
另外,每当Webview尝试创建对话框(例如,记住密码等)时,WebView都会崩溃,因为它期望活动上下文。
markshiz 2012年

1
当webView要显示对话框(例如询问“是否要保存密码”)时,它会粉碎应用程序,然后它会粉碎:(
Dawid Drozd

2
这个错误在android 5.0 lilpops中仍然有效。因此,我们需要牢记这一点,我们仍然需要解决此错误问题。
GFxJamal 2015年

35

我有这种方法的运气:

在您的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
这绝对对我有用。非常感谢!我所做的唯一改进是使用了活动上下文而不是应用程序上下文,我希望它将使我免于Flash或对话框出现在Web视图中时发生的其他崩溃问题。
SilithCrowe,2012年

太糟糕了,它似乎对我不起作用。sConfigCallback仍然保留引用:/
Surya Wijaya

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

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

请注意,此Web客户端应处理常见情况,但功能可能不如常规Web客户端那么全。例如,我还没有针对Flash等进行过测试。


发现与该实现方式有关的一个问题:在DialogFragment中使用WebView我在构造函数中收到了ContextThemeWrapper,而不是Activity和ClassCastException。
sandrstar 2012年

如果您在Webview中具有Flash内容,则至少在Kindle Fire上,您将从com.adobe.flashplayer.FlashPaintSurface ...获取ClassCastException。
克里斯托弗·佩里

2013年2月1日更新,以解决BrowserFrame.sConfigCallback中的其他泄漏
emmby 2013年

对我而言,记忆仍然没有释放
塔伦·塔克

确保您在活动的onDestroy()中调用了NonLeakingWebView.destroy()
emmby 2013年

8

基于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类,而我不想这样做是为了解决性能问题)。


实际上,我发现这是最好的解决方案(对我来说至少)。我使用的是Xamarin Android,每次关闭带有WebView的活动都会丢失约1 MB的内存。
Tobias81

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

3

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

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

基础知识

  • 要创建网络视图,需要参考(例如活动)。
  • 终止进程:

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

转折点:

默认情况下,所有活动在一个应用程序中以相同的进程运行。(该过程由程序包名称定义)。但:

可以在同一应用程序中创建不同的进程。

解决方案: 如果为活动创建了不同的过程,则可以使用其上下文来创建Web视图。当该过程被终止时,所有引用该活动的组件(在本例中为webview)都将终止,主要的期望部分是:

强制调用GC来收集此垃圾(网络视图)。

帮助代码:(一种简单的情况)

共两项活动:说出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

并删除引用它的webview

注意:使用此kill命令时要格外小心。(由于明显原因,不建议使用)。不要在活动(在这种情况下为活动B)中实现任何静态方法。请勿使用其他任何对此活动的引用(因为该活动将被杀死并且不再可用)。


如何使用这种方法(即重定向,登录,链接,表单等)来处理Web视图内的导航?
Megakoresh

您还应该指出,在单独的进程中运行的活动不能访问相同的静态值,共享的首选项或同时写入相同的sqlite db。
CamHart

2

您可以尝试将Web活动放在一个单独的进程中,并在该活动被销毁时退出,如果对您而言,多进程处理不是一件大事。


2

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

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


这就是为我解决问题的原因。将其添加到FrameLayout,然后在活动的onDestroy()中从该FrameLayout删除Web视图。
卡尔B

为我工作,以及-看到alibabacloud.com/forum/read-520了详细的分析
保瓦

1

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

对于“记住密码”的情况,可以通过WebView设置进行修复setSavePassword(false)


另一种情况(在Galaxy Nexus,HTC Hero上复制,但不在Galaxy Ace上复制):从下拉列表中进行选择(这也触发WebView显示对话框)。
Denis Gladkiy 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.