WebView中的内存泄漏

时间:2010-06-28 07:16:30

标签: android android-webview

我有一个使用xml布局的活动,其中嵌入了WebView。我根本没有在我的活动代码中使用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   即使没有使用,也是的组件   防止记忆   在屏幕旋转/ app后收集g   重启......这是当前的一个错误   实施,或者有什么   具体是一个人需要做的时候   使用WebViews

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

9 个答案:

答案 0 :(得分:53)

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

webView = new WebView(getApplicationContext());

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

答案 1 :(得分:32)

我对这种方法运气不错:

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

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

答案 2 :(得分:10)

这是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;
        }
    }
}

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

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

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

请注意,此webclient应处理常见情况,但可能不像常规Web客户端那样功能齐全。例如,我没有对像flash这样的东西进行测试。

答案 3 :(得分:7)

基于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类,我不想为性能问题做)。

答案 4 :(得分: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();
}

答案 5 :(得分:3)

我修复了像这样令人沮丧的Webview的内存泄漏问题:

(我希望这对许多人有帮助)

<强>基本

  • 要创建Web视图,需要引用(例如活动)。
  • 要杀死进程:

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

转折点:

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

  

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

<强>解决方案: 如果为活动创建了不同的流程,则可以使用其上下文创建Web视图。当这个过程被杀死时,所有引用了这个活动的组件(在这种情况下都是webview)都被杀掉了,主要的好处是:

  

强制调用GC来收集垃圾(webview)。

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

总共两项活动:说A&amp;乙

清单文件:

<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&gt;乙

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)。不要使用任何其他参考此活动(因为它将被杀死,不再可用)。

答案 6 :(得分:2)

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

答案 7 :(得分:2)

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

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

答案 8 :(得分:1)

“app context”解决方法存在问题:WebView尝试显示任何对话框时崩溃。例如,在登录/传递表单子目录中记住“记住密码”对话框(任何其他情况?)。

可以使用WebView设置'setSavePassword(false)修复“记住密码”的情况。