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

ApplicationExitInfo 与版本化线上诊断

ApplicationExitInfo 是 Android 保存的近期进程退出记录,带有系统对退出事件的分类和现场信息。进程可能来不及执行崩溃采集 SDK 的回调,但系统一侧仍有记录可查:Android 会保存退出原因(reason)、退出状态(status)、进程重要性(importance)、时间、最近一次内存采样和部分 trace。Android 11 / API 30 起,应用可以在新进程启动后查询这些记录,用来分类崩溃、ANR、低内存终止、用户操作和包状态变化。

源码基线:AOSP android-17.0.0_r1,Android 17 / API 37;相关内容集中在框架和应用接口,不涉及特定 Linux 内核版本特有的机制,因此不记录内核标签。

这套记录有自己的局限:历史容量有限,字段也可能缺失。它需要与崩溃采集 SDK、ANR 监控和原生符号化配合使用——符号化负责把程序地址还原为函数或源码位置;单靠一条退出记录,说明不了某段业务代码就是根因。

本章前半部分讲 ApplicationExitInfo 这类被动退出记录,以及它和崩溃采集 SDK、ANR 监控、原生符号化的配合方式;后半部分讲 Android 15 之后 ProfilingManager 与 ProfilingTrigger 提供的主动与触发式采集。两部分能力都随 API、flag、系统实现和用户设置变化。

退出原因、状态、Trace 与访问范围

进程退出归因的观测目标

线上稳定性排查通常同时跑着几路采集:Java 崩溃处理器记录未捕获异常,原生崩溃 SDK 记录操作系统信号(signal)和自己生成的现场,ANR 监控记录主线程状态与系统超时,ApplicationExitInfo 则在进程结束后补上系统一侧的记录。我们要先把各来源的原始事件保存下来,再依据进程、时间、构建和现场特征归并;直接相加会把同一次故障重复计数。

一条退出记录至少要回答六个问题:

问题字段或来源用途
哪个进程退出查询包名、processName、pid,以及 package UID、real UID、defining UID区分主进程、独立进程和外部服务进程;UID 是 Linux 用来区分应用身份与权限的用户标识
系统怎样分类reason、status、description区分 Java 崩溃、原生崩溃、ANR、低内存和用户或包状态变化
退出前的重要性importance 与应用自己保存的前后台场景估计用户影响;importance 不等同某个页面的生命周期
何时退出timestamp、构建与配置版本、应用本地事件关联发布、配置变更和用户反馈
可用的内存证据pss、rss 与应用最近一次资源摘要PSS 是按比例分摊共享页后的物理内存,RSS 是包含共享页的驻留物理内存;两者都是最近一次系统采样,可能为 0
是否有系统附件traceInputStream、Android 17 的 AnrInfo补充 ANR 跟踪记录或 tombstone(系统生成的原生崩溃现场文件);附件和结构化信息都可能缺失

表里的UID是Linux身份,PID是进程标识。package UID是安装时分配给包的身份,并不意味着多个包必然共享一个身份,real UID 是进程实际运行使用的内核 UID,隔离进程里两者可能不同;defining UID 则在外部服务使用应用 Zygote(预先加载运行环境并派生应用进程的模板进程)时标识服务提供方。普通单进程应用里这些值常常相同,到了多进程和外部服务场景就要分别保留。

公开 ApplicationExitInfo 没有 getSubReason():AOSP 服务内部的细分原因(subreason)不在第三方应用可读取的 SDK 合约里,普通应用的数据结构把它设成必填字段就会落空。description 只供人阅读,它在不同设备、不同版本上的格式都没有稳定保证,想靠解析这段字符串恢复内部subreason,拿不到稳定结果。

归因之外,我们还要把事件分类和产品严重度分开:REASON_CRASH、REASON_CRASH_NATIVE 和 REASON_ANR 通常可直接进入内部稳定性事件流;REASON_LOW_MEMORY 要结合 importance 和场景判断;用户、包更新、权限与多用户状态变化通常作为时间线背景。计算指标前必须跨来源去重,并明确分子、分母和“用户可感知”的定义。

API 30+ 的 ApplicationExitInfo

Android 11 引入 ActivityManager.getHistoricalProcessExitReasons(packageName, pid, maxNum)。结果按时间从新到旧排列:

  • packageName = null 匹配调用者 UID 下的包;查询其他 UID 的包需要系统级 DUMP 权限,这条路普通第三方应用走不通。
  • pid = 0 表示不按 PID 过滤。PID 会被系统复用,单拿它当跨进程启动周期的事件 ID 并不可靠。
  • maxNum = 0 表示返回当前仍保留的全部匹配记录,历史范围仍受系统容量限制。
  • 通过 BIND_EXTERNAL_SERVICE 绑定的外部服务进程也可能出现在调用包的退出记录中,归因时要保留进程名与 UID 字段。

公开文档只承诺历史信息放在环形缓冲区里,容量满后淘汰旧记录。AOSP android-17.0.0_r1 的默认资源 config_app_exit_info_history_list_size 是每包 16 条,设备厂商可以覆盖这个资源值,它同样不在 SDK 合约里。我们再往下看实现:该标签下 AppExitInfoContainer 用 ArrayList<ApplicationExitInfo> 保存记录,容量超限时删掉时间最早的记录,同一 PID 可以保留多次退出。所以“以 PID 为键、同一 PID 必然覆盖”只是旧实现的结论,放到这个标签上并不成立。

这段代码只读取轻量元数据,trace 也不会在调用线程里复制,但调用仍要放到应用自己的 executor 里。maxNum = 0 读的是系统当前保留的记录,本地按版本化来源指纹持久化与upsert,并重扫重叠窗口;摘要已ACK不应阻止之后补到的附件更新。不能只按严格递增wall-clock时间戳跳过已处理记录,系统也没有公开唯一事件ID。

import android.app.ActivityManager
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi

data class ExitSummary(
    val processName: String?, val pid: Int,
    val packageUid: Int, val realUid: Int, val definingUid: Int,
    val reason: Int, val status: Int, val importance: Int,
    val timestampMs: Long, val pssKb: Long, val rssKb: Long,
    val description: String?, val processStateSummary: ByteArray?,
    val anrId: Int?, val anrType: Int?, val anrTimeoutMs: Long?,
    val anrUserPerceptible: Boolean?
)

@RequiresApi(Build.VERSION_CODES.R)
fun readExitSummaries(context: Context, maxNum: Int = 0): List<ExitSummary> {
    require(maxNum >= 0)
    val am = requireNotNull(context.getSystemService(ActivityManager::class.java))
    return am.getHistoricalProcessExitReasons(context.packageName, 0, maxNum)
        .map { info ->
            val anr = if (Build.VERSION.SDK_INT >= 37) info.anrInfo else null
            ExitSummary(
                processName = info.processName, pid = info.pid,
                packageUid = info.packageUid, realUid = info.realUid,
                definingUid = info.definingUid,
                reason = info.reason, status = info.status,
                importance = info.importance, timestampMs = info.timestamp,
                pssKb = info.pss, rssKb = info.rss,
                description = info.description,
                processStateSummary = info.processStateSummary?.clone(),
                anrId = anr?.anrId, anrType = anr?.anrType,
                anrTimeoutMs = anr?.timeoutMillis,
                anrUserPerceptible = anr?.isUserPerceptible
            )
        }
}

ExitSummary是本例自定义DTO。调用方先做API30+分支,再在受控后台executor里处理服务缺失、权限/远端异常,并记录query_failed;不要把失败转为空列表。该函数只读取元数据,不负责持久队列或去重。description 只用于受控明细,稳定分组不要依赖它;processStateSummary 与 ANR 字段都可能为 null。查询要经 Binder 跨进程调用,批量返回的对象也会占用内存,所以别把它放进首帧关键路径同步执行。

reason 是分类主字段,status 怎么解释取决于 reason:进程调用 exit() 时,status 保存退出码;进程因操作系统信号结束时,保存的是信号编号。业务协议应保存 SDK 枚举值与采集设备的 API 级别,不要自行复制 reason 的整数常量。

部分设备不支持 LMK 分类上报,应先调用 ActivityManager.isLowMemoryKillReportSupported() 探测。在不支持的设备上,内存压力终止可能表现为 REASON_SIGNALED,且 status 为 SIGKILL。即使设备支持,REASON_LOW_MEMORY 也只说明系统当时存在内存压力,单凭它认定应用内存泄漏,依据并不充分。

importance 记录进程死亡前的系统重要性级别,能区分前台、前台服务、可见、服务、缓存等进程状态;至于当时停在哪个页面,它恢复不了。所以应用仍要自己保存最近场景、最近一次 Activity resume、首帧是否完成和升级迁移状态。

PSS/RSS 是系统最近一次采样,进程在采样前死亡时可能为 0。服务端应把 0 标记为“不可用”,既不要插值,也别把它当作进程死亡时刻的精确内存。另一个容易出错的地方是 timestamp:它来自可被校时调整的墙上时钟(wall clock),而应用性能事件的时间轴常用只向前递增的单调时钟(elapsed time);关联这两类时间戳时,先确认同一设备、同一次开机区间,再用附近记录的wall/elapsed校准点换算,直接相减会出错。

ActivityManager.setProcessStateSummary() 可保存最多 128 字节的应用状态,系统可能限制高频调用并抛出 RuntimeException。这块空间放的是不含个人数据的场景码、实验或配置版本和短状态位;账号、令牌、自由文本,以及恢复界面所需的状态,都要另找去处。更新只在状态变化时进行,实验归属以服务端实验分组记录(assignment)为准。

ANR 与原生崩溃的现场关联

getTraceInputStream() 从 API 30 开始存在,常见内容是系统 ANR 跟踪记录;Android 12 / API 31 起,reason 为 REASON_CRASH_NATIVE 时还可以返回符合 AOSP tombstone.proto 的 protobuf——一种二进制结构化数据格式。ANR 文本与 tombstone protobuf 是两种格式,我们解析前先分清拿到的是哪一种:文件扩展名或“流非空”都定不了内容类型。

trace 附件还有三个容易误判的点:

  • trace 保存在独立的系统级环形缓冲区,可能被包括其他应用在内的新记录覆盖,因此返回 null 属于允许的结果。
  • 应用发生 ANR 后恢复,又因其他原因退出时,之前的 ANR trace 可能附在后一次退出记录上。实际退出原因与附件类型必须分别保存。
  • 读取流伴随 I/O;应用应在后台按字节上限复制到私有临时文件,校验写入成功后再发布为待上传附件。超过配额、解析失败和缺失都要作为明确状态上报。

ANR 事件可以按以下证据关联:

  1. 应用轻量监控记录主线程停顿、页面、操作,以及同一单调时钟上的 I/O 和网络摘要。
  2. 后续进程读取历史退出记录,保留 reason、进程名、timestamp、importance 和 trace 是否存在。
  3. 服务端在允许的时间误差内,用进程、构建、场景和堆栈特征寻找同一事件的候选来源;匹配不上唯一来源时保留多条事件,不强行合并。
  4. 取得系统 trace 后再分析锁等待、Binder、I/O、调度、GC 与消息执行。一次堆栈采样本身还定不了根因。

Android 17 / API 37 的 getAnrInfo() 还提供 ANR ID、类型、系统等待的超时时长和 isUserPerceptible()——isUserPerceptible() 表示系统是否向用户显示了 ANR 对话框。AnrInfo 只在 reason 为 REASON_ANR 时才可能存在,旧版本靠解析 description 构造不出等价字段。ANR ID 只保证在同一种 ANR 类型内唯一,跨类型或跨数据源去重仍要带上类型、进程和时间。

API 37 还增加 ActivityManager.registerAnrWarningListener():应用接近 ANR 超时时它会尽力回调,执行回调的 executor 要避开主线程。这个回调可能不来,也可能来了却来不及做完工作,所以只适合写入预分配好的轻量摘要或触发受控采集,拿它当 ANR 覆盖率保证是不行的。

原生崩溃事件有两类独立的原始来源:一类是 Crashpad、Breakpad 或自研 SDK 生成的小型转储(minidump)和事件封装 envelope,另一类是 REASON_CRASH_NATIVE 对应的 tombstone。服务端用构建 ID、ABI、操作系统信号、故障地址(fault address)、崩溃线程和符号化后的栈顶帧匹配候选。两边互为补充:SDK 现场缺失时,tombstone 可以补上系统一侧的现场;tombstone 缺失时,SDK 事件照样保留——某个附件缺席,不该连带删掉事故记录。

Play Console、Crashlytics 和自研 APM 在安装来源、采样、去重和用户统计口径上各不相同,ApplicationExitInfo 则是系统自己的近期进程记录。同一次崩溃或 ANR 可能同时出现在多个来源里,统一报表必须先归并事件,并保留来源事件 ID 列表(source_event_ids)与归并置信度。

Android 11 以下的替代路径

API 29 及以下没有公开的历史退出原因 API。应用可以保存退出前状态并在下次启动生成“异常退出候选”,但光是这些状态,还区分不了 SIGKILL、低内存终止、系统重启、厂商进程管理和用户移除任务。

低版本可用路径如下:

目标低版本方案边界
Java 崩溃Thread.UncaughtExceptionHandler 写入最小事件封装,并继续调用原处理器内存不足(OOM)、磁盘故障或进程快速终止时可能写不完
原生崩溃用 sigaction 注册信号处理器,准备备用信号栈和预留资源,或采用经验证的 Crashpad/Breakpad信号处理器只能调用异步信号安全(async-signal-safe,即允许在信号处理器中安全调用)的函数;还要正确转交给已有处理器
ANR主线程 Looper(消息循环)的消息耗时、线程采样、用户明确协助生成的系统诊断包(bug report)应用自建看门狗(watchdog)只能观察卡顿,不代表系统已经判定 ANR;普通应用无权读取 /data/anr/
低内存或系统终止启动标记、前后台状态、周期性内存摘要SIGKILL 不可捕获,结果只能标为候选原因
Java 堆诊断调试环境使用公开堆转储;生产环境只在严格采样和授权下采用经验证方案HPROF(Java 堆转储文件)体积大且敏感,会增加内存、I/O 和存储压力

低版本方案都依赖提前保存的轻量状态:应用启动时写入带设备启动周期和应用会话标识的 launch_started,业务达到可用状态后写入 launch_finished,Java 崩溃处理器只追加最小 crash_pending。下次启动发现未完成标记时,先生成“上次进程未完成启动”的候选,再结合设备是否重启、应用是否升级、最近前后台状态与内存摘要分类。

系统或应用崩溃可能发生在标记落盘中途,状态文件需要原子替换、版本号和校验。标记还会受清除数据、备份恢复和时钟变化影响;单凭一次残留标记就归为 LMK,是站不住的。

本轮固定核对KOOM 2.2.2(commit fe5f37e188a2a86dd477d7c3c9bc2b79e70bb7d5)的一类fork-dump方案会先暂停 Android Runtime(ART),通过 fork 创建子进程,再由子进程生成 HPROF。它依赖 ART 私有符号、动态链接和特定版本兼容代码,属于 SDK 之外的私有实现。项目若采用此类方案,要把支持版本、厂商 ROM 验证、失败回退、隐私、磁盘峰值和停用开关列为准入条件;正式发布构建别为了兼容旧版本而默认启用私有 ART 依赖。

API 30+ 上轻量状态机仍值得保留,用来提供业务场景和发现系统记录缺失,但 reason 以公开系统字段为主;低版本状态机只能输出带置信度的候选类别。

KOOM 2.2.2:fork dump 的父子职责和私有依赖

图:按固定commit画出fork后两分支:父进程恢复ART并等待,子进程转储后退出。调用者仍会等待结果,暂停和fork也有成本。

低版本证据等级与统一事件模型

Android 5–10 的退出推断,我们要把结论、置信度和原始证据分开保存。一个未闭合的会话标记只说明上次进程没有完成预期写入,至于 SIGKILL、LMK、设备重启、用户停止、包更新还是写盘失败,它区分不了。可以按下面的证据等级来分级:

应用结论最低证据对外口径
JAVA_CRASH_CONFIRMED完整未捕获异常记录可对应上一会话应用确认的 Java 崩溃
NATIVE_CRASH_CONFIRMED完整 minidump 或经校验的信号报告应用确认的原生崩溃
ANR_CONFIRMED_EXTERNALPlay、bugreport 或厂商系统 trace 可与会话匹配外部平台确认的 ANR
ANR_SUSPECTEDwatchdog 连续保存主线程与调度现场应用观测到卡死,不等于系统 ANR
LOW_MEMORY_SUSPECTED会话未闭合且退出前内存、线程、文件描述符(FD)或堆证据异常疑似内存压力,不写成低内存终止守护进程(LMKD)已确认
ABNORMAL_END_UNKNOWN只有未闭合标记,或多类证据冲突原因未知

统一数据域中,system_reason_code 只接收 API 30+ ApplicationExitInfo 的公开 reason;legacy_reason 只接收应用规则结论;reason_source 区分 android_system、external_platform、app_confirmed 和 app_inferred。

coalesce()(返回第一个非空值的数据库函数) 把两类原因合成一个字段时,会丢掉证据来源。事件还应保留 session_id、process_name、设备启动周期、前一进程启动的单调时间、发现旧会话的时间、confidence、原始证据数组 evidence[]、附件引用、采集器与规则版本。

低版本没有可靠的退出时间,下一次启动时间只表示何时发现旧会话未闭合;PID 还会复用。关联时用会话、进程、构建、设备启动周期和证据指纹(evidence fingerprint,由多项稳定特征生成的匹配键)。外部 ANR 或崩溃记录匹配不上唯一来源时,保留候选及分数,别为了凑一个统一事件而丢掉来源关系。

调试环境里,/data/anr/、系统事件日志、dumpsys(导出系统服务状态的诊断命令)和 LMKD 记录都可以补充证据,但普通发布包把它们当稳定的生产接口是行不通的。KOOM 一类 fork-dump 方案只能在进程仍有执行机会时保存 Java 堆现场,并依赖 ART 私有实现;这类记录可以进入 evidence[],但疑似内存压力要升格为系统确认,还差一步。所有 minidump、HPROF、内存映射(maps)、线程栈和日志附件都要分别控制采样、存储、加密、访问和删除。

端侧存储与上报设计

退出历史容量有限,应用要在后续活跃进程里及时查询,但查询和附件 I/O 都别进入首帧关键路径。应用可交互后,由后台 executor 读取轻量元数据,再交给受系统调度约束的任务处理 trace 与上传。

下面的流程图里,我们把元数据与大附件分开处理:trace 复制失败不挡退出摘要进队列;上传成功后仍保留来源键(key),避免下一次启动重复计数。图中的 upsert 表示“来源指纹已存在则更新,不存在则插入”。

退出元数据按来源指纹upsert后独立排队;符合采样和字节预算时再读取、校验并原子发布附件。摘要和附件各自ACK,服务端保存来源关系后再跨来源关联。

摘要先入队,后补附件按来源身份独立更新

图:摘要持久化、附件生成、上传与跨来源归并分别记状态;已见过元数据不代表之后无需检查新附件。

系统稍后可能为已有记录补上 ANR trace,所以这里需要 upsert。若本地只保存“已看过 timestamp”并永久跳过该记录,就可能漏掉后来出现的附件。

ExitEnvelope 是项目自定义的退出事件封装,可以使用以下公开字段:

字段说明
source_fingerprint查询包名、进程名、包 UID、PID、timestamp、reason、status 的稳定编码,用于同来源 upsert
sourceapplication_exit_info、crash_sdk、watchdog、manual_marker 等
reason / status / descriptionreason 和 status 原样保存;description 只作为受控明细
importance / foreground_state一起保存,服务端决定严重等级
pss_kb / rss_kb / memory_sample_available明确单位,并区分 0、缺失与有效系统采样
process_state_summary应用自己设置的最多 128 字节状态;解码时带数据结构版本(schema version)
anr_id / anr_type / anr_timeout_ms / anr_user_perceptibleAPI 37 的结构化 ANR 字段
local_markers上次启动标记、页面、升级状态、灰度配置版本
resource_snapshot最近一次内存、文件描述符数、线程数和磁盘可用空间采样
attachment_type / attachment_state / attachment_id区分 ANR 文本、tombstone protobuf、缺失、超限和解析失败

source_fingerprint 只标识同一采集来源中的记录,当不了跨来源的统一事件 ID。崩溃 SDK 与系统退出记录的时间精度、进程标识和到达顺序都不同,匹配结论由服务端输出“确定匹配、候选匹配、未匹配”,端侧不要提前合并、把原始关系弄丢。

隐私要求优先于字段完整度。ANR trace、tombstone、堆转储与日志可能包含线程名、文件路径、URL、系统属性或内存片段。进入上传队列前要限制字节数、验证格式、删除不需要的数据,并应用项目的数据授权、保留和删除策略;做不到可靠脱敏的附件,就不要上传。

采样和容量策略没有全项目统一的固定比例:摘要预算按事件率、用户影响和上传成本确定;附件还要按网络、电量、磁盘、用户授权和诊断价值二次决策。崩溃循环时,端侧保留少量代表样本和丢弃计数,服务端按版本、进程和由退出原因与堆栈生成的故障签名限流。

常见误判

多进程应用容易出现归因错位。getHistoricalProcessExitReasons(null, 0, maxNum) 会匹配调用者 UID 下的包,外部服务进程的记录也可能被卷进来;主进程读到推送进程或外部服务进程的 REASON_LOW_MEMORY,直接记成主进程启动失败就归错位了。查询包名、processName、package UID、real UID 和 defining UID 都要进入明细。

REASON_USER_REQUESTED 分不出用户是在设置里强行停止,还是从最近任务(Recents)移除。Android 14 / API 34 以前,包更新或组件状态变化还可能落到这个 reason 上;API 34 才加入 REASON_PACKAGE_UPDATED 与 REASON_PACKAGE_STATE_CHANGE。跨版本报表要结合系统 API 级别解释,旧系统的 REASON_USER_REQUESTED 里混着包更新,全部标成用户行为就错了。

REASON_USER_STOPPED 指多用户设备上的系统用户或工作资料被停止,与用户点击 Force stop 是两种事件。REASON_SIGNALED 只说明进程因操作系统信号退出;在不支持 LMK 分类上报的设备上,低内存终止可能表现为 SIGKILL,而其他原因同样可能发送 SIGKILL。没有更多证据时,光凭信号编号定不了唯一原因。

AppExitInfoTracker.preventExitInfoUpdate() 会保护已有的 REASON_ANR、REASON_CRASH 和 REASON_CRASH_NATIVE 记录,后续显式终止信息另起新记录,减少相邻终止动作覆盖故障原因。这是 android-17.0.0_r1 的实现细节,各设备厂商和旧版本是否一致没有 SDK 保证,外推要小心。

Android 框架会把退出历史持久化,并在系统服务就绪(system ready)后加载,但公开 API 只承诺近期的环形缓冲区记录——固定文件路径、固定条数、跨重启永久保留,这些都不在承诺里。没有历史记录,可能出自缓冲淘汰、系统尚未记录、数据被清除或厂商实现差异中的任何一种,拿它断定“进程未异常退出”是靠不住的。

退出原因与稳定性指标体系的映射

ApplicationExitInfo 可以补充 26.2 的 Crash 上报和 26.5 的证据包。服务端可按下表建立内部分类,但统计前要完成跨来源去重:

层级事件进入指标方式
明确故障REASON_CRASH、REASON_CRASH_NATIVE、REASON_ANR进入对应内部事件流;用户率和事件率分别计算
资源相关REASON_LOW_MEMORY、REASON_EXCESSIVE_RESOURCE_USAGE按 importance、设备内存与场景分层,不能自动标成内存泄漏
需要排查REASON_INITIALIZATION_FAILURE、REASON_DEPENDENCY_DIED、REASON_FREEZER、未知信号独立观察趋势并调查样本,不并入崩溃或 ANR 指标
用户或系统状态REASON_USER_REQUESTED、REASON_USER_STOPPED、包或权限状态变化、REASON_EXIT_SELF作为时间线背景;按 API 级别处理旧版本复用语义

无崩溃用户比例(crash-free users)、ANR 率等产品指标需要稳定的用户分母和来源去重,光凭退出记录数量生成不了。报告同时保留用户可感知故障率、内部退出事件率与数据完整性;三者分别描述用户影响、系统记录数量和数据可信程度。

ApplicationExitInfo 在线 API 参考已经把 REASON_MEMORY_LIMITER 与 REASON_ANOMALY 标到 Android 17 minor release 37.2,但这两个常量不在 android-17.0.0_r1 源码中。覆盖 android-17.0.0_r1 / API 37 基线的数据协议不要把它们设为必填枚举;若设备实际返回这些 reason,应保存原始整数、API 级别和 SDK_INT_FULL,再按后续版本能力扩展解析。

ApplicationExitInfo 与 GWP-ASan(抽样检测堆内存越界与释放后使用) / MTE(硬件内存标记扩展) 报告拼接

GWP-ASan 抽样检测堆内存越界与释放后使用,MTE 是硬件内存标记扩展,HWASan(基于地址标记的原生内存错误检测器) 基于地址标记检测原生内存错误;它们发现内存安全错误时,进程可能以原生崩溃或 abort 结束。这时原生崩溃事件封装应保存构建是否启用相关工具、ABI、操作系统信号、故障地址、构建 ID 和工具提供的结构化信息,后续再用 REASON_CRASH_NATIVE 与 tombstone protobuf 补充系统现场。

关联的时间容差要按两类采集来源的时间精度、排队延迟和设备 wall clock 质量确定。processName、ABI、操作系统信号、故障地址、原生栈顶帧与构建 ID 一致时,可以提高匹配置信度。工具报告用于解释内存错误类型,tombstone 用于补充线程、寄存器、内存映射和系统现场信息;原始来源仍需保留。GWP-ASan/MTE 原理见 15.10、17.9 和 20.3。

低版本可观测性能力对照表

Android 版本系统退出记录ANR 系统 trace原生 tombstone 端侧读取推荐策略
API 21-29无公开 API普通应用不能直接读取 /data/anr/普通应用不能直接读取 /data/tombstones/自建崩溃采集、watchdog 与资源采样,用启动标记补充判断
API 30ApplicationExitInfo 可用traceInputStream 可补充 ANR trace原生 tombstone 仍无公开 protobuf 入口用系统 reason 补充 ANR 与 LMK,原生崩溃依赖 Crashpad 或 Breakpad
API 31-33ApplicationExitInfo 可用同 API 30REASON_CRASH_NATIVE 可读取 tombstone protobuf,但可能为 null归并系统 tombstone、崩溃 SDK 与资源摘要
API 34-36包更新和组件状态拥有独立 reason同 API 30同 API 31避免把新版包更新误计为 REASON_USER_REQUESTED
API 37增加结构化 AnrInfotrace 仍可读取;AnrInfo 另行提供 ANR ID、类型、超时和用户感知字段同 API 31使用结构化 ANR 字段;保留旧版本兼容路径

版本分支要以运行时 API 检测为准。API 30 及以上优先保存系统 reason,API 31 及以上尝试读取原生 tombstone,API 37 再读取 AnrInfo;任何一步缺失都回到轻量业务标记与独立的崩溃或 ANR 来源。系统记录给到的是分类证据,离根因还有一步。

版本化采集能力与回退矩阵

退出记录给到的是被动诊断基线,Profiling API 在其上增加主动与触发式采集;我们要先探测设备能力,再按版本和结果交付状态选择回退路径。

同一个故障发生在 Android 10 和 Android 17 上,能拿到的系统证据差别明显:版本号只说明 API 可能存在,系统是否真的生成附件,还要看设备实现、后台采样、限流和故障类型。所以诊断系统要先判断设备版本和实际能力,再选择采集入口;旧版本保留其公开 API 范围,新行为的语义以正式开发者文档和 android-17.0.0_r1 标签下的源码为准。

三条诊断路径分别回答什么

我们把 Android 10 到 Android 17 的公开诊断能力分成三条路径。本文沿用平台术语 profile,泛指系统 trace、堆转储或采样结果;trigger 表示由系统事件触发采集的条件。

路径要回答的问题起始版本公开入口典型结果
退出追溯进程为何退出Android 11 / API 30ActivityManager#getHistoricalProcessExitReasons()reason、status、退出时间、最近一次 PSS / RSS、可选 trace
应用请求采集App 已知问题正在复现,能否请求一次 profileAndroid 15 / API 35ProfilingManager#requestProfiling(),或 AndroidX Profilingsystem trace、Java heap dump、heap profile、stack sampling
系统事件触发采集系统识别到特定事件时,能否保存事件前后的 profileAndroid 16 / API 36addProfilingTriggers()、全局结果监听器后台 trace 快照、Java heap dump、stack sample 等

三条路径各有分工。退出追溯记录的是进程终点,适合确认由 ANR、原生崩溃、用户操作或系统资源处置引起的退出,但通常缺少故障前的完整运行时序;应用请求采集针对我们可以控制的复现时段;系统事件触发采集按trigger各自契约运行且受限流;需要历史trace的类型依赖后台采样,OOM可请求heap dump,cold-start会新建采集,用来捕获难以预测的事件。

一次问题可以关联多条路径。例如,ANR 发生时系统可能生成触发式 system trace;进程随后被杀,下次启动又能读到 ApplicationExitInfo。两者要作为独立来源归档,再通过时间、进程和事件语义建立关联——时间相近,不构成覆盖其中一份的理由。

版本能力表

系统版本可用的主要能力仍需保留的降级手段
Android 10 / API 29App 自有日志、Crash SDK;开发或用户协助场景下使用 Perfetto、bug report(系统诊断包)业务状态快照、请求 ID、会话 ID、复现步骤
Android 11 / API 30增加 ApplicationExitInfo;ANR 记录可能带 traceCrash SDK、卡顿监控、人工 Perfetto
Android 12–14 / API 31–34native crash 记录可带 tombstone protobuf;API 33 / 34 又增加部分 reason保留 Crash SDK、卡顿监控和人工 Perfetto,并持续处理 trace 缺失
Android 15 / API 35增加 ProfilingManager#requestProfiling()低版本采集方案仍需保留
Android 16 / API 36增加 APP_FULLY_DRAWN、ANR 两类系统 trigger系统后台 trace 没运行或限流时仍会没有产物
Android 16 minor release / API 36.1增加主动请求后台 trace 快照、注册全部 trigger 及三类用户终止 trigger运行时按完整 SDK 版本检查
Android 17 / API 37增加 cold start、OOM、anomaly、CPU kill、app compat trigger;部分设备启用 MemoryLimiter设备覆盖、系统限流和无产物路径都要监控

API 36.1 是 Android 16 的 minor SDK release(次版本 SDK 发布),同一大版本内也可以新增 API;它和 SdkExtensions.getExtensionVersion() 表示的 Mainline SDK Extension(可由模块更新提供的扩展版本)是两套版本机制。

调用36.1新API前,先检查Build.VERSION.SDK_INT >= 36,再检查Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.BAKLAVA_1;SDK_INT 只记录大版本,靠 SDK_INT >= 36 分不出 36.0 和 36.1。Android 17 的 SDK_INT_FULL 高于该值,同样满足条件。

Android 10–14:沿用前文退出证据

Android 10 只能依赖 App 自有状态、Crash SDK,以及受控场景下的 bug report 或 Perfetto;Android 11 才加入 ApplicationExitInfo。Android 12 开始,native crash 记录可能带 tombstone protobuf;Android 13、14 又补充了 freezer、包状态变化和包更新等 reason。

这些退出字段、trace 类型、低内存能力探测和低版本回退策略已在前半篇完整展开,这里不再重复读取代码和字段解释,只把它们作为版本矩阵中的“被动退出证据”,继续讨论 Android 15 之后的主动与触发式 profiling。

Android 15:应用请求 profiling

Android 15 / API 35 引入公开的 ProfilingManager,实现位于 Mainline Profiling 模块 packages/modules/Profiling——Mainline 模块是可以经系统组件更新升级、无需完整系统 OTA 的 Android 组件。直接 API 签名为:

requestProfiling(int profilingType, Bundle parameters, String tag(调用方附加的短标识), CancellationSignal cancellationSignal, Executor executor, Consumer<ProfilingResult> listener)

应用也可以用 AndroidX Profiling 提供的 request builder(请求构造器)来构造请求,省去直接维护 Bundle 参数和平台版本分支的成本。Bundle 是 Android 用键值对传递参数的容器。平台支持四种请求类型:

profiling type适用问题主要成本或限制
Java heap dump完整记录某一时刻的 Java 堆对象,用于分析泄漏和堆占用构成可能暂停应用;文件可能含对象字段和业务数据
heap profile对一段时间内的内存分配采样,用于定位分配热点和堆增长采样结果有偏差;时段要覆盖问题发生阶段
stack sampling定期抽取线程调用栈,用较低成本观察 CPU 热点短函数和瞬态热点可能未被采到
system trace记录调度、Binder、应用 trace 等系统时间线,用于分析启动和卡顿缓冲区和时长影响文件大小与覆盖范围

请求通过异步回调返回,并且可能不执行。系统级与应用级rate limiter(频率限制器)、已有任务、执行或后处理错误、磁盘不足、非法参数都可能使请求失败,系统也可能延后开始。应用要归档 ProfilingResult#getErrorCode() 和可选的 getErrorMessage(),并把“请求已发出”和“结果已生成”作为两个状态。

结果回调有两个入口:

  • 请求专用的 Executor 与 Consumer<ProfilingResult>,用于当前 requestProfiling()。Executor 决定在哪个线程执行回调,Consumer 接收结果对象。
  • registerForAllProfilingResults() 注册的全局监听器,既接收应用请求结果,也接收系统 trigger 结果。

如果请求没有同时提供专用 listener / executor,进程也没有已注册的全局 listener / executor 组合,系统会丢弃该请求,不会启动采集。系统触发的结果没有请求现场的 callback(回调),只能经全局监听器交付;应用若在生成结果时未运行,下次启动并重新注册监听器后仍可能收到该结果。

成功文件的位置必须读取 ProfilingResult#getResultFilePath();目录结构和文件命名属于实现细节,上传器不要自行拼接 /data/user/0/<package>/files/profiling/...。ProfilingResult在API35提供error code、error message、结果路径与tag;getTriggerType()从API36才加入,要单独做版本检查。仅ERROR_NONE才进入成功文件处理;tag是调用方附加的短标识;它没有 getProfilingType()。

主动请求时,应用要把请求类型和自有 request ID 一起保存。系统触发时,应根据 trigger type、tag 和收到的文件类型归档,数据模型中不要虚构 profiling type 字段。

Android 16-17:系统事件触发 profiling

trigger注册表达应用对某类事件结果的兴趣,不保证一定有产物。需要历史trace快照的类型,要求事件发生时系统抽样后台trace正在运行、注册有效且预算允许;其ring buffer满后覆盖旧事件。OOM heap dump与cold-start新建trace/stack采样按各自机制处理,不能套用同一后台trace前提。

API 36

trigger触发条件系统结果解释边界
TRIGGER_TYPE_APP_FULLY_DRAWN冷启动后调用 Activity.reportFullyDrawn()正在运行的 system trace 快照依赖应用正确调用 reportFullyDrawn()
TRIGGER_TYPE_ANR系统已识别 ANR,准备执行后续处置前正在运行的 system trace 快照触发不表示进程一定因 ANR 被杀

这两类结果都依赖系统当时存在可保存的后台 trace。ANR 的 ApplicationExitInfo trace 是线程堆栈文本,Perfetto system trace 是系统时间线;两者需要不同的解析器。

API 36.1

trigger触发条件系统结果
TRIGGER_TYPE_APP_REQUEST_RUNNING_TRACE先注册该 trigger,再调用 requestRunningSystemTrace(tag)正在运行的 system trace 快照
TRIGGER_TYPE_KILL_FORCE_STOP用户在应用信息页点“强行停止”正在运行的 system trace 快照
TRIGGER_TYPE_KILL_RECENTS用户从最近任务界面移除应用并导致终止正在运行的 system trace 快照
TRIGGER_TYPE_KILL_TASK_MANAGER用户在 Task Manager 中停止应用正在运行的 system trace 快照

这些常量和方法在 API 36.1 才加入。同版本还增加 addAllProfilingTriggers(),用于注册当前进程对全部 trigger 的兴趣;若同一种 trigger 又通过 addProfilingTriggers() 单独注册,单独配置优先。接入代码要以 SDK_INT_FULL 对比 VERSION_CODES_FULL.BAKLAVA_1,并让 Android Lint 检查 minor SDK API;SDK_INT == 36 说明不了设备已经具备 36.1。

API 37

trigger触发条件文档规定的结果与注意事项
TRIGGER_TYPE_OOMApp 抛出 Java OutOfMemoryErrorJava heap dump;自定义 UncaughtExceptionHandler 必须继续调用默认 handler
TRIGGER_TYPE_ANOMALY系统检测到资源异常产物按异常类型变化,ProfilingResult#getTag() 提供附加分类
TRIGGER_TYPE_KILL_EXCESSIVE_CPU_USAGEApp 因过量 CPU 使用被杀,退出 reason 为 REASON_EXCESSIVE_RESOURCE_USAGEAOSP/API reference 写明返回后台 system trace 快照
TRIGGER_TYPE_COLD_STARTApplicationStartInfo(系统启动记录)判断为 cold start新启动的 system trace 与 stack sampling profile
TRIGGER_TYPE_APP_COMPAT系统发现未来版本将不再支持的异常行为产物随兼容性问题变化,tag 提供附加信息

MemoryLimiter 的退出与触发证据

Android 17 在部分设备上实施 MemoryLimiter,且不受应用 targetSdkVersion 限制。被其终止的进程表现为 REASON_OTHER,getDescription() 包含精确字符串 "MemoryLimiter:AnonSwap";命中限制时,TRIGGER_TYPE_ANOMALY 还可能提供 Java heap dump,但仍受设备覆盖、相应功能flag与配置、资源及限流约束;这类heap dump不能简单套用“已有后台system trace”的前提。

机器判断只认文档明确保证的完整标记:宽泛的 "MemoryLimiter" 匹配不了,把所有 REASON_OTHER 都归入内存限制同样行不通。MemoryLimiter kill 与 Java OutOfMemoryError 是两回事:前者走 anomaly trigger,后者才对应 TRIGGER_TYPE_OOM。

TRIGGER_TYPE_OOM 依赖默认未捕获异常处理路径。UncaughtExceptionHandler 是线程异常无人处理时的末端回调;自定义 handler 如果不继续调用原默认 handler,系统就用不上这个 trigger。应用仍可在合适时机主动请求 Java heap dump,但要评估进程当时是否还有足够资源完成请求。

TRIGGER_TYPE_ANOMALY 的产物没有固定格式。Android 17 文档列出的场景包括:命中系统内存限制时返回 heap dump,过量 Binder 调用时返回 stack sample;采集和处置的先后安排不能当作“应用回调必在终止前执行”的保证。该tag的MemoryLimiter含触发、延迟和可禁用kill等配置;结果投递还可能跨进程重启,应独立记录触发、文件生成和回调交付。

多个 package 共享同一 UID、并同时注册某些异常 trigger 时,系统可能不提供附件。这时结果处理器先看 trigger type、tag 和文件,再选择解析器。

TRIGGER_TYPE_COLD_START 会尽早启动一份新的 system trace 和 stack sampling profile,持续到应用调用 reportFullyDrawn();未调用时,公开 API 文档给出的默认停止时间为 5 秒。它用的 discard buffer(满后丢弃新事件的缓冲区)靠丢弃新事件保住启动初期的内容。采集启动仍可能有延迟,因此产物不保证覆盖进程创建后的每个事件。

TRIGGER_TYPE_KILL_EXCESSIVE_CPU_USAGE 仍有官方文档差异:android-17.0.0_r1 源码注释和 2026-08-03 更新的 API reference 写的是后台 system trace 快照,Android 17 features 页面写的是 call-stack sample。接入端应保存系统返回的原始产物及文件类型,解析器不要按 trigger 名写死。本文以版本化源码和 API reference 作为接口语义依据,并保留差异记录,等待官方文档统一。

冷启动触发新建采集,discard buffer 保留最早事件

图:冷启动触发可延迟开始;fully-drawn停止采集,未报告时默认5秒。discard满后丢新事件,不能保证捕获进程起点。

系统事实与应用推断的归档

推荐把退出记录、profiling 结果、Crash SDK 样本和业务状态保存为不同记录类型。caseId 是应用生成的诊断案例编号,只负责建立关联,抹不掉来源差异。

退出记录字段

字段说明
source固定为 application_exit_info
packageName、processName包和进程身份
packageUid、realUid、pid系统身份;注意隔离进程和 PID 复用
timestampMs系统记录的进程死亡时间
reason、status公开 reason 与退出状态
pssKb、rssKb最近一次样本;0 应按缺失处理
descriptionRaw限制访问的原始描述,只对有文档保证的标记做机器判断
traceKind、traceHashanr_trace、native_tombstone_proto 或 none;hash 是按文件内容计算的摘要
appVersion、device、osBuild、apiLevel解释版本差异所需环境

退出指纹可将包名、进程名、UID、PID、时间戳、reason与status做带版本的无歧义编码,再取摘要;null与空串也要区分。它是尽力身份而非SDK唯一事件ID。后补附件独立upsert;若平台后续修订分类导致指纹变化,保留候选关联与规则版本,不能声称从此绝不重复。系统会在进程退出后复用 PID,主键里只有 PID,两次退出就区分不开。读取窗口可能重复返回同一条历史记录,因此应先按完整系统字段去重,再与客户端案例做候选关联。

profiling 结果字段

字段说明
sourceprofiling_request 或 profiling_trigger
requestId主动请求时由 App 生成;系统 trigger 可为空
requestedProfilingType只在主动请求时由 App 自己记录
triggerTypeAPI36+才读getTriggerType();API35按已保存的主动请求上下文标注来源,不调用新成员
tag原样保存,同时限制长度和敏感信息
errorCode、errorMessage结果状态
resultFilePathLocal本地处理使用;不要直接作为服务端长期标识
artifactMime、artifactHash、artifactSize按收到的文件计算;MIME 表示文件内容类型,hash 用于校验内容与去重
caseId、sessionId应用侧关联字段

主动请求按应用持久化的request ID建立请求身份;同一结果可能同时到专用与全局listener,文件hash作为附件去重键。系统trigger没有公开稳定结果ID或结果时间戳;相同非空产物可用trigger type、tag与内容hash尽力去重,首次接收时间另存,不能把每次重投递的接收时间放进稳定键。无文件的错误回调保留本地接收ID和error code;未收到回调另记未知。缺少稳定平台身份时保留重复/冲突计数和来源,不通过立即重复请求掩盖限流。

常见事件的关联规则

事件建议关联方式不应采用的方式
Java crashCrash SDK 样本与 REASON_CRASH 按进程、时间、异常摘要做候选匹配只看 PID
Native crashminidump 与 REASON_CRASH_NATIVE、tombstone 按进程、signal、时间、build ID 匹配把 tombstone 和 minidump 互相覆盖
ANRREASON_ANR、ANR trace、triggered system trace 按事件时间和进程关联假定 trigger 发生后进程必然被杀
Java OOMTRIGGER_TYPE_OOM heap dump 与 Java crash/退出记录关联把 MemoryLimiter kill 当成 OOM
MemoryLimiterREASON_OTHER 加精确 description 标记,并关联 anomaly heap dump匹配所有 REASON_OTHER
用户终止REASON_USER_REQUESTED 或 36.1 用户终止 trigger 单独分类计入 crash 率

时间接近只能生成候选关系。服务端应保留原记录,给关联关系附上算法版本和置信度,便于后续修正。

排障决策表

问题Android 10Android 11-14Android 15Android 16Android 17
慢启动自有启动指标;实验设备 Perfetto自有指标与 Perfetto,并用退出记录排除异常终止主动请求 system trace可注册 APP_FULLY_DRAWN优先评估 COLD_START trace + stack sampling
ANR卡顿监控、bug report、人工 PerfettoREASON_ANR + 可选 ANR trace可主动请求 system trace 复现增加 ANR trigger 快照沿用 ANR trigger 与退出记录
Native crashCrashpad / Breakpad minidumpAPI 31 起再取可选 tombstone protobufminidump + 可选 tombstone protobufminidump + 可选 tombstone protobufminidump + 可选 tombstone protobuf
Java OOMCrash SDK 与内存趋势Crash SDK、内存趋势与退出记录可控场景主动请求 heap dump / profile主动请求 heap dump / profile,并关联退出记录增加 OOM trigger heap dump
被系统杀自有前后台和资源记录reason、status、最近 PSS / RSS;探测 low-memory kill reporting(低内存终止上报)能力退出记录与自有资源记录退出记录,并按适用版本增加用户终止 trigger增加 CPU kill、anomaly 和 MemoryLimiter 证据

高版本能力是补充证据,低版本日志、指标和 Crash SDK 仍要保留;某个 trigger 在 API 上可用,也不表示每台设备、每次事件都能产生结果。

隐私、限流与采集成本

profiling 文件可能包含比普通日志更敏感的内容:

  • Java heap dump 可能保存对象字段、缓存、请求参数和页面状态。
  • system trace 可能出现方法名、线程名、调度时序、Binder 交互及部分系统现场信息。
  • stack sampling 和 heap profile 会暴露代码结构、调用栈或分配路径。
  • ANR trace、tombstone 与 minidump 可能包含路径、寄存器、映射和构建信息。

采集设计至少要包含用户授权或适用的合规依据、数据最小化、传输与静态加密、访问审计、保留期限、删除机制和远程关闭能力。tag、caseId 和文件名中不要放用户输入、账号、URL、token 或其他敏感值。

采集预算应来自设备实验和线上观测。system trace 按时长与缓冲大小评估,stack sampling 按采样频率评估,heap dump 按停顿、峰值内存和文件大小评估。配置应区分诊断案例、版本、设备和采集类型;系统 rate limiter 之外,应用仍需自己的并发控制与预算。命中限流或跳过采集时,上报轻量状态和原因,不要立即循环重试。

与 26.3 证据包模板的关系

26.3 负责问题受理、复现步骤、远程日志、灰度处置和问题单流程。版本化的“系统诊断附件”包括:

  • ApplicationExitInfo 退出记录及可选 ANR/native 附件;
  • 应用请求产生的 system trace、heap dump、heap profile 或 stack sampling;
  • trigger 产生的文件、tag、trigger type 和错误状态;
  • 每份文件的来源、hash、大小、采集时间、系统版本、权限和保留期限。

问题单展示时应保留来源标签。值班人员需要知道某个结论来自系统退出记录、App 自有日志还是 profiling 文件,才能判断证据强度。

版本矩阵与 Profiling 的源码、文档引用

全文小结

ApplicationExitInfo 可以补充进程来不及上报的退出分类,并把 ANR trace、原生 tombstone、最近一次 importance 与应用状态放到同一份证据记录中。公开 API 不提供 subreason,PSS/RSS 只是最近一次采样值,历史条数与附件都可能缺失;服务端必须保留缺失状态和来源关系。

Android 15 之后的 ProfilingManager 和 trigger-based profiling 补上了主动、事件前后采集,但 API 存在,产物未必存在。诊断系统应先做完整版本与能力探测,再分别归档退出记录、profiling 结果和 App 自有证据;Android 17 的 AnrInfo、MemoryLimiter 标记等高版本信息只用于增强证据,替代不了低版本回退、符号文件和可重复实验。

参考资料