WebView 在多个进程中运行原生代码,以在 Android 应用中呈现 Web 内容。如果 WebView 实例不受管理,可能会导致内存泄漏、内存不足 (OOM) 崩溃和应用性能下降。
本文档介绍了 WebView 多进程内存模型,说明了如何正确管理其生命周期以防止泄漏,并提供了用于诊断内存问题的实用工作流。
了解 WebView 内存架构
为了有效管理 WebView 内存,请了解 Android 如何为网页内容分配资源:
多进程执行:在 Android 8.0(API 级别 26)及更高版本中,
WebView会在多个进程中将 Web 内容与应用的核心功能分开(在低 RAM 设备上,它可能会回退到单个进程):- 宿主(浏览器)进程:运行
Activity以及 Java 或 Kotlin 代码的主应用进程。 - 隔离的渲染器进程:一个单独的沙盒进程 (
SandboxedProcessService),用于解析 HTML 和 CSS、执行 JavaScript 以及渲染网页。
- 宿主(浏览器)进程:运行
原生内存占用空间:大多数
WebView内存(包括渲染的图形、DOM 树和 JavaScript 运行时内存)都分配在原生内存中,而不是 Java 堆上。Java 堆转储 (.hprof) 仅显示轻量级 Java 封装容器对象,不会捕获网页内容使用的真实内存。原生内存的系统影响:与受应用的
maxHeap限制且会因OutOfMemoryError而快速失败的 Java 堆分配不同,原生内存可以静默增长到 GB 级。当未释放的原生内存填满物理 RAM 和交换空间 (zRAM) 时,Android 的低内存终止守护程序 (LMK) 会开始终止后台进程以回收内存。这会降低设备的整体多任务处理能力,最终导致前台应用被终止。
管理 WebView 生命周期
适当的生命周期管理对于防止内存泄漏至关重要。一个常见的错误是,假设从布局中移除 WebView 或让 Activity 自动完成会释放其内存。
为确保完全清理 Java 上下文引用和原生渲染资源,您必须在宿主组件的生命周期(例如 onDestroy())中明确编排拆解序列,停止有效网页执行、将视图从其容器中分离并释放原生绑定。
清理 WebView 实例
为确保在销毁 Activity 或 Fragment 时实现干净的关闭并释放资源,请执行以下操作:
- 从其父容器 (
ViewGroup) 中移除WebView。 - 停止正在进行的加载并清除导航历史记录。
- 调用
destroy()。 - 清除对
null的引用。
以下示例演示了如何正确清理 WebView:
Kotlin
override fun onDestroy() {
myWebView?.let {
// Remove the WebView from its parent ViewGroup.
(it.parent as? ViewGroup)?.removeView(it)
// Stop active loading and clear history.
it.stopLoading()
it.clearHistory()
// Destroy the instance.
it.destroy()
}
myWebView = null
super.onDestroy()
}
Java
@Override
protected void onDestroy() {
if (myWebView != null) {
// Remove the WebView from its parent ViewGroup.
if (myWebView.getParent() instanceof ViewGroup) {
((ViewGroup) myWebView.getParent()).removeView(myWebView);
}
// Stop active loading and clear history.
myWebView.stopLoading();
myWebView.clearHistory();
// Destroy the instance.
myWebView.destroy();
}
myWebView = null;
super.onDestroy();
}
了解销毁后的内存
当您调用 destroy() 时,系统会释放 Activity 上下文、清理视图层次结构并停止 Web 后台工作。不过,您可能会发现,进程的物理内存(常驻内存大小)不会立即降至 WebView 之前的基准值。
这是正常现象。原生运行时缓存、共享库和已分配的内存页会一直驻留在进程中,直到操作系统回收它们或进程终止。destroy() 的主要目标是防止用户在基于 Web 的屏幕之间来回导航时出现累积性 Activity 内存泄漏。
关键调试指标
分析 WebView 内存消耗时,请重点关注以下指标:
常驻内存大小 (RSS):映射到进程中的物理 RAM 总量,包括共享代码和库(在 Android Studio Profiler 中标记为“总计”)。
匿名 RSS (RssAnon):由进程直接分配的内存,不受磁盘上文件的支持(例如原生堆和 JavaScript 运行时分配)。这表示 Web 内容的主要内存开销(在 Android Studio 分析器中标记为“已分配”)。
私有内存占用空间 (PMF):匿名 RSS 和交换空间 (zRAM) 的总和。PMF 反映了应用对系统造成的实际不可逐出内存负担。
浏览器 PMF 与渲染器 PMF:应用的 main 进程使用的内存与隔离的渲染器进程使用的内存。繁重的网页内容主要会导致渲染器进程出现峰值。
实时对象计数(
WebViews、Activities、Views):内存中保存的有效界面、Context 和WebView实例的数量。跟踪这些内容有助于确定内存增长是由保留的 Java 引用还是仅限本地的分配引起的。私有其他内存和原生堆:在
dumpsys meminfo中,原生 C/C++ 分配和自定义内存映射(例如 ChromiumPartitionAlloc或嵌入式 JavaScript 运行时堆)会显示在“原生堆”和“私有其他内存”下,而不是“Java 堆”下。
如需详细了解进程内存计数器及其类别,请参阅进程内存词汇表。
实用诊断工作流
由于 WebView 跨多个进程运行并分配原生内存,因此请使用以下工具和技术来检查其占用空间:
分析和诊断工具
如需检查内存分配和诊断泄漏,请使用以下工具:
Android Studio 内存分析器:使用内存分析器直观呈现原生内存分配情况,跟踪内存类别随时间的变化,并检测屏幕过渡期间的
Activity泄漏。使用 Perfetto 进行内存跟踪:使用 Perfetto 记录系统级内存计数器(例如 RSS 和匿名 RSS),以观察总体内存增长情况。请注意,
WebView原生引擎分配不会在 Perfetto 的堆分析工具中生成调用栈。使用 Chrome 开发者工具检查网页内容中的 JavaScript 堆快照和 DOM 分配。
检查实时对象数量
如需确定内存增长是由保留的 Java 框架对象(例如界面组件)还是原生分配引起的,请检查 dumpsys meminfo 的 Objects 部分:
adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"输出会显示实时对象数量:
Objects
Views: 142 ViewRootImpl: 1
AppContexts: 3 Activities: 1
Assets: 12 AssetManagers: 0
Local Binders: 32 Proxy Binders: 45
Parcel memory: 15 Parcel count: 30
Death Recipients: 2 WebViews: 1
此部分显示了活跃框架对象、IPC 句柄和 Parcel 分配的数量。对于 WebView 诊断,主要关注 Activities 和 WebViews。
反复执行目标用户互动(例如打开和关闭网页界面),并比较计数:
实例泄漏:如果
WebViews或Activities在每次导航时都会递增,并且不会返回到基准值,则表示您的应用正在泄漏 JavaWebView实例或宿主Activity(例如,由于缺少ViewGroup.removeView()或保留的监听器引用)。由于泄露的Activity会将其整个视图树和解码后的图片资源固定在内存中,因此重复访问会迅速耗尽 Java 堆并导致OutOfMemoryError崩溃。原生内存泄漏或 DOM 内存泄漏:如果
WebViews和Activities保持不变,而总进程 RSS 和 Private Other 继续攀升,则内存泄漏源于未释放的原生资源、DOM 元素或 JavaScript 引擎绑定。由于这些分配位于原生内存中并绕过 ART 垃圾收集器,因此标准 Java 内存泄漏检测工具无法检测到它们,并且它们会持续累积,直到操作系统终止应用。
使用 CLI 对隔离的渲染器进程进行分析
仅使用应用的软件包名称运行 dumpsys meminfo 时,只会输出主宿主进程的内存。如需检查呈现网页的隔离渲染器进程,请执行以下操作:
查找隔离的渲染器服务的进程 ID (PID):
adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"输出会显示隔离的进程记录及其 PID RENDERER_PID(例如
22155):Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}使用渲染器进程的 PID 检查其内存细分情况:
adb shell dumpsys meminfo <var>RENDERER_PID</var>检查宿主应用进程,以评估浏览器端占用空间:
adb shell dumpsys meminfo <var>PACKAGE_NAME</var>
检查内存映射和分配
如需查看哪些原生子系统或分配器占用匿名内存,请检查进程内存映射:
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"下表列出了常见的匿名内存标记及其与内存增长的相关性:
| 内存标记 | 子系统 | 与应用和网站内容的相关度 | 内存使用量增加的常见原因? |
|---|---|---|---|
[anon:partition_alloc] |
Chromium PartitionAlloc | WebView 中 DOM 树、渲染缓冲区、V8 JavaScript 堆和 WebAssembly 执行的分配。 |
是(高):加载大型网页、包含丰富媒体内容的 DOM 或未能直接在废弃的 WebView 实例上调用 destroy() 会直接增加此标记的值。 |
[anon:scudo...] 或 [anon:libc_malloc] |
Android 原生堆分配器 (Scudo / jemalloc) | NDK 库、JNI 桥和原生图形流水线使用的一般 C/C++ 原生分配。 | 是(中到高):当原生 JNI 封装容器或第三方 C++ 依赖项在导航过程中保留未释放的分配时,就会发生内存增长。 |
[anon:...](例如 [anon:quickjs_heap...]) |
自定义脚本或原生运行时 | 嵌入式 JavaScript 引擎、自定义 WebAssembly 运行时或自定义原生缓冲区池。 | 是(取决于上下文):在混合应用中很常见,这些应用在原生视图旁边执行脚本引擎,但未能清理运行时绑定。 |
应用内内存 API 的限制
应用内内存 API(例如 Debug.getMemoryInfo 或 ActivityManager.getProcessMemoryInfo)仅测量调用进程。
在多进程模式下,这些 API 无法捕获隔离的渲染器进程所消耗的内存。如需准确评估总内存,请依赖 dumpsys meminfo、Perfetto 或 Android Studio 性能分析器等系统工具。
对混合应用中的高内存用量进行问题排查
在诊断重复 WebView 交互(例如打开网页链接或浏览由 Web 提供支持的 Feed)期间出现的不明原因的内存增长时,请使用以下初步诊断工作流程来确定内存泄漏是源自 Java 层还是原生引擎:
隔离泄漏类型(Java 与原生):在重复的用户过渡(例如打开和关闭网页文章或滑动浏览 Feed)之前和之后运行
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"。- 观察:如果
Activities和WebViews计数保持稳定(例如,1-2 个活跃实例),则表示应用没有泄漏Activity上下文或 JavaWebView实例。
- 观察:如果
衡量不同互动之间的内存增量(时间序列跟踪):捕获多次用户互动中的
dumpsys meminfo快照,以计算每次过渡的分配率:- 观察结果:Java 堆保持在上限且运行正常(使用期间会飙升,垃圾回收后会下降),但 Private Other 和 Native Heap 会在每次过渡时稳步增加几兆字节。这证明内存泄漏完全发生在 ART 运行时之外的原生内存中。标准 Java 堆转储 (
.hprof) 不会显示任何问题。
- 观察结果:Java 堆保持在上限且运行正常(使用期间会飙升,垃圾回收后会下降),但 Private Other 和 Native Heap 会在每次过渡时稳步增加几兆字节。这证明内存泄漏完全发生在 ART 运行时之外的原生内存中。标准 Java 堆转储 (
检查匿名内存映射:使用 ADB 检查进程内存映射(请参阅检查内存映射和分配):
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"- 观察:内存增长集中在
[anon:partition_alloc]或嵌入式脚本引擎堆中,同时 JNI 全局引用缓慢增加。这表示,虽然 Java 视图已被替换,但底层原生网页对象或 JavaScript 绑定未被释放。
- 观察:内存增长集中在
补救措施:
- 确保每个回收或舍弃的
WebView都会明确停止活动脚本 (stopLoading())、清除历史记录并调用destroy()。 - 拆除与已关闭视图关联的自定义 JavaScript 桥接回调或 JNI 全局引用。
- 确认
Private Other和进程 RSS 在导航过渡后趋于稳定。
- 确保每个回收或舍弃的
其他资源
如需详细了解如何调试和分析内存和 WebView 性能,请参阅以下资源: