管理和诊断 WebView 内存

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 实例

为确保在销毁 ActivityFragment 时干净地关闭并释放资源,请执行以下操作:

  1. 从其父容器 (ViewGroup) 中移除 WebView
  2. 停止活跃加载并清除导航记录。
  3. 调用 destroy()
  4. 将引用清除为 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: 应用的主进程使用的内存与隔离的渲染器进程使用的内存。大量网页内容主要会导致渲染器进程出现峰值。

  • 活跃对象计数(WebViewsActivitiesViews: 内存中保留的 活跃界面、上下文和 WebView 实例的数量。跟踪这些计数可以确定内存增长是由保留的 Java 引用还是仅限原生的分配引起的。

  • 私有其他和原生堆: 在 dumpsys meminfo 中,原生 C/C++ 分配和自定义内存映射(例如 Chromium PartitionAlloc 或嵌入式 JavaScript 运行时堆)显示在 Native Heap 和 Private Other 下,而不是 Java Heap 下。

如需详细了解进程内存计数器及其类别,请参阅 进程内存术语表

实用诊断工作流

由于 WebView 跨多个进程运行并分配原生内存,因此请使用以下工具和技术来检查其占用情况:

性能分析和诊断工具

如需检查内存分配和诊断泄漏,请使用以下工具:

  • Android Studio 内存分析器: 使用内存分析器可视化原生分配,跟踪内存类别随时间的变化,并检测跨屏幕转换的Activity泄漏。

  • 使用 Perfetto 进行内存跟踪: 使用 Perfetto 记录系统级 内存计数器(例如 RSS 和匿名 RSS),以观察整体内存 增长。请注意,WebView 原生引擎分配不会在 Perfetto 的 堆性能分析工具中生成 调用堆栈。使用 Chrome 开发者工具检查网页内容中的 JavaScript 堆快照和 DOM 分配。

检查活跃对象计数

如需确定内存增长是由保留的 Java 框架 对象(例如界面组件)还是原生分配引起的,请检查 Objectsdumpsys 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 诊断,主要关注 ActivitiesWebViews

重复执行目标用户互动(例如打开和关闭网页屏幕),并比较计数:

  • 实例泄漏: 如果 WebViewsActivities 在每次 导航时递增且不返回基准值,则应用会泄漏 Java WebView 实例或宿主 Activity(例如,由于缺少 ViewGroup.removeView() 或保留的监听器引用)。由于泄漏的 Activity 会将其整个视图树和解码的图片资源固定在内存中,因此重复访问会迅速耗尽 Java 堆并导致 OutOfMemoryError 崩溃。

  • 原生或 DOM 泄漏: 如果 WebViewsActivities 保持不变,而进程 RSS 总量和 Private Other 继续攀升,则泄漏源于未释放的原生资源、DOM 元素或 JavaScript 引擎绑定。由于这些分配驻留在原生内存中并绕过 ART 垃圾回收器,因此它们对标准 Java 泄漏检测工具不可见,并会持续累积,直到操作系统终止应用。

使用 CLI 对隔离的渲染器进程进行性能分析

使用应用的软件包名称运行 dumpsys meminfo 仅输出主宿主进程的内存。如需检查呈现网页的隔离渲染器进程,请执行以下操作:

  1. 查找隔离的渲染器服务的进程 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:...}
    
  2. 使用渲染器进程的 PID 检查其内存细分:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. 检查宿主应用进程以评估浏览器端占用情况:

    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.getMemoryInfoActivityManager.getProcessMemoryInfo)仅衡量调用进程。 在多进程模式下,这些 API 无法捕获隔离的渲染器进程消耗的内存。如需准确评估总内存,请依赖于 dumpsys meminfo、Perfetto 或 Android Studio 性能分析器等系统工具。

对混合应用中的高内存进行分类

在诊断重复 WebView 互动(例如打开网页链接或导航网页驱动的信息流)期间无法解释的内存增长时,请使用以下分类工作流来隔离泄漏是源于 Java 层还是原生引擎:

  1. 隔离泄漏类型(Java 与原生): 运行 dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" 在重复用户转换(例如打开和关闭网页 文章或滑动浏览信息流)之前和之后。

    • 观察: 如果 ActivitiesWebViews 计数保持稳定(例如 1-2 个活跃实例),则应用不会泄漏 Activity 上下文或 Java WebView 实例。
  2. 衡量互动中的内存增量(时间序列跟踪): 捕获多个用户互动中的 dumpsys meminfo 快照,以计算每次转换的分配率:

    • 观察: Java 堆保持上限且运行状况良好(使用期间出现峰值,并在垃圾回收后下降),但 Private OtherNative Heap 在每次转换时稳步攀升数 MB。这证明泄漏完全发生在 ART 运行时之外的原生内存中。 标准 Java 堆转储 (.hprof) 不会显示任何问题。
  3. 检查匿名内存映射: 使用 ADB 检查进程内存映射 (请参阅检查内存映射和分配):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • 观察: 内存增长集中在 [anon:partition_alloc] 或嵌入式脚本引擎堆中,同时 JNI 全局引用缓慢攀升。这表明,虽然 Java 视图被替换,但底层原生网页对象或 JavaScript 绑定未释放。
  4. 修复

    • 确保每个回收或废弃的 WebView 都会明确停止活跃脚本 (stopLoading())、清除历史记录并调用 destroy()
    • 拆解与已关闭视图关联的自定义 JavaScript 桥回调或 JNI 全局引用。
    • 确认 Private Other 和进程 RSS 在导航转换后稳定。

其他资源

如需详细了解如何调试和分析内存和 WebView 性能,请参阅以下资源: