【问题标题】:Memory leak in WebViewWebView 中的内存泄漏
【发布时间】:2010-06-28 07:16:29
【问题描述】:

我有一个使用嵌入 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/

确实是一个非常有趣的帖子。 最近我过得很艰难 解决我的内存泄漏问题 安卓应用。最终结果是 我的 xml 布局包括一个 WebView 组件,即使不使用,是 防止记忆被 g-在屏幕旋转/应用程序后收集 重启……这是当前的错误吗 实施,或者有什么 具体到什么时候需要做 使用 WebViews

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

【问题讨论】:

  • 动态创建WebView会出现这种情况吗?
  • 我刚刚测试过,但没有任何区别。
  • 同时我在code.google.com/p/android/issues/detail?id=9375 提交了一个错误报告,但也许有人有解决方法;那么请发布它。
  • 好的。另一个几乎没有修改的测试:当我以编程方式创建 WebView 并将“this”(活动)设置为上下文时,活动仍将保留在内存中。但是,当我使用 getApplicationContext() 时,没关系,并且在没有任何保留引用的情况下删除了活动。

标签: android android-webview


【解决方案1】:

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

webView = new WebView(getApplicationContext());

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

【讨论】:

  • ty for that getApplicationContext() 在创建 WebView 时实际上解决了我的内存泄漏问题。但是当我将 webview 添加到另一个 ViewGroup 时,内存泄漏再次出现。我的猜测是采用父级的baseContext。有什么解决方法吗?我也用 getApplicationContext() 创建了父级......所以我想我已经没有理论了
  • 请注意,使用应用程序上下文意味着您将无法单击 webview 中的链接,因为这样做会导致崩溃:“从 Activity 上下文之外调用 startActivity()需要 FLAG_ACTIVITY_NEW_TASK 标志。这真的是你想要的吗?"
  • 此外,任何时候 webview 尝试创建一个对话框(例如,记住密码等),webview 都会崩溃,因为它需要一个活动上下文。
  • 当 webView 想要显示对话框时它会粉碎应用程序,例如询问“你想要保存密码”然后它会粉碎:(
  • 这个错误在 android 5.0 lilpops 中仍然存在。所以我们需要牢记这一点,我们仍然需要解决这个错误问题。
【解决方案2】:

这个方法我运气不错:

将 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方法,从view层级中移除,否则会泄露。

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 可以工作,并且我确信其他布局尺寸也可以工作。

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

【讨论】:

  • 这对我来说绝对有用。非常感谢!我所做的唯一改进是使用活动上下文而不是应用程序上下文,我希望这将使我免受其他地方提到的当 Flash 或对话框出现在 web 视图中时发生的崩溃。
  • 太糟糕了,它似乎对我不起作用.. sConfigCallback 仍然存在引用:/
【解决方案3】:

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

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

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

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

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

【讨论】:

  • 发现这种实现方式只有一个问题:在 DialogFragment 中有 WebView 我在构造函数中收到了 ContextThemeWrapper 而不是 Activity 和 ClassCastException。
  • 如果 Web 视图中有 Flash 内容,您会从 com.adobe.flashplayer.FlashPaintSurface 获得 ClassCastException... 至少在 Kindle Fire 上是这样。
  • 2013 年 2 月 1 日更新以解决 BrowserFrame.sConfigCallback 中的额外泄漏问题
  • 对我来说记忆还没有释放
  • 确保在活动的 onDestroy() 中调用 NonLeakingWebView.destroy()
【解决方案4】:

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

@Override
public void onDetach(){

    super.onDetach();

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

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

## 更新##

如果你和我一样,在几个片段中都有 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();
        }
    }
}

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

【讨论】:

  • 实际上我发现这是最好的解决方案(至少对我来说)。我正在使用 Xamarin Android,每次关闭带有 WebView 的 Activity 时都会丢失约 1 MB。
【解决方案5】:

读完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 修订版。
  • 我认为当第一个创建的带有 WebView 实例的 Activity 将被销毁时,这是修复它的好方法。我因这个问题受了很多苦。但是,当您尝试使用 WebView 启动另一个 Activity 时,可能会出现问题。
【解决方案6】:

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

(希望对大家有帮助)

基础知识

  • 要创建 web 视图,需要一个引用(比如一个活动)。
  • 终止进程:

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

转折点:

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

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

解决方案: 如果为活动创建了不同的进程,则可以使用其上下文来创建 web 视图。当这个进程被杀死时,所有引用这个活动的组件(在这种情况下是 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 上按下 backKey 时,会调用 onDestroy:

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

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

并带走引用它的 webview

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

【讨论】:

  • 如何使用这种方法处理 web 视图中的导航(即重定向、登录、链接、表单等)?
  • 您还应该指出,在不同进程中运行的活动无法访问相同的静态值、共享首选项或同时写入相同的 sqlite 数据库。
【解决方案7】:

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

【讨论】:

    【解决方案8】:

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

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

    【讨论】:

    • 这就是为我解决问题的方法。将其添加到 FrameLayout,然后在 Activity 的 onDestroy() 中从该 FrameLayout 中删除 Web 视图。
    • 也为我工作 - 请参阅 alibabacloud.com/forum/read-520 了解详细分析
    【解决方案9】:

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

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

    【讨论】:

    • 另一种情况(在 Galaxy Nexus、HTC Hero 上重现,但在 Galaxy Ace 上没有):从下拉列表中选择(这也触发 WebView 显示对话框)。
    猜你喜欢
    • 2011-07-15
    • 1970-01-01
    • 2017-01-22
    • 1970-01-01
    • 2013-06-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-28
    相关资源
    最近更新 更多