Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


title: OOM、进程资源治理与 WebView Renderer 恢复 chapter: '20.5' section: '20.5' status: finalized applicable_versions: Android 10 (API 29) - Android 17 (API 37) last_verified: '2026-08-21' last_verified_against: AOSP android-17.0.0_r1 ART, Bionic, HWUI and ActivityThread; android17-6.18-2026-06_r6 kernel; Android Developers API 30 ApplicationExitInfo/ActivityManager low-memory kill report docs; API 37 OOM profiling, memory, Bitmap and ComponentCallbacks2 docs confidence: medium-high sources:

  • type: aosp path: https://android.googlesource.com/platform/art/+/refs/tags/android-17.0.0_r1/runtime/gc/heap.cc
  • type: aosp path: https://android.googlesource.com/platform/art/+/refs/tags/android-17.0.0_r1/runtime/gc/heap.h
  • type: aosp path: https://android.googlesource.com/platform/art/+/refs/tags/android-17.0.0_r1/runtime/gc/heap-inl.h
  • type: aosp path: https://android.googlesource.com/platform/art/+/refs/tags/android-17.0.0_r1/runtime/thread.cc
  • type: aosp path: https://android.googlesource.com/platform/art/+/refs/tags/android-17.0.0_r1/runtime/native/jdk_internal_misc_Unsafe.cc
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/libs/hwui/hwui/Bitmap.cpp
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/libs/hwui/jni/Bitmap.cpp
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/app/ActivityThread.java
  • type: aosp path: https://android.googlesource.com/platform/bionic/+/refs/tags/android-17.0.0_r1/libc/bionic/pthread_create.cpp
  • type: aosp path: https://android.googlesource.com/platform/bionic/+/refs/tags/android-17.0.0_r1/libc/include/malloc.h
  • type: aosp path: https://android.googlesource.com/kernel/common/+/refs/tags/android17-6.18-2026-06_r6/fs/file.c
  • type: aosp path: https://android.googlesource.com/kernel/common/+/refs/tags/android17-6.18-2026-06_r6/kernel/fork.c
  • type: aosp path: https://android.googlesource.com/kernel/common/+/refs/tags/android17-6.18-2026-06_r6/mm/mmap.c
  • type: official path: https://developer.android.com/blog/posts/prioritizing-memory-efficiency-essential-steps-for-android-17
  • type: official path: https://developer.android.com/reference/android/content/ComponentCallbacks2
  • type: official path: https://developer.android.com/reference/android/os/ProfilingTrigger#TRIGGER_TYPE_OOM
  • type: official path: https://developer.android.com/reference/android/app/ApplicationExitInfo
  • type: official path: https://developer.android.com/reference/android/app/ActivityManager
  • type: official path: https://developer.android.com/studio/profile/record-native-allocations
  • type: official path: https://developer.android.com/topic/performance/graphics/manage-memory
  • type: official path: https://developer.android.com/topic/performance/memory-management
  • type: official path: https://developer.android.com/guide/topics/manifest/application-element
  • type: blog path: Clippings/Android 应用稳定性剖析与优化 - OOM 发生路径:了解 OOM 是如何产生的.md
  • type: blog path: Clippings/Android 应用稳定性剖析与优化 - 初识内存:内存是什么?.md
  • type: blog path: Clippings/Android 应用稳定性剖析与优化 - 实现 FD 监控:文件描述符(FD)超限怎么办?.md
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/android-17.0.0_r1/core/java/android/webkit/WebViewClient.java
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/android-17.0.0_r1/core/java/android/webkit/RenderProcessGoneDetail.java
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/android-17.0.0_r1/core/java/android/webkit/WebView.java
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/android-17.0.0_r1/core/java/android/webkit/WebViewRenderProcessClient.java
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/android-17.0.0_r1/core/java/android/webkit/WebViewRenderProcess.java
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/android-17.0.0_r1/libs/hwui/private/hwui/WebViewFunctor.h
  • type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/android-17.0.0_r1/libs/hwui/WebViewFunctorManager.cpp
  • type: chromium-source path: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/android_webview/docs/architecture.md
  • type: aosp-kernel path: https://android.googlesource.com/kernel/common/+/refs/tags/android17-6.18-2026-06_r6/mm/vmscan.c
  • type: official path: https://developer.android.com/develop/ui/views/layout/webapps/handle-termination
  • type: official path: https://developer.android.com/develop/ui/views/layout/webapps/managing-webview
  • type: official path: https://developer.android.com/reference/android/webkit/WebViewClient
  • type: official path: https://developer.android.com/reference/android/webkit/WebView
  • type: official path: https://developer.android.com/reference/android/app/ActivityManager#getHistoricalProcessExitReasons(java.lang.String,%20int,%20int)
  • type: official path: https://developer.android.com/reference/androidx/webkit/WebViewCompat tags:
  • oom
  • memory
  • thread-limit
  • fd-leak
  • virtual-memory
  • webview
  • stability
  • renderer-process
  • recovery related_chapters:
  • '20.1'
  • '23.2'
  • '23.3'
  • '23.1'
  • '23.5'
  • '4.2'
  • '4.3'
  • '13.8'
  • '22.16'
  • '26.2' pipeline_stage: ready-to-publish task6_state: reviewed task9_state: reviewed task2b_state: fixed last_idle_audit_at: '2026-08-21T22:35:02+08:00' last_idle_audit_run_id: 20260821-223502-idle-audit-67768f8d last_consolidated_at: '2026-08-24' consolidated_from:
  • src/part5-app/ch20-stability/05-oom-governance.md
  • src/part5-app/ch20-stability/09-webview-renderer-oom-recovery.md

OOM、进程资源治理与 WebView Renderer 恢复

OOM(Out of Memory)通常指内存不足。稳定性平台还常把线程、文件描述符和虚拟地址空间耗尽放在同一类资源问题中,本文一并说明。OOME 专指 Java 的 OutOfMemoryError 异常,它只是资源失败的一种表现。

Java heap 达到 ART 的 growth limit、由 native 层提供存储的 Java API 分配失败、线程创建失败,都可能抛出 OOME。普通 malloc() / mmap() 失败也可能只返回错误;FD(file descriptor,文件描述符)耗尽通常表现为 EMFILE,把高编号 FD 误交给 select() 还可能触发 FORTIFY(Bionic 的运行时边界检查)并 abort();LMKD(Low Memory Killer Daemon,低内存终止守护进程)结束进程时没有 Java 异常。

治理时先保留原始错误、退出原因和进程资源快照,再按资源类型和内存区域选择证据。平台源码按 Android 17 / API 37 / android-17.0.0_r1 核对;Linux 资源限制按 android17-6.18-2026-06_r6 内核源码核对。

OOM 可能来自 Java Heap、Native、图形内存、地址空间或系统低内存终止。WebView Renderer 位于独立进程,它的退出既可能表现为页面白屏,也可能由应用通过回调重建。

OOM 类型、现场证据与恢复边界

OOM 与 OOME 如何出现

同一个“内存不足”告警可能来自完全不同的系统层。先把事件放入正确类别:

事件常见表现主要证据是否经过 Java OutOfMemoryError
ART Java heap(托管对象堆)分配失败Failed to allocate ... growth limit ...OOME 错误文本、Java 栈、heap dump、GC(garbage collection,垃圾回收)/heap 指标
由 native 层提供存储的 Java API 分配失败native alloc、Bitmap OOM、特定 JNI 错误Java 栈、native allocation 栈、PSS/RSS由 API 决定
Java platform thread(传统 Java 线程)创建失败Could not allocate JNI Envpthread_create (...) failedOOME 错误文本、线程数、线程栈、VmSize、进程限制ART 会抛出 OOME
普通 native(C/C++ 层)分配失败malloc/calloc/mmap 返回空指针或 ENOMEM,也可能被调用方转换为异常或 abort()native 栈、heapprofd、maps/smaps、errno(系统错误编号)不一定
低内存结束进程进程被系统终止;Android 11 / API 30+ 的支持设备可记录 ApplicationExitInfo.REASON_LOW_MEMORYApplicationExitInfo(API 30+)、LMKD/系统日志、内存压力
FD 耗尽或高编号 FD 误用EMFILE、创建 socket/pipe/Looper 失败,或 FORTIFY abort/proc/self/fd、rlimit、FD 创建与关闭记录通常否
虚拟地址空间或 VMA(virtual memory area,虚拟内存区域)耗尽mmap 返回 ENOMEM,后续表现取决于调用方/proc/self/maps、VmSize、映射数、失败栈不一定

这张表决定采集工具。Heap dump 是 Java 对象与引用关系的堆快照,看不到所有 native 映射;heapprofd 是采样 native 分配调用栈的分析工具,看不到 Java 对象引用;LMKD 直接终止进程后,崩溃捕获 SDK 没有机会执行收尾代码。

ART 如何抛出 OOME

ART 的多条 native 失败路径会调用 Thread::ThrowOutOfMemoryError()。Android 17 的实现先尝试构造带错误文本的 java.lang.OutOfMemoryError;若构造 OOME 时再次耗尽内存,线程改用 Runtime 启动阶段预先准备的 OOME 对象。

下面是 art/runtime/thread.cc 的关键分支,省略了日志和无关代码。

void Thread::ThrowOutOfMemoryError(const char* msg) {
  if (!tls32_.throwing_OutOfMemoryError) {
    tls32_.throwing_OutOfMemoryError = true;
    ThrowNewException("Ljava/lang/OutOfMemoryError;", msg);
    tls32_.throwing_OutOfMemoryError = false;
  } else {
    Dump(LOG_STREAM(WARNING));
    SetException(
        Runtime::Current()->GetPreAllocatedOutOfMemoryErrorWhenThrowingOOME());
  }
}

预分配对象保证线程还能设置异常状态,却不保证完整错误文本和 Java 栈;递归分支还会输出当前线程信息帮助诊断。Heap::ThrowOutOfMemoryError() 另有 native stack overflow(C/C++ 调用栈空间耗尽)分支,使用 GetPreAllocatedOutOfMemoryErrorWhenHandlingStackOverflow()

OOME 成为 ART 的待处理异常后,控制流回到由 ART 执行的 Java/Kotlin 代码。没有被业务捕获时,它交给 Thread.UncaughtExceptionHandler(线程未捕获异常的最终处理器),Android 默认处理器随后结束进程。自定义处理器必须继续调用安装前保存的默认处理器;API 37 的 ProfilingTrigger.TRIGGER_TYPE_OOM 也把这条调用路径作为自动生成 Java heap dump 的前提之一。

OOME 错误文本用于诊断,格式会随分配器和平台版本变化。监控平台可以提取 allocation size、growth limit 等已知字段做辅助分组,原始文本和栈必须保留,不能把正则结果当成长期接口。

Java Heap OOM 分类与治理

Java heap OOM 要回答三个问题:本次请求多大、GC 后还活着多少对象、到 growth limit 之前还剩多少可用空间。只看“当前 free bytes”很容易误判。

从错误信息定位

典型 Android 17 heap OOME 可能包含以下信息:

java.lang.OutOfMemoryError: Failed to allocate a 48 byte allocation with
3610680 free bytes and 3526KB until OOM, target footprint 536870912,
growth limit 536870912; giving up on allocation because <1% of heap free after GC.

下面保留错误文本中的英文字段,便于直接检索日志;括号内给出含义:

  • allocation size:本次请求大小。请求很小仍失败,通常说明堆已接近限制;一次超大请求则要检查输入尺寸和乘法溢出。
  • free bytes(空闲字节):ART 统计的 heap 空闲总量。它大于请求值也不能单独证明分配可成功;目标 heap space(ART 管理的一块堆区域)是否有足够大的连续块、分配后能否保留最小空闲比例,也会参与判断。
  • until OOM(到 OOME 还可增长的空间)GetFreeMemoryUntilOOME() 给出的剩余增长空间,受当前 footprint 和 growth limit 约束。
  • target footprint(目标堆占用):GC 用来调节 heap 增长的目标值,不等同于硬上限。
  • growth limit(增长上限):应用 Java heap 的增长限制。设备通过 dalvik.vm.heapgrowthlimitdalvik.vm.heapsize 等属性配置 Runtime;android:largeHeap="true" 会让 ActivityThread 调用 VMRuntime.clearGrowthLimit(),普通应用走 clampGrowthLimit()
  • fragmentation(碎片)文本:当空闲总量不小于请求且目标 allocator(内存分配器)支持碎片诊断时,LogFragmentationAllocFailure() 会报告最大连续块。LOS(Large Object Space,ART 存放大对象的堆区域)分支没有这段碎片详情。
  • <1% of heap free after GC:分配后无法保留 ART 要求的最小空闲比例。它描述的是 ART 对 heap 占用比例的限制,不能仅凭这句话断定存在引用泄漏。

ART 分配失败路径

Heap::AllocateInternalWithGc() 会尝试分配、执行适用的 GC,并在满足条件时重试。仍无法得到对象后才调用 Heap::ThrowOutOfMemoryError()

下面的节选展示 Android 17 生成错误文本和选择碎片诊断的条件。

void Heap::ThrowOutOfMemoryError(
    Thread* self,
    size_t byte_count,
    AllocatorType allocator_type) {
  size_t total_bytes_free = GetFreeMemory();
  oss << "Failed to allocate a " << byte_count
      << " byte allocation with " << total_bytes_free
      << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME())
      << " until OOM, target footprint "
      << target_footprint_.load(std::memory_order_relaxed)
      << ", growth limit " << growth_limit_;

  if (total_bytes_free >= byte_count &&
      allocator_type != kAllocatorTypeLOS) {
    // 省略按 allocator_type 选择 AllocSpace 的代码。
    if (!space->LogFragmentationAllocFailure(oss, byte_count)) {
      oss << "; giving up on allocation because <"
          << kMinFreeHeapAfterGcForAlloc * 100
          << "% of heap free after GC.";
    }
  }
  self->ThrowOutOfMemoryError(oss.str().c_str());
}

这段代码说明两点:碎片诊断只在“空闲总量足够”且分配目标不是 LOS 时尝试;错误文本描述的是失败时状态,不能替代 heap dump 中的对象引用关系。

四类 Java heap 问题

类型证据特征治理方向
引用泄漏同一类对象和 GC Root(GC 认为始终存活的引用起点)路径跨场景持续增长修复生命周期、监听器、线程本地变量、静态集合或错误缓存所有权
设计性常驻过大对象都有合法持有者,但场景完成并短暂稳定后的存活集已接近预算缩小模型、分页、按需加载、限制缓存和减少多份表示
分配抖动存活集不高,短时间内频繁创建临时对象,分配速率与 GC 频率很高复用缓冲区、减少中间对象、流式解析、避免热路径装箱
单次大对象或尺寸错误OOME 栈集中在数组、Bitmap、解压或反序列化入口校验输入上限、分块处理、目标尺寸解码、检查宽高与字节数乘法

Heap dump 更适合在资源接近内部告警线、进程仍健康时采集。发生 OOME 后再完整 dump 需要额外内存和 I/O,成功率低,还可能延长用户可见停顿。线上可用趋势采样找到内存上升场景,再在可控设备、受控小流量版本或实验室复现并抓取 HPROF(Android Java heap dump 文件格式)。

Runtime.totalMemory() - Runtime.freeMemory() 表示当前已提交 heap 中尚未空闲的部分,不等于 GC 后存活集。判断泄漏要比较同一场景、同一 GC 状态下的对象数量和 retained size(某对象被回收后可一并释放的内存总量),不能用一次 Runtime 采样下结论。

largeHeap 的边界

largeHeap 只改变应用可使用的 Java heap 上限,不会增加设备物理内存,也不会降低 native、图形缓冲、线程栈和其他进程的压力。更大的 heap 还会容纳更多存活对象,让系统更早承受内存竞争。

可以为少数确有大内存工作集的产品评估 largeHeap,前提是低内存设备、后台切换、进程重建和系统压力测试都有数据。泄漏、无界缓存和错误尺寸分配不能靠它处理。修改 ART 私有字段、反射调用隐藏接口或动态“扩堆”的做法依赖内部实现,也可能破坏 GC 假设,不应进入生产方案。

Native 内存 OOM

Native 分配没有统一的“native heap 上限”。malloc() 可能因地址空间用尽、系统无法继续提供内存、allocator 元数据不足或系统策略失败;mmap()、图形缓冲和线程栈又分别经过不同分配路径。进程也可能在分配函数返回失败前被 LMKD 结束。

哪些路径会转换为 OOME

Android 17 中有明确的转换点:

  • jdk_internal_misc_Unsafe.ccUnsafe_allocateMemory() 调用 malloc();空指针时通过 Thread::ThrowOutOfMemoryError("native alloc") 抛出 OOME。
  • libs/hwui/hwui/Bitmap.cppallocateHeapBitmap() 使用 calloc() 分配普通 Bitmap 像素;返回空指针后,libs/hwui/jni/Bitmap.cpp 的 JNI 入口调用 doThrowOOME()
  • JNI(Java Native Interface,Java/Kotlin 与 C/C++ 代码交互的接口)的某些 API 会在输入无法表示或分配失败时抛出 OOME,例如超长 NewStringUTF。具体结果由 JNI 入口实现决定。
  • 应用或第三方 native 库直接调用 malloc/calloc/realloc/mmap 时,失败结果属于调用方契约。忽略空指针可能转成 SIGSEGV(非法内存访问)信号,主动 abort() 会产生 SIGABRT(主动终止)信号,只有显式调用 JNI/ART 异常接口才会变成 Java OOME。

根据 Android Bitmap 内存说明,普通 Bitmap 自 Android 8.0 起由 native heap 持有像素;Bitmap.Config.HARDWARE 使用 GraphicBuffer / AHardwareBuffer(可供图形系统使用的硬件缓冲区)。两者都能增加进程或系统内存压力,但采集方式和归属不同。Java heap dump 中体积较小的 Bitmap Java 包装对象,不能代表全部像素或图形缓冲成本。

先区分虚拟地址、驻留页和比例分摊

指标回答的问题不能回答的问题
VmSize(Virtual Memory Size)/ /proc/self/maps进程保留和映射了多少虚拟地址区间这些页是否常驻、是否独占
VmRSS / smaps_rollup RSS(Resident Set Size,驻留集大小)当前有多少页位于物理内存共享页应由哪个进程负责
PSS(Proportional Set Size,比例分摊集大小)独占页全额计入、共享页按映射进程数分摊后的进程成本单个 native allocation 的调用栈
Debug.getNativeHeapAllocatedSize()bionic(Android C 标准库)malloc 管理的已分配字节趋势mmap()、线程栈、GraphicBuffer 等全部 native 成本
heapprofd被采样的 malloc/free 调用栈、大小和存活情况未经过受支持 allocator 的所有映射与图形内存

因此,“native heap 指标没涨”不能排除 mmap、线程栈或图形缓冲增长;“VmSize 很大”也不能直接认定物理内存泄漏。

诊断顺序

  1. Android 11 / API 30 及以上,用 ActivityManager.getHistoricalProcessExitReasons() 读取 ApplicationExitInfo(系统保存的历史进程退出记录),区分 OOME crash、native crash 和 REASON_LOW_MEMORY;Android 10 / API 29 没有这组历史退出 API,只能依赖 LMKD/系统日志、崩溃记录和重启前的轻量本地标记。API 30+ 还要先调用 ActivityManager.isLowMemoryKillReportSupported() 判断设备是否支持低内存退出原因;不支持时,低内存终止可能记录为 REASON_SIGNALEDSIGKILL,缺少 REASON_LOW_MEMORY 不能排除系统内存压力。系统直接终止进程时没有可依赖的未捕获异常回调。
  2. 对比同场景的 PSS/RSS、Java heap、native malloc、图形内存、线程和映射数,先确定哪一类内存在增长。
  3. malloc 分配使用 heapprofd 或 Android Studio native allocation profiler,保留分配与释放栈;接入条件受构建类型、profileable(允许性能分析工具连接)配置、设备和系统策略影响。
  4. mmap() 映射解析 /proc/self/mapssmaps(进程的虚拟内存区间及逐区间统计),按文件路径、匿名映射名称和权限汇总。
  5. 图形与媒体对象结合 Perfetto(系统性能时间线工具)、dumpsys meminfo、dma-buf(内核共享缓冲区)/GraphicBuffer 工具和对象生命周期检查。
  6. 对 JNI 资源建立明确的所有权与释放协议:close()、RAII(对象离开作用域时由析构函数释放资源)、引用计数和失败路径都要覆盖,不能只等待 Cleaner(由 GC 触发、执行时机不确定的清理机制)。

采样本身会增加内存与 CPU,线上应限制时长、采样率和目标进程。资源接近内部告警线时优先保存轻量计数与场景,详细分析放到受控小流量版本或可复现设备。

线程数 OOM(pthread_create 失败)

pthread_create() 是 POSIX 线程创建接口。每个已经启动的 Java platform thread 都对应 native thread,并占用线程栈、guard 区(放在线程栈边缘、用于检测越界的不可访问区)、ART Thread、JNI 环境和内核 task(可调度执行单元)等资源。线程数增加会同时消耗虚拟地址空间、native 内存和调度能力;协程或排队任务的数量不能直接换算为操作系统线程数。

Android 17 的失败分支

Thread::CreateNativeThread() 先创建 ART Thread 和扩展 JNI 环境 JNIEnvExt,设置 pthread 栈大小,再调用 pthread_create()。下面是失败后错误文本的关键代码。

std::string msg(
    child_jni_env_ext.get() == nullptr
        ? StringPrintf("Could not allocate JNI Env: %s",
                       error_msg.c_str())
        : StringPrintf("pthread_create (%s stack) failed: %s",
                       PrettySize(stack_size).c_str(),
                       strerror(pthread_create_result)));
soa.Self()->ThrowOutOfMemoryError(msg.c_str());

Could not allocate JNI Env 说明失败发生在 pthread 启动前;pthread_create ... failed 要继续看 strerror() 给出的错误原因。Android 17 的 Bionic 在 stack/TLS(线程局部存储)映射失败时返回 EAGAIN,其含义是当前资源不足、稍后重试可能成功。内核 clone 还可能因 task 数、RLIMIT_NPROC(按用户限制可创建的进程/线程数量)或其他资源不足返回错误。android17-6.18-2026-06_r6/kernel/fork.ccopy_process() 会在相关限制失败时返回 -EAGAIN;诊断时必须保留设备上的原始 errno(系统错误编号及文本),不能把所有 EAGAIN 都记成同一个原因。

栈大小由 ART 的 FixStackSize() 和线程请求共同决定,不存在适用于所有设备与架构的“每线程固定 1 MB”。诊断应从 OOME 错误文本里的栈大小、/proc/self/task 数量和 maps 中的 stack/guard 映射出发。

线程治理与监控

  • 对自有任务使用队列长度受限的 executor、结构化协程和统一调度入口,记录线程池大小、正在执行的任务数、排队任务数、拒绝次数和取消次数。
  • 审计 SDK、WebView、媒体、数据库和网络库的线程池。多个库各自“合理”的池相加后仍可能过量。
  • Dispatchers.IO 适合阻塞 I/O,但它不是全应用线程总额控制器;多个 limitedParallelism(限制并行任务数的调度器视图)也需要共享一份产品级并发预算。
  • 在线程创建点记录责任模块、名称和创建场景。优先使用自有 ThreadFactory / executor 观察;拦截 native 线程创建入口的 Hook 只用于受控诊断,需处理递归调用、性能和兼容性。
  • 周期读取 /proc/self/statusThreads,并统计 /proc/self/task。两者是采样值,线程快速创建与退出时可能不同。
  • 告警阈值来自设备和场景基线,例如场景完成并短暂稳定后的线程数分布、增长速度和剩余地址空间。固定 400 或 500 对不同进程没有通用意义。
  • 线程数超过内部告警线时不要立即调用 Thread.getAllStackTraces() 抓取所有栈;它会创建大量对象。平时保留线程名、责任模块和采样栈,超过告警线后只补充有限线程证据。

线程数下降也要看任务是否仍能完成。把线程改成一个无界队列,可能把资源 OOM 改成排队延迟或 ANR。

FD 泄漏导致的资源型崩溃

FD 是内核分配给文件、socket、pipe 等已打开资源的整数句柄。进程达到 RLIMIT_NOFILE(单进程可打开 FD 数量的软/硬限制)后,open()socket()pipe()eventfd()epoll_create1() 可能返回 EMFILE;系统级打开文件表耗尽还可能表现为 ENFILEandroid17-6.18-2026-06_r6/fs/file.c 的 FD 分配路径会按当前进程的 files table 和限制返回 -EMFILE

FD 耗尽通常不经过 ART OOME。仍应将内存、线程、FD 和地址空间放进同一套进程资源看板,同时保持事件类型分开。

FORTIFY 与真实上限

FORTIFY 是 Bionic 的运行时边界检查机制。下面的日志表示代码把过大的 FD 编号交给基于 fd_set 位集合的 select() 接口:

FORTIFY: FD_SET: file descriptor >= FD_SETSIZE

FD_SETSIZEselect() 数据结构能表示的 FD 编号上限,和 RLIMIT_NOFILE 不是同一个值。只要进程拿到的 FD 编号超过 FD_SETSIZE,FORTIFY 就可能调用 abort() 结束进程;此时 FD 总数仍可能低于进程上限。修复既要找 FD 增长源,也要检查库是否错误地用 select() 处理高编号 FD。

采集一份可解释的 FD 快照

一份有效快照至少包含:

  • getrlimit(RLIMIT_NOFILE) 的软限制(当前生效值)、硬限制(进程可把软限制提高到的最大值)和当前 FD 数。
  • /proc/self/fd/<n>readlink() 结果,也就是每个 FD 指向的资源;按 socket、pipe、anon_inode(没有普通文件路径的内核对象)、文件、设备和未知类型分组。
  • 持续增长类别的创建方、创建栈、创建时间和可用的业务 ID。
  • close() 结果、重复关闭、dup* 产生的多个 FD 指向同一底层资源的关系,以及对象生命周期。
  • 快照时的线程数、网络连接、页面或任务场景。

遍历 /proc/self/fd 本身会短暂打开目录 FD,其他线程也可能同时关闭或复制 FD,因此结果只代表采集过程附近的状态。readlink() 失败可能是资源恰好被其他线程关闭,不能直接记成泄漏。

常见泄漏点包括未关闭的 ParcelFileDescriptorCursorAssetFileDescriptorInputStream、socket、ImageMediaExtractor 和重复注册的 pipe/eventfd。Kotlin use {}、Java try-with-resources 和 C++ RAII 应覆盖成功、异常、取消与超时路径。

何时记录 native FD 创建点

只统计数量无法定位创建方时,可以在受控版本记录 open/openatsocket/acceptpipe/pipe2dup*eventfdepoll_create*close。实现要处理可变参数、同一函数的不同符号名、递归调用、采样和 FD 编号复用;漏掉 dupaccept 会让创建与关闭记录对不上。

不要在每次 open/close 时采集完整调用栈和全局映射表。先找持续增长的类别,再对目标模块采样;FD 数接近内部告警线时输出已有轻量记录,避免监控组件自己申请更多 FD 或大块内存。

虚拟内存空间耗尽(32 位进程)

32 位进程的地址空间较窄,.so 动态库、Java heap、native heap、线程栈和 mmap() 映射更容易互相挤压。Android 17 主线设备与应用以 64 位为主要形态,但兼容 32 位 ABI(Application Binary Interface,应用二进制接口)的设备仍可能运行 32 位进程;用 Process.is64Bit() 记录当前进程架构,不要只看设备 CPU。

“32 位用户空间固定为 3 GB、内核固定占 1 GB”不能作为跨设备结论。内核配置、架构、ASLR(Address Space Layout Randomization,地址空间布局随机化)、保留区和进程映射共同决定可用范围,可分配的最大连续区间还会小于剩余总地址空间。

地址空间由哪些映射构成

区域观察方式边界
ART heap spacesmaps/smaps 中的 dalvik/ART 匿名区Java heap footprint 与 growth limit 相关,但保留量和已提交量要区分
native allocator arenas(分配器管理的内存池)libc malloc 匿名映射Debug.getNativeHeapAllocatedSize() 只覆盖 allocator 统计,不等于全部映射
.so、dex、oat、vdex 等 native 库和应用/运行时代码文件带文件路径的映射同一文件可有多个权限区段,共享页不能按 VmSize 当作独占物理成本
线程栈与 guardstack/匿名映射、线程创建记录大量线程会消耗地址区间,即使栈页尚未全部驻留
Bitmap、媒体和共享内存匿名映射、memfd(以内存为后端的匿名文件)、dma-buf 或设备映射是否计入进程 RSS/PSS 取决于映射与统计方式
保留地址区间PROT_NONE(禁止读写执行的页保护)或 allocator/Runtime 保留区VmSize 会增长,但页可能尚未常驻

64 位进程也可能因无界映射、VMA 数量、异常地址保留或超大连续映射而收到 ENOMEMandroid17-6.18-2026-06_r6/mm/mmap.cdo_mmap() 会在长度、地址或 map_count(当前进程的 VMA 数量)超过系统约束等条件下返回 -ENOMEM;64 位进程仍要管理映射生命周期。

排查顺序

  1. 保存进程位数、失败 errno、请求长度、调用栈和连续性要求。
  2. 读取 /proc/self/status 的 VmSize/VmPeak(当前/历史峰值虚拟地址空间)和 /proc/self/maps 区间数量,和同设备同场景健康样本比较。
  3. 按映射名称、权限和责任模块聚合 maps/smaps,区分持续增长与单次巨型请求。
  4. 同时检查线程、Java growth limit、native allocator、文件映射和图形/媒体资源,避免只盯一个总量。
  5. 对 32 位进程检查最大空洞(尚未映射的最大连续地址区间)和碎片;总剩余地址空间足够,不代表存在满足本次请求的连续区间。
  6. 对 64 位异常 VmSize 先识别 Runtime 保留区。较大的保留区可以是正常实现,证据要回到失败栈和映射增长。

Debug.MemoryInfo.getTotalPrivateDirty() 统计进程独占、内容已被修改的驻留页,属于物理内存指标,不能判断虚拟地址空间是否接近限制。

治理方向

  • 能迁移时提供完整 64 位 ABI,并验证 64 位指针变大带来的 native heap 增量和第三方 .so 兼容。
  • 限制线程、映射和内存映射文件的数量与生命周期,及时 munmap() 或关闭资源所有者。
  • 大对象采用分块、流式或尺寸上限,避免要求巨型连续区域。
  • mallopt(M_PURGE, 0) 请求 allocator 归还可清理的空闲物理页,不能自动释放所有保留地址区间,也不能修复映射泄漏。
  • 拆进程会获得独立地址空间,却会复制 Runtime、.so、线程和缓存,增加整机内存与 IPC(进程间通信)成本。只有隔离边界和测量数据同时成立时才采用。

OOM 前降级、自动取证与资源预算

OOM 前的降级与失败处理

OOME 发生后,当前线程连创建异常对象都可能失败。观测和降级应在资源接近预算时完成;异常已经交给 UncaughtExceptionHandler 后,只保留最小记录,并沿默认处理路径结束进程。

在 OOM 前释放可重建资源

降级动作按风险排序:

  1. 对图片、页面模型、预取、媒体缓冲和离线队列设置硬上限与缓存淘汰规则,不能等待系统回调才控制。
  2. 当页面退出、UI 隐藏或任务取消时释放资源所有者;close() 和协程取消路径都要测试。
  3. 内存占用持续上升时停止非必要预取、降低目标图片尺寸、缩小并发,并保留用户当前操作所需数据。
  4. 后台阶段释放可快速重建的 UI 缓存,避免同步序列化或磁盘写入阻塞主线程。
  5. 重任务保存阶段性进度,重试时允许同一步骤安全执行多次,让进程被系统结束后能够恢复,不依赖 OOM 处理器抢救。

API 34 及以上 不再向应用发送旧的 TRIM_MEMORY_RUNNING_*TRIM_MEMORY_MODERATETRIM_MEMORY_COMPLETE。面向 Android 17 的代码聚焦 TRIM_MEMORY_UI_HIDDEN(界面已不可见)与 TRIM_MEMORY_BACKGROUND(进程已在后台、可能为释放整机内存而被终止);需要兼容旧系统时,再为旧常量保留分支。

下面的示例让项目自定义缓存根据仍会送达的两个级别缩容;回调运行在主线程,trimTo() 只能释放引用和完成有界操作。

override fun onTrimMemory(level: Int) {
    when {
        level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> {
            memoryCaches.trimTo(CacheProfile.BACKGROUND)
        }
        level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
            memoryCaches.trimTo(CacheProfile.UI_HIDDEN)
        }
    }
}

释放引用后由 Runtime 决定 GC 时机,不要在生产路径主动调用 System.gc()。需要关闭文件或 native 资源时,关闭动作必须快速;耗时清理由后台任务处理,但不能继续持有本应释放的大对象。

UncaughtExceptionHandler 与 API 37 OOM 触发器

全局 UncaughtExceptionHandler 不适合执行 Debug.getMemoryInfo()、heap dump、JSON 序列化或网络上报,这些动作会继续申请内存。若产品需要 OOM 标记,应预先准备固定大小的记录和写入量受限的路径,失败时立即放弃。

自定义处理器必须保存安装前的处理器,并对所有 Throwable 调用它。API 37 的 ProfilingTrigger.TRIGGER_TYPE_OOM 可以在 OOME crash 时生成 Java heap dump;官方明确要求自定义处理器调用默认处理器,否则自动触发器不可用。触发器仍受系统资源和频率限制,不能保证每次产生结果。已生成的结果要等应用后续启动并注册 registerForAllProfilingResults 回调后读取,不是崩溃线程上的同步恢复机制。

在可控的可选分配边界捕获 OOME,只适用于输入尺寸已知、没有共享状态写到一半、可以返回低分辨率或失败结果的操作。全局吞掉 OOME 后继续运行,进程的资源使用可能仍接近上限,其他线程也可能已失败。

sigsetjmp / siglongjmp 可以从 SIGSEGV、SIGABRT 信号处理位置跳回预先保存的 native 执行点,只适合受控的 crash 隔离实验。它不能处理 Java heap OOME,也不能修复 native allocator 失败;这种跳转会绕过正常的函数返回、锁释放、C++ 析构和 JNI 状态恢复,不应作为 OOM 降级方案。

大型 App 的内存预算管理

预算要同时覆盖 Java、native、图形/媒体缓冲、线程、FD 和地址空间;只看 Java maxMemory() 会漏掉大量进程成本。

预算维度与测量方式

维度实验室/CI 证据线上轻量指标
Java live heapHPROF、对象数、retained size、场景前后差值Runtime heap 已用量、GC 次数/时间、接近预算事件
native mallocheapprofd、Android Studio native allocationsgetNativeHeapAllocatedSize() 趋势
进程物理成本dumpsys meminfo、PSS/RSS、smaps_rollup 汇总低频 PSS/RSS 或平台允许的 MemoryInfo
图形/媒体Perfetto、dma-buf/GraphicBuffer 与媒体工具自有缓冲区数量、尺寸、格式和生命周期
线程与调度/proc/self/task、线程栈、executor 指标线程数、线程池正在执行/排队/拒绝任务数
FDrlimit、/proc/self/fd 分类、创建方FD 总数、主要类别和增长率
虚拟地址maps/smaps、映射数、最大空洞VmSize、映射数、进程位数

所有指标都要绑定场景和时间点。冷启动后固定等待 5 秒无法代表首页完成加载后的内存状态:网络、图片、延迟初始化和 GC 时机都可能不同。更可靠的采样点是“场景完成条件满足 + 短暂稳定窗口”,并记录测试数据、账户、网络、设备温度和进程冷热状态。

从产品预算到模块责任

预算制定可以按以下步骤执行:

  1. 选择低内存、主流和高配置设备组,记录 ActivityManager.getMemoryClass()getLargeMemoryClass()isLowRamDevice() 与进程位数。
  2. 为冷启动、首页完成加载后的短暂稳定窗口、重页面峰值、后台驻留和多次往返分别建立基线,至少重复多轮并报告分布。
  3. 把内存增量对应到可以修改的责任模块:缓存、图片、模型、线程池、native 句柄、图形缓冲或映射文件。证据不足时保留“进程共享”项,不强行指定模块。
  4. 模块预算同时规定场景完成后的常驻量、峰值、回落时间和失败策略。只规定“不得超过 N MB”,容易把内存使用推迟到另一个阶段。
  5. CI(Continuous Integration,持续集成)比较同设备同数据的分布与基线,门槛要考虑测量噪声、反复测量得到的波动范围和产品余量,不能固定使用“增加 5 MB 就阻断”。
  6. 发布后按设备、系统、ABI、版本和场景观察受 OOME 影响的活跃用户占比、REASON_LOW_MEMORY、PSS 高百分位、线程数与 FD 数。
  7. 发生回归时同时检查计算比例所用的用户总数和场景覆盖,避免测试样本减少后误判为优化。

模块预算之和不能直接等于进程预算:Runtime、共享库、allocator、系统组件和模块共享对象都需要单独余量。进程预算也不能挤到设备可承受上限,前后台切换、相机、WebView、媒体与系统更新都会改变整机竞争。

验证降级是否有效

每个降级动作都要验证三类结果:

  • 资源结果:目标指标是否下降,下降发生在多长时间内,是否只是从 Java 转移到 native 或磁盘。
  • 功能结果:当前操作是否有明确失败提示、低规格结果或可重试状态,进程重建后数据是否一致。
  • 性能结果:缓存缩小后是否引入启动、网络、解码、功耗或 ANR 回归。

OOM 治理达到可发布状态时,每个接近资源预算的事件都有责任模块,系统终止与 OOME 分开统计,慢设备和 32 位兼容进程都有覆盖,任何 UncaughtExceptionHandler 都保留平台默认处理路径。

Renderer 退出、回调与页面重建

应用进程 OOM 与 WebView Renderer 退出需要分开归因。Renderer 被终止后,WebView 状态、业务导航和资源释放决定能否安全恢复。

WebView 页面突然变白时,宿主 Activity 可能仍能响应,导航栏和原生按钮也都正常。若同时收到 onRenderProcessGone(),可以确认关联的 Renderer(负责网页解析、脚本执行与绘制的渲染进程)已经退出。此时旧 WebView 失效,reload()goBack()evaluateJavascript() 和 JS Bridge(JavaScript 与原生代码之间的通信接口)调用都救不回它。

标题中的 OOM 是 Out of Memory(内存不足)的缩写。本文讨论的是系统在内存压力下结束 Renderer 这一类情况;每次白屏或每个 didCrash=false 都不能直接定为 OOM。

下文把一次 onRenderProcessGone() 回调简称为 gone 事件。本文以 Android 17 / API 37 / android-17.0.0_r1 的 framework(Android 系统框架)契约为准。WebView provider(向 framework 提供 WebView 实现的可更新系统包)可能来自 Chromium,也可能由厂商定制,因此排查时还要记录设备上的 provider 包名与版本。文中涉及内核内存压力的源码名词时,以 Android common kernel(Android 公共内核源码仓库)的 android17-6.18-2026-06_r6 标签为参照;这个标签用于固定源码版本,不代表所有 Android 17 设备都运行同一内核。

先分清平台、Provider 和进程

标准 android.webkit.WebView 同时跨越两条版本线:

  • Android 平台提供 WebViewWebViewClient、Renderer 优先级和终止处理 API;
  • WebView provider 包提供 Chromium/Blink、页面合成、沙箱 Renderer 和大部分实现,并可独立于系统版本更新。

android-17.0.0_r1 只能固定公开 API 与 framework 注释,不能唯一确定设备使用的 Chromium 源码版本(revision)。复盘至少要记录 Android build(系统构建号)、provider 包名、versionNameversionCode、ABI(应用二进制接口,也就是 32/64 位和指令集)、设备型号,以及应用使用的是系统 WebView、定制 Chromium 还是第三方内核。

现代标准 WebView 可以按三个执行域理解:

  1. 宿主进程保存 WebView Java 对象、Activity/Fragment 状态、业务 Bridge,以及 provider 的 browser-side 代码;这里的 browser-side 指导航、网络、权限等浏览器控制逻辑,并不表示另有一个浏览器应用;
  2. 沙箱 Renderer 运行 Blink 排版引擎、JavaScript,以及样式计算、布局、绘制和部分页面合成工作;
  3. 宿主的 HWUI RenderThread(Android UI 硬件加速渲染线程)通过 WebView functor 接收 provider 的绘制回调。functor 是 provider 与 HWUI 之间的原生绘制桥,结果通常先合入应用窗口,再交给 SurfaceFlinger(Android 的系统画面合成服务)和 HWC(Hardware Composer,硬件合成器)。

视频、受保护内容或 provider overlay(由 provider 单独提交的叠加画面)可能增加独立的 SurfaceControl 图层。Renderer 退出后,已提交的旧帧可能短暂保留,也可能变成空白或静止画面。宿主窗口仍在,只能说明应用进程尚未退出,不能说明网页仍可交互。

onRenderProcessGone() 能证明什么

Android 8 / API 26 起,WebViewClient.onRenderProcessGone(view, detail) 是 Renderer 退出后的公开入口。Android 17 的 WebViewClient.java 给出四条硬约束:

  • 多个 WebView 可能关联同一个 Renderer;
  • 每个受影响的 WebView 都会收到回调;
  • 当前回调只清理参数中的 view,不能假设其他实例也已退出;
  • 传入的 WebView 不能继续使用,必须从 View 层级移除并清理引用。

返回值决定宿主是否继续运行:

返回值Android 17 契约工程含义
true应用声明已处理退出当前旧实例已被移除、销毁,业务开始恢复或展示原生错误页
falseRenderer 崩溃时宿主随之崩溃;Renderer 被系统结束时宿主也被结束应用没有能力安全处理,保留默认终止语义

默认实现返回 false。只有在清理动作已完成、后续代码不会再触碰旧实例时,才应返回 true。用 true 隐藏回调后仍在访问旧对象的错误,会把一次清晰的 Renderer 退出变成随机异常或长期白屏。

didCrash() 只做二分类,不能给出完整原因

RenderProcessGoneDetail.didCrash() 返回:

  • true:Renderer 被观察到发生崩溃;
  • false:Renderer 被系统结束,AOSP 注释说明最常见背景是低内存。

false 仍不足以单独证明“页面 OOM”。应用在 API 29 及以上可能主动调用 WebViewRenderProcess.terminate();设备实现和系统资源管理也会影响进程寿命。provider 更新通常会结束已经加载 WebView 的整个应用进程,这类进程重启要单独记录,不能假定一定会留下 renderer-only gone 事件。若应用会主动终止 Renderer,应在调用前生成一条短时有效的“预期终止标记”(下文简称 termination token),至少包含会话、调用时间、有效期和关联的 WebView 实例。随后到达且命中这条标记的 gone 事件,才归为预期终止。未命中时,再结合设备内存档位、前后台状态、Renderer 优先级、系统内存压力和问题页面是否反复出现来判断。

rendererPriorityAtExit() 返回退出时的最终 Renderer 优先级。一个 Renderer 可被多个 WebView 共享;WebView.java 规定,最终优先级取所有关联实例请求值中的最高值,实例销毁后不再参与计算。RenderProcessGoneDetail.java 也提醒,退出值可能高于某个单独实例请求的值。这个字段适合描述现场,不能反推出是哪一个 WebView 提高了优先级,也不能单独解释它为何被回收。

RenderProcessGoneDetail 不提供 Renderer PID(进程号)、页面内存、JavaScript 堆、Native 代码调用栈或最近网络请求。公开 API 没有精确的 Renderer 内存读数,不能用宿主进程的 PSS 冒充;PSS 的含义会在“怎样判断内存诱因”一节说明。

恢复是一段状态迁移

可靠的容器应把 Renderer gone 处理成一组有先后顺序的状态。下面的大写名称可以直接作为容器内部的状态枚举:

ACTIVE
  -> GONE_CALLBACK
  -> DETACHED_AND_DESTROYED
  -> RECOVERY_UI
  -> NEW_WEBVIEW_LOADING
  -> VISUAL_COMMITTED
  -> BUSINESS_READY

任一步失败都进入原生错误页。VISUAL_COMMITTED 对应 onPageCommitVisible():旧导航的内容不会再被绘制,响应正文已经进入 DOM,后续绘制可以出现新页面内容,但 CSS、图片等资源此时可能尚未完成。支付、编辑、登录等页面还需要 H5(运行在 WebView 中的 Web 前端页面)主动发出可校验的 BUSINESS_READY,表示业务接口和关键交互已经可用;宿主还要为这次握手设置超时。

恢复前要回答三个问题:

  1. 当前页面能否安全重放?
  2. 是否已有一次自动恢复尝试?
  3. Activity 是否仍处于可展示状态?

资讯详情、帮助页等幂等页面(重复加载不会额外改变业务结果)可以自动恢复。支付提交、表单编辑、身份验证和一次性 URL 应跳转到服务端状态查询或原生错误页,不能原样重放。原始 URL 可能含登录凭证、订单号和查询参数,上报与恢复检查点都要按业务规则去掉敏感信息。

一个可控的 Activity 恢复骨架

下面的示例展示单 WebView Activity 的最小恢复状态。WebCheckpoint 是业务自定义的恢复检查点,只保存去敏后的安全重载地址、页面类型和是否允许自动重载。业务方法留作接口,重点是旧实例清理、延后重建、自动恢复次数上限和页面可用信号:

class H5Activity : AppCompatActivity() {
    private lateinit var container: ViewGroup
    private var webView: WebView? = null
    private var checkpoint: WebCheckpoint? = null
    private var autoRecoveryUsed = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.h5_activity)
        container = findViewById(R.id.webview_container)

        checkpoint = restoreCheckpoint(savedInstanceState)
        autoRecoveryUsed =
            savedInstanceState?.getBoolean("auto_recovery_used") == true
        installWebView(checkpoint)
    }

    private fun installWebView(target: WebCheckpoint?) {
        check(webView == null)

        val next = WebView(this)
        configureWebView(next)
        next.webViewClient = object : WebViewClient() {
            override fun onPageStarted(
                view: WebView,
                url: String,
                favicon: Bitmap?,
            ) {
                checkpointFor(url)?.let { checkpoint = it }
            }

            override fun onPageCommitVisible(view: WebView, url: String) {
                if (view === webView) {
                    reportVisualCommit(checkpoint)
                }
            }

            override fun onRenderProcessGone(
                view: WebView,
                detail: RenderProcessGoneDetail,
            ): Boolean {
                val targetAtExit = checkpoint
                val didCrash = detail.didCrash()
                val priorityAtExit = detail.rendererPriorityAtExit()

                detachAndDestroy(view)
                reportRendererGoneSafely(
                    didCrash = didCrash,
                    priorityAtExit = priorityAtExit,
                    target = targetAtExit,
                )
                showRecoveringUi()

                container.post {
                    if (!isFinishing && !isDestroyed) {
                        recoverAfterRendererExit(targetAtExit, didCrash)
                    }
                }
                return true
            }
        }

        webView = next
        container.addView(
            next,
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT,
        )

        if (target == null) {
            next.loadUrl(homeUrl())
        } else {
            next.loadUrl(target.safeReloadUrl)
        }
    }

    private fun detachAndDestroy(target: WebView) {
        (target.parent as? ViewGroup)?.removeView(target)
        if (webView === target) {
            webView = null
        }
        target.destroy()
    }

    private fun recoverAfterRendererExit(
        target: WebCheckpoint?,
        didCrash: Boolean,
    ) {
        val mayAutoReload =
            !didCrash &&
                !autoRecoveryUsed &&
                target?.autoReloadAllowed == true

        if (mayAutoReload) {
            autoRecoveryUsed = true
            installWebView(target)
        } else {
            showFallbackPage(target)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        saveCheckpoint(outState, checkpoint)
        outState.putBoolean("auto_recovery_used", autoRecoveryUsed)
        super.onSaveInstanceState(outState)
    }

    override fun onDestroy() {
        webView?.let(::detachAndDestroy)
        super.onDestroy()
    }
}

旧实例按官方要求从视图树移除、清除引用并调用 destroy(),之后不再设置 client、停止加载或执行 JavaScript。代码先把 RenderProcessGoneDetail 中需要的字段复制成普通值,避免延后任务继续持有回调对象;事件上报函数也必须自行处理异常,不能让异常从回调中抛出。重建通过 container.post 放入主线程消息队列,等回调返回后再执行,避免尚未退出回调又开始创建 WebView。任务执行前还会检查 Activity 是否正在结束或已经销毁。自动恢复次数写入 savedInstanceState(Activity 重建时由系统传回的状态 Bundle),避免屏幕旋转等配置变更把次数重新置零。

示例对崩溃采取保守策略:同一页面可能稳定复现 Chromium 或页面内容触发的问题,因此不自动重载。被系统结束的页面也只有一次自动恢复机会。WebCheckpoint 应保存去敏后的业务路由和重放规则,不应序列化完整浏览历史、POST 数据或 JavaScript 运行时状态。

configureWebView() 要统一安全与功能配置,包括允许访问的 origin(协议、主机名和端口三者组成的来源)、Safe Browsing、Cookie 策略、文件访问、混合内容、WebChromeClient 和 JS Bridge。只有可信页面需要的 Bridge 才能重新注册。若每个业务页面都直接创建 WebView,恢复出的实例很容易漏掉安全配置。

多 WebView 容器

共享 Renderer 时,同一次退出会触发多个回调。每个回调都只销毁参数中的 view 并返回 true;容器管理器再按页面栈决定统一展示错误页,或等待所有实例完成清理。不能在第一个回调中假设其余 WebView 已失效,也不能漏掉后台、缓存池或 ViewHolder 中的实例。

引用清理至少覆盖:

  • Activity、Fragment 与自定义 View 字段;
  • Adapter、ViewHolder 和页面栈;
  • WebView 缓存池;
  • JS Bridge、ValueCallback、下载与文件选择回调;
  • 延迟 Runnable、协程和业务观察者。

回调完成后仍持有旧 WebView 的任务要被取消。另一种做法是给每次新建的实例分配递增代号,任务执行前比对代号,不匹配就放弃,避免旧任务操作新实例。

Renderer 优先级与低内存策略

WebView.setRendererPriorityPolicy(priority, waivedWhenNotVisible) 只影响多进程模式下 Renderer 在系统内存不足时被选为回收对象的可能性,不会限制它最多能用多少内存。Android 17 定义三个级别:

级别含义
RENDERER_PRIORITY_IMPORTANT默认值,Renderer 绑定优先级与宿主主进程相近
RENDERER_PRIORITY_BOUND中等,内存紧张时更容易成为回收目标
RENDERER_PRIORITY_WAIVED最低,系统低内存时会更积极回收

下面的设置请求可见时使用 BOUND,不可见时按 WAIVED 处理:

webView.setRendererPriorityPolicy(
    WebView.RENDERER_PRIORITY_BOUND,
    true,
)

降低优先级只会让系统在内存紧张时更容易选中 Renderer,不会立即释放页面内存。只有当容器能处理全部关联 WebView 的 onRenderProcessGone()、能够识别不可重放页面,并且允许后台页面重建时,才适合调整这项策略。可见交易页通常不应只为节省内存就降低优先级。

多个 WebView 共享 Renderer 时,最终优先级取关联实例请求值的最大值;销毁某个 WebView 后,它不再参与计算。只修改一个后台实例,未必改变共享 Renderer 的最终优先级。

怎样判断内存诱因

WebView 页面资源分布在多个域:

  • Renderer:DOM(页面的文档对象树)、JavaScript 堆、Blink 对象、已解码图片和部分光栅化资源;
  • 宿主/provider:导航与网络等 browser-side 状态、Bridge、缓存与 WebView 对象;
  • GPU/图形:纹理、分块渲染数据、图形缓冲区与驱动分配;
  • 系统:其他进程的竞争、zram 压缩交换空间、内存回收活动和设备内存档位。

宿主的 Java 堆处于正常范围,不能排除 Renderer 或图形内存压力;Renderer 被系统结束,也不能反向证明某个 JavaScript 对象泄漏。

一次有效的 gone 事件至少记录:

维度字段
页面去敏后的业务路由、H5 构建版本、页面类型、停留时长、是否允许重复加载
容器WebView 数量、是否来自缓存池、可见性、自动恢复次数
退出didCrashrendererPriorityAtExit、预期 termination token 是否命中
Provider包名、versionNameversionCode、系统 WebView / 定制 Chromium / 第三方内核
宿主进程前后台、PSS/RSS、最近 onTrimMemory()、进程存活时长
设备Android build、API、ABI、内存档位、设备型号
恢复销毁完成、新实例创建、视觉提交、H5 就绪、进入原生错误页的原因

PSS(Proportional Set Size)把共享内存按比例计入进程,适合估算进程对物理内存的贡献;RSS(Resident Set Size)会把当前驻留的共享页全部计入,跨进程相加可能重复。这里记录二者是为了补充宿主现场,不能把它们当成 Renderer 的内存值。

在 Android 8 及以上,WebView.getCurrentWebViewPackage() 可在 WebView 加载前后调用。已经加载时,它返回当前进程实际使用的 provider;尚未加载时,它返回“此刻加载将会使用”的 provider,这个结果随后可能过期。设备不支持 WebView 或配置异常时,返回值也可能为 null。AndroidX WebKit 环境可使用对应的兼容 API。provider versionName 常能帮助定位 Chromium 版本,但厂商格式不统一;从版本号解析 milestone(Chromium 的主版本代号,如 M140)只能作为针对特定 provider 的逻辑。

实验室可用 Perfetto(系统跟踪工具)、bugreport(系统诊断包)、Chrome DevTools(网页调试工具)和 provider 对应的 Chromium 符号文件调查 Renderer、GPU 与系统内存压力;符号文件用于把原生地址还原成函数名和调用栈。查看内核证据时,reclaim 表示内存回收活动,PSI(Pressure Stall Information)memory 表示任务因内存压力停顿的时间比例,page fault 表示缺页事件,dma-buf 则常用于追踪跨进程共享的图形缓冲区;zram 的含义见上文。若要与本文源码对应,应固定到 android17-6.18-2026-06_r6。线上公开 API 通常拿不到 Renderer 的精确 PSS,不要依赖反射、读取其他进程 /proc 或私有 Chromium 接口。

ApplicationExitInfo 记录的是应用进程的退出信息,适合补充宿主进程为何结束。Renderer 被回收而宿主继续运行时,不一定会产生应用可查询的对应记录;它不能替代 onRenderProcessGone() 事件,也不能在没有时间和进程证据时与某次 Renderer gone 一一配对。

WebViewRenderProcessClient:发现卡死,谨慎终止

Android 10 / API 29 起,WebViewRenderProcessClient 提供:

  • onRenderProcessUnresponsive():Renderer 因长阻塞任务等原因无法及时处理输入或导航;
  • onRenderProcessResponsive():同一 Renderer 恢复响应后回调一次。

Android 17 源码说明,无响应期间会重复回调,相邻回调最短间隔为 5 秒;WebView 不会自动采取动作。一次 unresponsive 回调只说明 Renderer 没有及时处理输入或导航,不等于应用发生 ANR(Application Not Responding,应用无响应),也不等于 Renderer 必须被结束。应记录持续时间、页面阶段、用户是否可退出,以及 H5 是否正在执行预期的耗时任务。

当产品允许用户放弃当前页面,且所有共享 WebView 都已实现终止处理时,才考虑调用 renderer?.terminate()。调用前要:

  1. 写入前文定义的短时 termination token;
  2. 切断新的 JS 注入和导航;
  3. 展示不会依赖旧 WebView 的原生 UI;
  4. 限制一次会话中的终止次数;
  5. 等待随后的 onRenderProcessGone() 完成清理。

WebViewRenderProcess 是不透明句柄:调用方可以请求终止,却不能从对象中取得稳定的 PID 等实现细节。回调参数在单进程模式下还可能为 nullterminate() 返回 true 只表示当前可以终止这个 Renderer,返回 false 表示无法终止;返回值不表示 gone 回调已经到达,更不表示新页面已经恢复。

指标要覆盖发生、清理和可用

应用崩溃率看不到那些已返回 true 的 Renderer gone。建议按以下状态记录一次恢复过程:

状态判定
gone_received回调到达,按崩溃、系统结束、预期终止分类
old_view_destroyed参数 View 已移除、引用已释放并调用 destroy()
new_view_created尚未达到自动恢复次数上限,且新实例已加入容器
visual_committed新实例收到 onPageCommitVisible()
business_readyH5 业务握手完成,关键接口可用
fallback_shown进入原生错误页,并记录原因
repeat_gone同一会话或短窗口内再次退出

核心指标必须有分母:

  • Renderer gone 用户率、会话率和页面访问率;
  • 崩溃、系统结束、预期终止的占比;
  • 从 gone 到视觉提交、业务就绪的成功率和耗时分布;
  • 自动恢复后的 repeat gone 率;
  • 不可重放页面进入原生错误页的比例与业务完成率;
  • 按 provider 版本、业务路由、设备内存档位和前后台状态分组后的差异。

“新 WebView 已创建”不算恢复成功,onPageFinished() 也不能保证用户已经看到可操作内容。视觉提交加业务握手更接近用户体验;两者都需要超时,并比对前文所说的实例代号,防止旧回调把新实例误报为成功。这个组合仍是应用侧的间接信号,不能证明某一帧已经由 SurfaceFlinger 提交到屏幕。需要逐帧诊断时,再检查宿主窗口的 FrameTimeline(帧从应用到显示系统的时间线)和实际显示时间。

分批启用、紧急停用和自动恢复次数

远程配置应位于容器层,常用控制项包括:

  • 是否允许 killed 场景自动恢复;
  • 哪些业务路由只展示原生错误页;
  • 每个会话的自动恢复次数;
  • 是否启用 WebView 缓存池;
  • 后台 WebView 是否请求较低 Renderer 优先级;
  • 哪些 provider 版本暂停自动重载。

远程开关不能依赖已经退出的 WebView 拉取。配置要在进入 H5 容器前缓存,并为离线状态准备保守默认值。

自动重载或后台降优先级应先对少量设备和用户启用,观察 gone 率、重复退出率和业务完成率后再扩大范围。异常上升时,远程配置应能立即停用对应策略。

若某个 provider 版本集中发生 Renderer 崩溃,应立即停用同一页面的自动重载,并保留 provider、H5 构建版本和去敏后的业务路由。provider 是独立更新组件,同一 Android 版本可能出现不同结果;统计时必须单独按 provider 版本分组。

故障演练

测试要覆盖不同退出方式,以及回调与 Activity 生命周期同时变化时可能出现的先后顺序问题:

用例需要确认
chrome://crash崩溃被识别,所有共享实例清理,宿主不退出
WebViewRenderProcess.terminate()预期 token 命中,gone 回调完成,终止次数受限
内存压力下 Renderer 被回收系统结束分支、后台延迟恢复、前台重建策略
两个 WebView 共享 Renderer每个受影响实例都返回 true,没有漏清理
回调后 Activity 立即销毁container.post 排队的恢复任务不会创建泄漏实例
敏感页面退出不重放 POST、一次性 URL 或未确认交易
新页面再次 gone自动恢复次数上限生效并进入原生错误页
provider 更新或切换进程重启后记录的新版本正确

chrome://crash 只能放在调试或测试工具中,并且可能影响共享 Renderer 的多个 WebView。它验证崩溃处理,不能代替低内存回收。内存压力测试应在可控设备上结合 Perfetto 或 bugreport 确认系统背景,不能用一次手工白屏证明 OOM。

API 26 以下没有 onRenderProcessGone()。若产品仍支持更低版本,需要单独定义进程级隔离、原生错误页和 provider 升级策略;不要把 API 26 的恢复契约套到旧系统。

源码与官方资料

常见误判

误判更严谨的结论
白屏就是 Renderer OOM只有 gone 回调能确认 Renderer 已退出;网络、HTTP、SSL、页面脚本和绘制停滞也会白屏
didCrash=false 就是页面 OOM它表示 Renderer 并非因崩溃退出;还要排除应用主动终止,并补系统内存压力证据
返回 true 就完成恢复旧实例清理、新实例重建、视觉提交和业务就绪都要验证
在旧实例上 reload() 能救回页面gone 后旧 WebView 不可复用,只能移除、销毁和新建
恢复时原样加载完整 URLURL 可能敏感或不可重放,应使用去敏后的业务恢复检查点
一个 WebView 收到回调就能批量销毁当前回调只处理参数实例,共享 Renderer 会逐个通知
宿主 PSS 就是 Renderer 内存两者属于不同进程/执行域,公开 API 没有精确 Renderer PSS
Android 17 决定 Chromium 行为还要记录设备 provider 包版本与对应的 Chromium 源码版本
Renderer 准备好一帧就代表恢复可用还需确认宿主能够绘制、收到视觉提交回调并完成业务握手

Renderer 恢复小结

Renderer gone 的恢复原则可以压缩成五句话:

  1. Android 17 固定 framework 契约,设备 provider 版本用于定位具体 Chromium 实现;
  2. 回调中的旧 WebView 只能移除、销毁和清引用;
  3. 每个受影响实例都要处理,共享 Renderer 不能漏;
  4. 自动恢复必须受页面可否重复加载、Activity 状态和次数上限约束;
  5. 视觉提交与业务就绪都成功,才能计为用户已恢复。

把 gone 事件、旧实例清理、新实例状态和 provider 版本放在同一条事件记录中,才能区分系统回收、Renderer 崩溃、主动终止和恢复代码自身的缺陷。

小结

OOM 治理先要区分 Java Heap、Native Heap、线程、FD、虚拟地址空间和 LMK 等失败边界,在资源趋势接近预算时由明确 owner 降级或释放,而不是在 OOME 已发生后做高开销抢救。WebView Renderer 退出则是另一个进程边界:宿主要销毁旧 WebView、受控重建并用视觉与业务就绪共同验证恢复。

参考资料