WebView 和内存

WebView 是一种强大的组件,可让您在 Android 应用中显示网页内容。不过,由于它本质上是一个功能齐全的浏览器引擎 (Chromium),因此具有很大的内存占用空间和复杂的多进程架构。

技术背景:多进程架构

在现代 Android 设备上,WebView 使用多进程模型来提高安全性和稳定性。当应用使用 WebView 时,内存会分布在不同的进程中:

  1. 浏览器进程(应用进程):这是应用的主进程。它包含 Java WebView 对象和 Chromium 引擎的“浏览器”部分。此进程管理界面、网络请求和 GPU 渲染(直接与 Android HWUI 渲染流水线集成)。 与 Chrome 不同,WebView 没有单独的 GPU 进程。
  2. 渲染器进程:此进程负责解析 HTML、执行 JavaScript 和布局。为了安全起见,它与系统的其余部分隔离开来。目前,应用的所有 WebView 都只获得一个渲染器进程(极少数特殊情况除外),这与 Chrome 经常为不同网站使用单独的渲染器进程不同。

WebView 架构

这对记忆有何影响

使用 dumpsys meminfo <your_package> 时,您只会看到浏览器进程(您的应用进程)使用的内存。渲染器进程使用的内存单独计算。

在浏览器进程中,WebView 内存的分布情况如下:

  • Java 堆:包含 WebView Java 封装容器和相关对象。
  • 原生堆:包含 Chromium 浏览器引擎的内部数据结构、缓存和状态。请注意,由于使用了 PartitionAlloc,某些 WebView 原生分配可能不会在 dumpsys meminfo 的“原生堆”下进行统计,而是可能会显示在“其他”或“未知”下。
  • 共享内存:用于共享图形缓冲区和其他数据。dumpsys meminfo 可能无法明确归类。

问题排查工具

Chrome DevTools

用于分析 WebView(渲染器进程)内部内存的最强大工具是 Chrome 开发者工具。

  1. 在应用中启用 WebView 调试:

    // NOTE: In production, this should be gated behind a developer setting
    // or only enabled for debuggable builds to prevent reverse engineering.
    WebView.setWebContentsDebuggingEnabled(true);
    
  2. 通过 USB 连接设备。

  3. 在宿主机上打开 Chrome,然后前往 chrome://inspect/#devices

  4. 找到您的应用,然后点击检查

  5. 在开发者工具窗口中,前往 Memory(内存)标签页,以拍摄堆快照或记录 JavaScript 堆的分配时间轴。

dumpsys meminfo

使用 adb shell dumpsys meminfo --all <package> 可查看内存细分情况。 在输出中查找 WebView 类别和对象数量。

分析渲染器

由于渲染器在单独的进程中运行,因此您无法仅通过剖析应用来剖析其原生堆。您必须专门识别渲染器进程的 PID。

在多个 WebView 处于有效状态时,识别正确的渲染器 PID:

  1. 使用 dumpsys activity

    adb shell dumpsys activity processes <your_package_name>
    

    查找 mConnections 部分。您会看到一个 ConnectionRecord,用于将您的应用与 SandboxedProcessService 相关联。相应进程的 PID 就是您的渲染器。示例

    mConnections:
      - ConnectionRecord{... com.android.memorylab/org.chromium.content.app.SandboxedProcessService0:0 ...}
    
  2. 检查进程名称:渲染器进程通常命名为 com.google.android.webview:sandboxed_processX 或类似名称。如果只有一个应用在使用 WebView,则可能只有一个。

获得 PID 后,您可以使用 heapprofd 对其进行分析。

WebView 内存最佳实践

显式销毁

应用应调用 WebView.destroy() 来指示何时完成实例操作。

虽然 WebView 会尝试确保实例可以进行垃圾回收并自动释放其所有资源,但在 100% 的情况下都很难保证这一点。即使自动垃圾回收机制正常运行,也可能会出现明显的延迟,导致应用保留资源的时间比预期长得多。

如果应用在适当的时间(例如在 Activity.onDestroy() 中)调用 WebView.destroy(),那么保留对 WebView 对象本身的引用不会泄漏任何重要的原生资源。在销毁 WebView 对象后,无需严格将 Activity 字段中对该对象的引用设为 null,因为当 Activity 本身被垃圾回收时,该对象也会被清理。

练习:WebView 内存实操

练习 1:观察多进程占用空间

  1. 启动 MemoryLab 并对应用的内存使用情况进行基准测量:

    adb shell dumpsys meminfo com.android.memorylab
    

    示例基准值(范围)TOTAL PSS: 18915 KB

  2. 点按 Launch WebView (Normal)

  3. 在 WebView 中,点按 Allocate JS Memory (1000 DIVs) 几次。

  4. 再次检查应用的内存:

    adb shell dumpsys meminfo com.android.memorylab
    
  5. 观察到应用进程中的内存与基准相比没有明显增加!这是因为 DOM 元素位于 Renderer 进程中。

  6. 查找渲染器进程:

    adb shell ps -A | grep webview | grep sandboxed
    

    输出示例:

    u0_i9002     14227  1087    1632732 135880 do_epoll_wait       0 S com.google.android.webview:sandboxed_process0
    
  7. 检查渲染器进程的内存(使用其 PID):

    adb shell dumpsys meminfo 14227
    
  8. 观察渲染器进程的高 TOTAL PSS。在我们的示例运行中,在分配了少量内存后,内存使用量便跃升至 ~55MB。请注意,JavaScript 分配(由 V8 引擎处理)通常会影响 dumpsys meminfoPrivate OtherUnknown (mmap) 部分,而不是 Dalvik 堆。

练习 2:Java 端 WebView 泄漏

一个常见错误是在静态字段或泄漏的长生命周期对象中保留 WebView 实例。由于 WebView 对象是一个“锚点”,它会占用原生资源,甚至整个渲染器进程,因此泄露该对象会造成非常严重的后果。

WebView 泄漏影响

  1. MemoryLab 中,点按 Launch WebView (Java Leak)
  2. 在网页加载完毕后,该 activity 会自动关闭(模拟重复导航和内存泄漏累积)。
  3. 点按该按钮 4 次。
  4. 检查应用中的 WebView 实例数:

    adb shell dumpsys meminfo com.android.memorylab
    

    找到底部的对象部分。您会看到 WebViews 的数量已增加到 4。

    rango 上的示例输出(4 个泄露的实例)

     Objects
               Views:       51         ViewRootImpl:        5
         AppContexts:       14           Activities:        5
              Assets:       38        AssetManagers:        0
       Local Binders:       55        Proxy Binders:       77
       Parcel memory:       41         Parcel count:       68
    Death Recipients:        3             WebViews:        4
    
  5. 捕获堆转储并使用 AHAT 查找内存泄漏。如果您的路径中没有 ahat,您可以从 Android 树中构建它:

    # Dump heap from device
    adb shell am dumpheap com.android.memorylab /data/local/tmp/heap.hprof
    adb pull /data/local/tmp/heap.hprof
    # Run ahat using the built JAR (found in out/host/linux-x86/framework/)
    java -jar out/host/linux-x86/framework/ahat.jar -p 8888 heap.hprof
    
  6. 在 AHAT Web 界面 (localhost:8888) 中,点击顶部菜单中的分配链接(或网站),查看总体内存用量。

    AHAT 分配

  7. 搜索 android.webkit.WebView 类。点击其实例数量即可查看所有实时实例。您应该会在列表中看到多个实例。

    AHAT WebView 实例

  8. 点击泄露的某个 WebView 实例。向下滚动到 Sample Path from GC Root 部分。您会看到它被 com.android.memorylab.WebViewActivity 中的 sLeakedWebViews 列表所持有。

    AHAT Path to GC Root


← 原生 | ↑ 向上 | 应用代码 →