管理和诊断 WebView 内存

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

为确保在销毁 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 上下文、清理视图层次结构并停止 Web 后台工作。不过,您可能会发现,进程的物理内存(常驻内存大小)不会立即降至 WebView 之前的基准值。

这是正常现象。原生运行时缓存、共享库和已分配的内存页会一直驻留在进程中,直到操作系统回收它们或进程终止。destroy() 的主要目标是防止用户在基于 Web 的屏幕之间来回导航时出现累积性 Activity 内存泄漏。

关键调试指标

分析 WebView 内存消耗时,请重点关注以下指标:

  • 常驻内存大小 (RSS):映射到进程中的物理 RAM 总量,包括共享代码和库(在 Android Studio Profiler 中标记为“总计”)。

  • 匿名 RSS (RssAnon):由进程直接分配的内存,不受磁盘上文件的支持(例如原生堆和 JavaScript 运行时分配)。这表示 Web 内容的主要内存开销(在 Android Studio 分析器中标记为“已分配”)。

  • 私有内存占用空间 (PMF):匿名 RSS 和交换空间 (zRAM) 的总和。PMF 反映了应用对系统造成的实际不可逐出内存负担。

  • 浏览器 PMF 与渲染器 PMF:应用的 main 进程使用的内存与隔离的渲染器进程使用的内存。繁重的网页内容主要会导致渲染器进程出现峰值。

  • 实时对象计数(WebViewsActivitiesViews:内存中保存的有效界面、Context 和 WebView 实例的数量。跟踪这些内容有助于确定内存增长是由保留的 Java 引用还是仅限本地的分配引起的。

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

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

实用诊断工作流

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

分析和诊断工具

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

  • Android Studio 内存分析器:使用内存分析器直观呈现原生内存分配情况,跟踪内存类别随时间的变化,并检测屏幕过渡期间的 Activity 泄漏。

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

检查实时对象数量

如需确定内存增长是由保留的 Java 框架对象(例如界面组件)还是原生分配引起的,请检查 dumpsys meminfoObjects 部分:

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 交互(例如打开网页链接或浏览由 Web 提供支持的 Feed)期间出现的不明原因的内存增长时,请使用以下初步诊断工作流程来确定内存泄漏是源自 Java 层还是原生引擎:

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

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

    • 观察结果:Java 堆保持在上限且运行正常(使用期间会飙升,垃圾回收后会下降),但 Private OtherNative Heap 会在每次过渡时稳步增加几兆字节。这证明内存泄漏完全发生在 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 性能,请参阅以下资源: