Android WebView 内存泄漏排查:一个线上 OOM 的完整复盘

4 阅读3分钟

上周线上报警,一个页面打开 5 次就 OOM。用户反馈"用着用着就闪退了",但本地复现不了。

最后定位到 WebView 的内存泄漏,修复后内存占用从 180MB 降到 40MB。

整个过程走了不少弯路,记录一下。

背景

项目是一个混合开发框架,原生壳 + WebView 加载 Vue 页面。WebView 负责展示,原生负责导航、支付、分享。

线上用户反馈:进入某个页面多次后,App 越来越卡,最后闪退。

看崩溃日志,全是 OutOfMemoryError ,堆栈指向 WebView 相关。

排查过程

第一步:本地复现

本地用同样的页面、同样的操作,复现不了。内存稳定在 80MB 左右。

怀疑是用户设备问题,或者特定场景。

第二步:加日志线上监控

在关键节点加内存监控日志:

Runtime runtime = Runtime.getRuntime();
long usedMem = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024;
Log.i("Memory", "Activity: " + getClass().getSimpleName() + ", Used: " + usedMem + "MB");

发灰度版本,收集用户数据。

发现:每次打开页面,内存涨 30-40MB,关闭页面后只降 5-10MB。打开 5 次后,内存到 200MB+,触发 OOM。

第三步:怀疑 Activity 泄漏

用 LeakCanary 检测,果然发现 Activity 泄漏。

泄漏链:

Activity → WebView → WebViewClient → Activity 的匿名内部类

问题代码:

webView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        // 这里持有 Activity 引用
        startActivity(new Intent(MainActivity.this, DetailActivity.class));
        return true;
    }
});

WebViewClient 是匿名内部类,隐式持有 Activity 引用。而 WebView 本身又持有 WebViewClient ,形成循环引用。

但这不是根因,因为 WebViewClient 泄漏很常见,通常不会导致这么严重的 OOM。

第四步:深挖 WebView 的坑

查资料发现,WebView 的内存回收有几个已知问题:

  1. WebView 不会自动释放渲染进程

WebView 在独立进程渲染,但关闭页面时,渲染进程不会立刻退出,内存占用持续。

  1. JavaScript 对象持有原生引用

如果 JS 端有对象持有原生接口的引用,GC 时无法回收。

  1. WebView 的 Context 必须用 Application

很多人习惯传 Activity 作为 WebView 的 Context ,这会导致 Activity 被 WebView 长期持有。 检查代码,果然:

// 错误写法
WebView webView = new WebView(this); // this 是 Activity

修复方案

修复 1:WebView 改用 Application Context

// 正确写法
WebView webView = new WebView(getApplicationContext());

这样 WebView 持有的是 Application,不会拖累 Activity。

修复 2:WebViewClient 改为静态内部类

private static class SafeWebViewClient extends WebViewClient {
    private WeakReference<Activity> activityRef;
    
    SafeWebViewClient(Activity activity) {
        this.activityRef = new WeakReference<>(activity);
    }
    
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Activity activity = activityRef.get();
        if (activity != null && !activity.isFinishing()) {
            activity.startActivity(new Intent(activity, DetailActivity.class));
        }
        return true;
    }
}

用 WeakReference 避免强引用持有 Activity。

修复 3:页面销毁时主动释放 WebView

@Override
protected void onDestroy() {
    if (webView != null) {
        // 先移除父布局
        ViewGroup parent = (ViewGroup) webView.getParent();
        if (parent != null) {
            parent.removeView(webView);
        }
        
        // 停止加载、清除缓存、销毁
        webView.stopLoading();
        webView.loadUrl("about:blank");
        webView.clearHistory();
        webView.removeAllViews();
        webView.destroy();
        webView = null;
    }
    super.onDestroy();
}

注意顺序:先 removeView ,再 stopLoading ,最后 destroy 。顺序错了可能崩溃。

修复 4:WebView 放独立进程

在 AndroidManifest.xml 里配置:

<activity 
    android:name=".WebActivity"
    android:process=":web" />

WebView 运行在独立进程,即使内存泄漏,也只是子进程 OOM,不会导致主 App 崩溃。

代价是进程间通信复杂一点,但稳定性大幅提升。

验证结果

修复后发灰度,监控一周:

内存占用峰值:180MB → 40MB

OOM 崩溃率:0.8% → 0.02%

页面打开速度:提升 15%(因为渲染进程复用更合理)

几个教训

  1. WebView 的 Context 必须用 Application

这是官方文档里提过的,但很多老代码没遵守。

  1. 匿名内部类是泄漏重灾区

Handler、Runnable、WebViewClient、WebChromeClient,都要小心。

  1. 销毁顺序不能错

先 removeView,再 stopLoading,最后 destroy。直接 destroy 可能触发空指针。

  1. 独立进程是兜底方案

如果业务复杂、WebView 用得多,直接放独立进程,省得折腾。

最后

WebView 是 Android 的坑王之一,内存、卡顿、兼容性,问题一堆。

但混合开发又离不开它,只能踩一个填一个。

你们项目里有类似的坑吗?评论区聊聊。