上周线上报警,一个页面打开 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 的内存回收有几个已知问题:
- WebView 不会自动释放渲染进程
WebView 在独立进程渲染,但关闭页面时,渲染进程不会立刻退出,内存占用持续。
- JavaScript 对象持有原生引用
如果 JS 端有对象持有原生接口的引用,GC 时无法回收。
- 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%(因为渲染进程复用更合理)
几个教训
- WebView 的 Context 必须用 Application
这是官方文档里提过的,但很多老代码没遵守。
- 匿名内部类是泄漏重灾区
Handler、Runnable、WebViewClient、WebChromeClient,都要小心。
- 销毁顺序不能错
先 removeView,再 stopLoading,最后 destroy。直接 destroy 可能触发空指针。
- 独立进程是兜底方案
如果业务复杂、WebView 用得多,直接放独立进程,省得折腾。
最后
WebView 是 Android 的坑王之一,内存、卡顿、兼容性,问题一堆。
但混合开发又离不开它,只能踩一个填一个。
你们项目里有类似的坑吗?评论区聊聊。