WebView 会跨多个进程运行原生代码,以在 Android 应用中呈现网页内容。如果不对 WebView 实例进行管理,可能会导致内存泄漏、内存不足 (OOM) 崩溃和应用性能下降。
本文档介绍了 WebView 多进程内存模型,说明了如何正确管理其生命周期以防止泄漏,并提供了用于诊断内存问题的实用工作流。
了解 WebView 内存架构
如需有效管理 WebView 内存,请了解 Android 如何为网页内容分配资源:
多进程执行: 在 Android 8.0(API 级别 26)及更高版本中,
WebView会跨多个进程将网页内容与应用的核心功能分开(在低 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 上下文,清理视图层次结构,并停止网页后台工作。但是,您可能会发现进程的物理内存(常驻内存大小)不会立即降至 WebView 基准值之前的水平。
这是正常现象。原生运行时缓存、共享库和分配的内存页会一直驻留在进程中,直到操作系统回收它们或进程终止。destroy() 的主要目标是防止用户在网页驱动的屏幕中来回导航时累积 Activity 内存泄漏。
关键调试指标
分析 WebView 内存消耗时,请关注以下指标:
常驻内存大小 (RSS): 映射到进程中的物理 RAM 总量,包括共享代码和库(在 Android Studio 性能分析器中标记为 Total )。
匿名 RSS (RssAnon): 由进程直接分配的内存,没有磁盘上的文件支持(例如原生堆和 JavaScript 运行时分配)。这表示网页内容的主要内存开销(在 Android Studio 性能分析器中标记为 Allocated )。
私有内存占用 (PMF): 匿名 RSS 和交换空间 (zRAM) 的总和。PMF 反映了应用对系统施加的实际不可驱逐的内存负担。
浏览器 PMF 与渲染器 PMF: 应用的主进程使用的内存与隔离的渲染器进程使用的内存。大量网页内容主要会导致渲染器进程出现峰值。
活跃对象计数(
WebViews、Activities、Views): 内存中保留的 活跃界面、上下文和WebView实例的数量。跟踪这些计数可以确定内存增长是由保留的 Java 引用还是仅限原生的分配引起的。私有其他和原生堆: 在
dumpsys meminfo中,原生 C/C++ 分配和自定义内存映射(例如 ChromiumPartitionAlloc或嵌入式 JavaScript 运行时堆)显示在 Native Heap 和 Private Other 下,而不是 Java Heap 下。
如需详细了解进程内存计数器及其类别,请参阅 进程内存术语表。
实用诊断工作流
由于 WebView 跨多个进程运行并分配原生内存,因此请使用以下工具和技术来检查其占用情况:
性能分析和诊断工具
如需检查内存分配和诊断泄漏,请使用以下工具:
Android Studio 内存分析器: 使用内存分析器可视化原生分配,跟踪内存类别随时间的变化,并检测跨屏幕转换的
Activity泄漏。使用 Perfetto 进行内存跟踪: 使用 Perfetto 记录系统级 内存计数器(例如 RSS 和匿名 RSS),以观察整体内存 增长。请注意,
WebView原生引擎分配不会在 Perfetto 的 堆性能分析工具中生成 调用堆栈。使用 Chrome 开发者工具检查网页内容中的 JavaScript 堆快照和 DOM 分配。
检查活跃对象计数
如需确定内存增长是由保留的 Java 框架
对象(例如界面组件)还是原生分配引起的,请检查 Objects
的 dumpsys meminfo部分:
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 互动(例如打开网页链接或导航网页驱动的信息流)期间无法解释的内存增长时,请使用以下分类工作流来隔离泄漏是源于 Java 层还是原生引擎:
隔离泄漏类型(Java 与原生): 运行
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"在重复用户转换(例如打开和关闭网页 文章或滑动浏览信息流)之前和之后。- 观察: 如果
Activities和WebViews计数保持稳定(例如 1-2 个活跃实例),则应用不会泄漏Activity上下文或 JavaWebView实例。
- 观察: 如果
衡量互动中的内存增量(时间序列跟踪): 捕获多个用户互动中的
dumpsys meminfo快照,以计算每次转换的分配率:- 观察: Java 堆保持上限且运行状况良好(使用期间出现峰值,并在垃圾回收后下降),但 Private Other 和 Native Heap 在每次转换时稳步攀升数 MB。这证明泄漏完全发生在 ART 运行时之外的原生内存中。
标准 Java 堆转储 (
.hprof) 不会显示任何问题。
- 观察: Java 堆保持上限且运行状况良好(使用期间出现峰值,并在垃圾回收后下降),但 Private Other 和 Native Heap 在每次转换时稳步攀升数 MB。这证明泄漏完全发生在 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 性能,请参阅以下资源: