title: App 可观测性架构与性能数据采集 chapter: '26.1' section: '26.1' status: finalized applicable_versions: Android 10 (API 29) - Android 17 (API 37) last_verified: '2026-08-15' last_source_verified_at: '2026-08-15' last_verified_against: Current Android vitals / startup / rendering / SystemClock / ProfilingManager / ProfilingTrigger / privacy docs, Firebase Performance Monitoring, OpenTelemetry Logs, and AOSP android-17.0.0_r1 sources retrieved 2026-08-15 confidence: high sources:
- type: clipping path: Clippings/线上疑难问题该如何排查和跟踪?-Android开发高手课-极客时间 1.md
- type: clipping path: Clippings/线上疑难问题该如何排查和跟踪?-Android开发高手课-极客时间 33.md
- type: clipping path: Clippings/线上疑难问题该如何排查和跟踪?-Android开发高手课-极客时间 34.md
- type: clipping path: Clippings/线上疑难问题该如何排查和跟踪?-Android开发高手课-极客时间 35.md
- type: official path: https://developer.android.com/topic/performance/vitals
- type: official path: https://developer.android.com/topic/performance/appstartup/analysis-optimization
- type: official path: https://developer.android.com/topic/performance/vitals/render
- type: official path: https://firebase.google.com/docs/perf-mon
- type: official path: https://developer.android.com/reference/android/os/SystemClock
- type: official path: https://developer.android.com/reference/android/os/ProfilingManager
- type: official path: https://developer.android.com/reference/android/os/ProfilingTrigger
- type: official path: https://developer.android.com/privacy-and-security/about
- type: official path: https://opentelemetry.io/docs/specs/otel/logs/
- type: aosp path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/os/SystemClock.java
- type: aosp path: https://android.googlesource.com/platform/packages/modules/Profiling/+/refs/tags/android-17.0.0_r1/framework/java/android/os/ProfilingManager.java
- type: aosp path: https://android.googlesource.com/platform/packages/modules/Profiling/+/refs/tags/android-17.0.0_r1/framework/java/android/os/ProfilingTrigger.java
- type: source path: https://android.googlesource.com/platform/packages/modules/StatsD/+/refs/tags/android-17.0.0_r1/framework/java/android/app/StatsManager.java
- type: source path: https://cs.android.com/android/platform/superproject/+/android-17.0.0_r1:frameworks/proto_logging/stats/atoms.proto
- type: source path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/services/core/java/com/android/server/am/CachedAppOptimizer.java
- type: source path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/services/core/java/com/android/server/am/MemoryLimiter.java
- type: source path: https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/services/core/jni/com_android_server_am_MemoryLimiter.cpp
- type: source path: https://android.googlesource.com/kernel/common/+/refs/tags/android17-6.18-2026-06_r6/Documentation/admin-guide/cgroup-v2.rst
- type: official path: https://developer.android.com/reference/androidx/metrics/performance/JankStats
- type: official path: https://developer.android.com/topic/performance/jankstats
- type: official path: https://developer.android.com/reference/android/os/Debug
- type: official path: https://developer.android.com/reference/android/os/Debug.MemoryInfo
- type: legacy-reference-preserved path: https://android.googlesource.com/platform/system/memory/+/refs/tags/android-17.0.0_r1/libmeminfo/
- type: source path: https://android.googlesource.com/platform/system/memory/libmeminfo/+/refs/tags/android-17.0.0_r1/
- type: legacy-reference-preserved path: https://android.googlesource.com/platform/system/memory/+/refs/tags/android-17.0.0_r1/libmeminfo/libmemevents/
- type: source path: https://android.googlesource.com/platform/system/memory/libmeminfo/+/refs/tags/android-17.0.0_r1/libmemevents/
- type: official path: https://developer.android.com/reference/android/app/ActivityManager
- type: official path: https://developer.android.com/reference/android/app/ActivityManager.MemoryInfo
- type: official path: https://developer.android.com/reference/android/app/ApplicationExitInfo
- type: official path: https://firebase.google.com/docs/perf-mon/custom-url-patterns
- type: official path: https://developer.android.com/topic/performance/power/setup-battery-historian
- type: official path: https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started/define-work#work-constraints
- type: official path: https://square.github.io/leakcanary/getting_started/ tags:
- observability
- metrics
- logs
- traces
- architecture
- android
- performance
- statsd
- jankstats
- memory-monitoring
- leakcanary
- apm
- android-17 related_chapters:
- '26.2'
- '17.11'
- '16.1' pipeline_stage: ready-to-publish task6_state: reviewed task9_state: reviewed last_idle_audit_at: '2026-06-30' task2b_state: fixed last_draft_polish_at: '2026-08-15T18:13:45+08:00' last_draft_polish_run_id: 20260815-181345-gracker-writing-462 last_review_finalize_at: '2026-08-15T18:13:45+08:00' last_review_finalize_run_id: 20260815-181345-gracker-writing-462 last_rework_at: '2026-08-15T18:13:45+08:00' last_rework_run_id: 20260815-181345-gracker-writing-462 last_consolidated_at: '2026-08-24' consolidated_from:
- src/part5-app/ch26-observability/01-observability-architecture.md
- src/part5-app/ch26-observability/03-performance-collection.md
App 可观测性架构与性能数据采集
App 可观测性需要回答四个问题:影响了多少用户、现场留下了哪些证据、可能由哪个模块负责、修复上线后同口径指标是否恢复。新版本的崩溃(Crash)率异常时,应先确认受影响的用户、机型和版本,再关联崩溃堆栈、发布记录与经过控制的用户操作摘要。指标(Metrics)用于观察群体趋势,日志(Logs)用于还原单次事件,追踪(Traces)用于解释耗时路径。整体架构可从数据模型、端侧采集、服务端处理和问题流转四个层面展开。
平台锚点为 Android 17 / API 37 / android-17.0.0_r1。可观测性协议大多由应用与服务端共同定义,第三方 SDK(Software Development Kit,软件开发工具包)的字段不属于 Android 平台保证。时间基准和线上系统性能剖析资料(profile)的边界,分别以 Android 17 的 SystemClock 与 ProfilingManager 实现为准。
应用可观测性从统一事件模型开始,把性能、稳定性、资源和业务上下文关联到会话、设备和版本。客户端采集还要控制线程、序列化、存储、网络和隐私开销。
信号模型、上下文与端云职责
指标、日志与追踪分别回答什么问题
三类证据的粒度和用途不同:指标适合观察群体趋势,日志适合还原单次现场,追踪适合解释时间线中的慢点。混用同一套存储、标签和采样规则,会让成本与查询方式同时失去控制。
| 类型 | 典型数据 | 适合回答的问题 | 不适合处理的问题 |
|---|---|---|---|
| 指标(Metrics) | Crash 率、ANR(Application Not Responding,应用无响应)率、启动 P95(第 95 百分位)、慢帧率、网络失败率、WakeLock(唤醒锁)异常率 | 这个版本是否变差、影响哪些机型、是否达到告警阈值 | 还原单个用户当时发生了什么 |
| 日志(Logs) | 业务日志、诊断日志、Crash 附加信息、用户反馈时间窗日志 | 这个用户的操作路径、请求参数摘要、错误码、降级原因 | 长期保存全部明细并做高频聚合 |
| 追踪(Traces) | Perfetto(Android 系统追踪工具)数据、方法耗时片段、网络阶段耗时、会话时间线 | 一次启动、卡顿或网络请求慢在哪个阶段 | 替代日常指标看板,或面向全部用户长期采集 |
Android vitals 侧重指标:在用户允许采集的前提下,Google Play 汇总稳定性、性能、电量和权限等质量数据。当前核心指标包括用户感知崩溃率(user-perceived crash rate)、用户感知 ANR 率(user-perceived ANR rate)和部分唤醒锁使用过量(excessive partial wake locks)。Play 使用最近 28 天的数据评估应用质量,并每天检查 28 天平均值。它可以作为 Play 分发侧的质量基线,但不提供应用自定义业务场景和内部日志;团队仍需补充页面、场景、构建版本与渠道等维度。Android Vitals 官方说明
Firebase Performance Monitoring 更接近应用内部性能观测:它可自动采集启动、HTTP/S 请求和按屏幕统计的渲染数据,并允许添加自定义代码追踪(custom code trace)、自定义指标(custom metrics)与筛选属性(attributes)。这里的 trace 是两个时间点之间的任务记录及其指标,不是 Perfetto 文件;attributes 可按国家、设备、应用版本和系统版本筛选数据。Firebase Performance Monitoring 官方说明
应用自建体系可以组合两类数据:Android vitals 提供 Play 侧质量结果,自建指标补充内部维度,日志和追踪提供现场证据。只看 vitals 能发现质量变化,但无法解释变化出现在哪个业务场景、由什么触发。
事件数据模型设计示例
指标、日志和追踪在端侧的编码方式不同,但需要共享关联字段。下面的 JSON 展示一份最小公共信封,即包在各类事件外层、用于关联和解码的一组公共字段。字段名属于应用协议示例,不是 Android 平台 API。
{
"$common": {
"schema_version": "obs.event.v3",
"event_id": "0198f1a4-...",
"event_type": "startup_metric",
"wall_time_ms": 1785312000123,
"elapsed_realtime_ns": 418273600012345,
"session_id": "resettable-session-id",
"trace_id": "9f0c...",
"scene_id": "MainActivity_onResume",
"scene_seq": 12,
"app_version": "8.4.2",
"build_number": 8420,
"os_version": "Android 17",
"api_level": 37,
"device_model": "example-model",
"device_brand": "Google",
"network_type": "WIFI",
"app_in_foreground": true,
"inclusion_probability": 0.1,
"sampling_rule_id": "baseline-startup-v3"
}
}
wall_time_ms 记录现实时间(wall clock),用于和发布、告警对时,但用户或网络都可能调整它;会话内排序与耗时计算应使用单调递增的 elapsed_realtime_ns。Android 的 SystemClock.elapsedRealtimeNanos() 包含深度睡眠时间,适合测量间隔,但设备重启后会归零,因此还需要 session_id(会话标识)和 scene_seq(场景内顺序号)划定边界。event_id 用于重试去重,trace_id 用于关联不同信号,schema_version 表示事件结构版本。
下面的指标片段展示启动事件专属字段。为节省篇幅,$common 只保留 event_type;生产事件仍应携带完整公共信封。
{
"$common": { "event_type": "startup_metric" },
"startup": {
"type": "cold",
"ttid_ms": 1820,
"ttfd_ms": 2350,
"attach_base_ms": 120,
"oncreate_ms": 450,
"first_screen_ms": 1250,
"dag_critical_path_ms": 1400,
"thread_pool_wait_max_ms": 80,
"provider_init_count": 12,
"provider_init_total_ms": 320,
"sdk_init_count": 8,
"blocked_by_network": true,
"network_requests": [
{ "name": "config_fetch", "duration_ms": 520, "cached": false },
{ "name": "ab_test", "duration_ms": 180, "cached": true }
]
}
}
ttid_ms 表示首次显示时间(Time to Initial Display,TTID),ttfd_ms 表示完全显示时间(Time to Full Display,TTFD);其他阶段名属于应用自定义协议,必须给出统一的起止点。日常事件适合保存阶段耗时、计数和阻塞原因,无需为每次启动保存系统追踪。指标出现回归后,再对受控样本采集性能剖析资料。
下面的日志片段只携带允许上报的模板标识与分类字段,避免把自由文本当成默认协议。
{
"$common": { "event_type": "user_log" },
"log": {
"level": "ERROR",
"tag": "PaymentFlow",
"message_template_id": "payment_confirm_timeout",
"message_fingerprint": "hmac-sha256:abc123...",
"error_code": 504,
"user_visible": true,
"prev_scene_id": "PaymentConfirmActivity",
"stack_trace_hash": "hmac-sha256:def456..."
}
}
普通上报应优先使用枚举模板和允许列表字段。对原始文本直接计算 SHA-256 摘要并不等于匿名化:低熵内容的可能取值很少,错误消息、手机号或 URL 仍可能被枚举猜出。需要稳定聚合时,应先移除敏感字段,再用受控密钥生成 HMAC(Hash-based Message Authentication Code,基于哈希的消息认证码)指纹。详细日志只能在明确的数据政策、用户告知或同意、目标时间窗、配额和自动过期约束下按目标取回(回捞)。
下面的追踪片段展示应用埋点生成的 span 摘要。span 表示追踪中的一个有起点和持续时间的工作区间;它可以与日志共享 trace_id,但不等同于系统追踪。
{
"$common": { "event_type": "method_trace" },
"trace": {
"span_id": "37ab...",
"parent_span_id": "a011...",
"span_name": "MainActivity.onCreate",
"start_elapsed_realtime_ns": 418273600320000,
"duration_ms": 145,
"thread": "main",
"sub_spans": [
{ "name": "setContentView", "duration_ms": 45 },
{ "name": "findViewById", "duration_ms": 12 },
{ "name": "ViewModel.init", "duration_ms": 68 }
]
}
}
trace_id、span_id 和 parent_span_id 构成父子关联,单调时钟给出同设备会话内的起点与时长。Android 15 / API 35 起,普通应用可通过 ProfilingManager 请求系统追踪(system trace)、Java 堆转储(heap dump)、原生堆采样(heap profile)或调用栈采样(stack sampling);请求受系统限流且不保证执行,结果会脱敏并只包含请求应用的相关信息。Android 16 / API 36 起可注册系统触发器;Android 17 / API 37 又增加冷启动(TRIGGER_TYPE_COLD_START)、异常行为(TRIGGER_TYPE_ANOMALY)等类型。该能力不保证异常后取得完整设备 Perfetto 数据;回调失败、文件配额、用户数据政策与上传策略都要单独处理。
数据模型不能依赖“字段永不删除”的约定维持兼容。每条事件都要携带数据结构(schema)版本;服务端至少兼容当前版本和迁移窗口内的旧版本。新增字段必须有缺省语义;废弃字段应先让服务端同时读取新旧格式,验证新格式数据后再停止发送旧字段。最小版本只需要事件类型、双时钟、构建版本、会话/场景 ID、采样纳入概率与业务核心字段。高基数字段的不同取值数量很大,不能直接放入指标标签集合。
应用侧监控体系分层设计
应用侧架构需要满足四项约束:采集入口低开销、缓冲区有容量上限、证据按价值分级、上传受配额控制。监控 SDK 不应在业务线程内完成编码、写盘或网络请求;故障期间,这些额外工作可能进一步增加延迟或资源压力。
下图展示采集、缓冲、存储、上传和远程控制之间的数据方向:
graph TD A[业务代码 / 系统回调] --> B[采集 API] B --> C[轻量事件模型] C --> D[有界内存队列] D --> E[本地分片存储] E --> F[批量上传 Worker] F --> G[服务端接入] H[远程配置与采样策略] --> B H --> F I[SDK 自监控] --> F
端侧至少分成五层:
- 采集层:接入 Crash、ANR、启动、卡顿、内存、网络、耗电、业务场景等信号。采集代码只记录时间戳、场景 ID、错误码和摘要字段,不能同步写文件或发网络请求。
- 缓冲层:使用有界队列或
RingBuffer(环形缓冲区)处理高频事件。队列满时按事件等级丢弃,丢弃数进入 SDK 自监控字段。 - 存储层:普通性能样本进入小块分片文件;Crash、ANR、HPROF(Java 堆转储文件)、Perfetto trace 等大文件使用独立目录和配额。详见 17.12 节的端侧 APM(Application Performance Monitoring,应用性能监控)存储设计。
- 上传层:按事件优先级、网络类型、前后台状态和服务端限流批量上传。弱网下优先上传摘要,延后上传大文件;Worker 指负责这项后台工作的调度单元。
- 控制层:服务端下发采样率、事件开关、远程诊断命令和熔断规则。熔断是在异常或成本超限时自动关闭高成本采集;每条配置都要带签名、版本号、过期时间、作用范围和回滚策略。
这个分层还要遵守两个执行约束。主线程只提交原始事件;分析和聚合通常放到服务端,端侧只做必要的聚合、压缩、脱敏和失败恢复。mmap(内存映射)也会产生首次缺页、文件扩容、同步和存储压力,不能据此假定主线程写入没有开销。17.12 节说明了 APM SDK 的持久化、编码协议、网络投递和自监控实现。
Crash 还需要一条不依赖普通异步队列的最小保全路径。进程异常退出时,后台线程可能来不及消费队列;Java 未捕获异常(uncaught exception)与原生信号(native signal)能安全执行的操作也不同。实现应预分配必要结构,避免在原生信号处理器中调用不具备异步信号安全性(async-signal-safe)的操作,并在下次启动时校验和补传未完成记录。具体边界见 26.2。
从采集到告警的完整数据路径
一套可用的应用可观测性系统通常经过:端侧采集 → 本地暂存 → 批量上传 → 接入清洗 → 实时聚合或离线聚合 → 告警 → 证据查询 → 修复验证。
| 阶段 | 主要任务 | 失败表现 |
|---|---|---|
| 采集 | 定义事件结构,补齐版本、设备、页面、场景、网络、前后台等公共字段 | 指标无法按场景拆分,异常样本缺少现场信息 |
| 上报 | 合并小事件,优先发送高价值摘要,保留失败重试和服务端限流处理 | 崩溃后数据丢失,弱网下低价值事件挤占通道 |
| 存储 | 明细、聚合、索引分开;大文件单独配额和保留期 | 查询慢、成本高、敏感文件长期留存 |
| 分析 | 按版本、机型、Android 版本、渠道、页面、用户分群看分布和趋势 | 全局均值正常,重点机型已经恶化 |
| 告警 | 绑定可执行的处理条件,例如某版本用户感知 ANR 率、启动 TTFD P95、慢会话比例 | 告警噪声多,工程团队不再信任 |
| 证据查询 | 从指标进入样本,再关联日志、追踪、崩溃栈和发布记录 | 看得到异常,看不到现场 |
| 验证 | 修复版本上线后,对比线上指标和基准测试结果 | 修复效果只能靠主观判断 |
Android 官方启动优化文档区分 TTID 和 TTFD:TTID 表示首帧出现,TTFD 表示应用达到自行上报的完全绘制、可用状态。启动监控不能只看一个总耗时;reportFullyDrawn() 的调用位置必须对应产品定义的可用状态。Macrobenchmark(Jetpack 宏基准测试库)的 StartupTimingMetric 可用于线下基准测试,线上再用端侧指标观察真实分布。App startup analysis and optimization
渲染也要区分指标和现场。Android 官方文档分别定义慢帧(slow frames)、冻帧(frozen frames)和 ANR;Perfetto FrameTimeline(帧时间线)可用于追踪慢帧或冻帧原因。线上指标负责指出哪些版本、页面和机型发生回归,设备实验或受控线上性能剖析负责解释具体样本。Slow rendering
服务端分析层要保留几类关联键:event_id、session_id、trace_id、span_id、scene_id、build_version、device_model、android_version 和 network_type。这些字段让 Crash、ANR、性能指标、用户日志和发布记录能够互相查询。session_id 应是短期、可重置且用途受限的标识,不能用永久设备 ID 代替。详见 16.1 节的采集到治理过程设计。
采样策略与数据量控制
可观测性系统的成本来自端侧 CPU / I/O、用户流量以及服务端存储与计算。采样策略要同时控制这些成本。容量评估不能只按 DAU(Daily Active Users,日活跃用户数)猜测,应使用“活跃设备数 × 每设备事件频率 × 编码后字节 × 保留期 × 副本数”计算,再用灰度实测校正压缩率和一次查询实际读取的数据量。
常用策略可以分成四类:
- 高优先级基线:在用户数据政策允许的范围内,优先保留 Crash / ANR 摘要、构建版本、设备维度和关键页面启动指标;仍需单设备限额、去重和熔断,“全量”不代表无限写入。
- 用户级采样:对高频性能事件按用户做确定性分桶,即用稳定规则把同一用户映射到同一采样组,避免逐事件随机采样造成会话时间线断裂。命中用户在一个时间窗内保持同一策略,便于还原连续会话。
- 异常补采:基线指标发现异常后,对目标版本、机型和渠道短期开启更高采样率;性能剖析采集还要满足平台限流、设备状态、隐私和文件配额。
- 大文件限额:HPROF、系统追踪和完整日志包必须限制单设备次数、文件大小、上传网络和保留时间。
采样配置要有版本号。客户端每次成功上报时带上本地配置版本,服务端可以随响应返回新配置;这条路径不保证即时到达,因此客户端还要为配置过期、长期离线和签名校验失败定义保守缺省值。配置应支持紧急关闭开关(kill switch),用于迅速停用异常或成本失控的采集能力;可关闭范围必须受限,避免远程配置破坏 Crash / ANR 等最低诊断能力。
采样后的数据必须携带 sampling_rule_id、分层字段和该事件的 inclusion_probability(纳入样本的概率)。只有抽样设计与纳入概率已知时,服务端才能按权重估计总体;异常补采、失败全量和成功抽样混合后,必须按层分别计算,不能把所有样本简单除以一个采样率。单用户排查界面也要显示采样命中与丢弃原因,避免把“没有采到”误判成“没有发生”。
可观测性建设的常见陷阱
从零建设可观测性系统时,下面六类问题最常见。
陷阱 1:把所有东西都上报,然后「服务端再筛」
端侧若默认“先全量上报,服务端再过滤”,写入、索引和高基数聚合会随事件频率增长。高基数表示字段有大量不同取值,例如完整 URL 或永久用户 ID;它会扩大索引和分组成本。服务端账单出现变化前,端侧已经消耗了 CPU、I/O 与用户流量。
设计时应先为每类事件估算频率、编码大小、基数和保留用途,再选择端侧聚合、确定性采样或原始事件。只上报异常会失去分母与正常基线;失败事件和成功样本可以采用不同层级,并在事件中记录各自的纳入概率。用户 ID、原始 URL、自由文本和堆栈全文等高基数字段不应直接进入指标标签。
陷阱 2:用指标替代日志和追踪
团队如果只关注指标看板,看到 Crash 率上升后,既没有日志定位页面、场景和操作摘要,也没有追踪解释耗时路径。
指标可以指出版本回归,日志可以给出支付确认事件的错误分类,追踪可以显示等待发生在哪个 span。三类信号的存储和采样方式不同,但要共享统一的关联信息;OpenTelemetry(开放式可观测性规范与工具体系)也使用 trace 与 span 标识连接日志和追踪,不只依赖相近时间戳。
陷阱 3:采样策略按事件随机,破坏了会话时间线
高频事件若逐条独立抽样,一个会话中只会留下不连续片段:日志显示用户打开了支付页,但前面的场景切换可能没有采到,排查时无法还原路径。
需要连续会话时,可使用 HMAC(sampling_key, resettable_id | sampling_rule_id) 生成稳定分桶,并让命中结果在配置时间窗内保持一致。客户端不上传原始标识;sampling_key 是服务控制的密钥,密钥轮换与标识重置都会改变分桶。这里的 HMAC 只用于稳定分组,不能证明标识已经匿名化。支付、地区或机型等分层若采用不同概率,估计总体时必须保留分层和纳入概率。
陷阱 4:端侧采集在主线程做编码和落盘
采集 API 常从主线程调用。如果 SDK 在调用栈内完成 JSON 序列化、文件扩容、压缩或同步写入,存储抖动会直接进入页面耗时;mmap 写入也可能因缺页和回写产生长尾。
采集入口应只读取必要的原始值并尝试写入有界队列,序列化和写盘放到专用线程。性能预算不能照搬固定数字,应在目标设备档位上测量入口的 P50、P95、P99(第 50、95、99 百分位)、分配次数、队列丢弃率和主线程长尾,并把 SDK 自身开销纳入发版门禁。
陷阱 5:把诊断数据当成默认开启的能力
用户日志回捞、远程诊断命令和系统性能剖析都属于高敏感、高成本能力,默认应关闭;只有命中明确策略并满足用户数据政策后才短期开启。全量自由文本日志不能作为基线能力。
可以区分两类数据通道:基线通道发送经过最小化的 Crash / ANR 摘要、启动/帧指标和网络结果;诊断通道处理日志回捞、ProfilingManager 结果和远程只读检查。诊断策略必须限定目标、时间窗、单设备配额和保留期,并允许从客户端和服务端两侧关闭。
陷阱 6:服务端告警阈值与用户感知脱节
固定的绝对增量没有考虑基线、样本量、设备分布和产品场景,同样的变化在不同页面可能含义完全不同。告警应绑定用户可感知的 SLO(Service Level Objective,服务等级目标),例如产品定义的 TTFD 慢会话比例、用户感知 ANR/Crash 率或冻帧会话比例,并同时检查置信区间、持续时间和版本/机型分层。Android vitals 的不良行为阈值(bad behavior threshold)采用 Play 分发口径;内部告警可以更早,但必须明确两者的分母不同。
用户日志与远程诊断:现场证据层
指标告诉团队哪里异常,日志和远程诊断帮助团队还原现场。可观测性架构除了指标看板,还要能在必要时为特定用户、特定版本、特定机型补采现场证据。
可执行的设计通常包含三类能力:
- 用户日志回捞:只拉取目标用户、目标时间窗、目标 tag(日志分类标签)的日志。日志正文默认脱敏,URL 查询参数(query)、认证令牌(token)、手机号和定位字段默认不入库。
- 远程诊断命令:对网络、存储、权限、配置和缓存状态执行允许列表内的只读检查,结果以结构化字段上报。命令必须带过期时间、设备配额、重放保护和服务端签名,不能提供任意 shell 命令、任意文件读取或动态代码执行入口。
- 异常触发补采:Crash、ANR、冻帧或启动超出产品阈值后,下一次启动优先上传摘要;命中灰度策略时,再请求受平台限制的性能剖析资料或目标日志。
Firebase Performance Monitoring 对 HTTP 请求使用不含查询参数的 URL 构造聚合模式,并声明不永久保存个人可识别信息。自建系统也应在采集前完成字段允许列表、脱敏与用途分级;哈希不是删除个人信息的替代方案。Firebase Performance Monitoring:User data
Android 17 平台源码边界
Android 17 的 SystemClock.java 区分三种时间:可被调整的现实时间(wall clock)、深度睡眠期间暂停的运行时间(uptime clock),以及包含深度睡眠且单调递增的经过时间(elapsed realtime)。事件协议同时保留现实时间和单调时间,分别用于跨系统对时与本机会话排序;一次 System.currentTimeMillis() 不能同时满足两种用途。
Android 17 的 ProfilingManager.java 写明请求受限流且不保证执行,返回结果经过脱敏并限定到请求进程。线上架构应把性能剖析资料视为稀缺的补充证据:先用指标确定目标群体,再用 span 和日志摘要缩小范围,随后在系统允许时采集。没有回调或没有制品都属于正常分支,诊断流程不能依赖它必定成功。
这里不使用 Linux 内核私有接口,也不从 eBPF(extended Berkeley Packet Filter,内核可编程追踪机制)、/proc(内核导出的进程与系统状态伪文件系统)或调度器实现推导应用协议,因此不引入内核源码标签。涉及内核采集路径的专题统一以 android17-6.18-2026-06_r6 为内核源码锚点。
最小可用架构
团队不必一开始建设完整平台。最小版本可以从下面几项开始:
- Crash / ANR 摘要:进程、线程、堆栈、版本、设备、前后台状态、最近场景。
- 启动与渲染指标:TTID、TTFD、慢帧 / 冻帧、关键页面场景 ID。
- 网络指标:DNS(Domain Name System,域名解析)、连接、TLS(Transport Layer Security,传输层安全协议)、首包、总耗时、错误码、网络类型。
- 受控日志:按分类标签和敏感级别区分、本地有界滚动、按目标会话与时间窗回捞。
- 采样与配置:带签名和过期时间的配置、确定性会话采样、纳入概率、异常补采与紧急关闭开关。
- 证据查询入口:从告警进入样本,再关联日志、span、性能剖析资料、发布版本和修复任务。
这些能力分别对应 26.2 的 Crash/ANR 上报、26.1 的性能指标采集和 26.3 的线上排查;各专题在同一事件标识、时钟和采样协议上继续扩展。
采样、缓冲、序列化与上传
数据模型确定后,客户端需要按信号价值分配采样和缓冲预算,并在离线、弱网和进程退出时保证可控降级。
概览
Android 没有一个面向普通 App、涵盖所有性能问题的“统一性能指标 Atom”。StatsD 是 Android 的系统统计收集与聚合服务,atom 是其数据结构定义中字段固定的一类统计事件。可用能力分布在不同权限层:StatsD 面向系统和特权组件;AndroidX JankStats 在 App 进程内提供帧级卡顿数据;在网络调用前后加入计时点的插桩、Debug.MemoryInfo 与业务埋点补充 App 自身指标;上传与服务端统计由 APM(Application Performance Monitoring,应用性能监控)系统负责。
平台上界为 Android 17 / API 37 / android-17.0.0_r1。缓存进程内存整理(Compaction)和暂停缓存进程执行的冻结机制(Freezer)早于 Android 17 已存在;Android 17 新增的 MemoryLimiter 还受功能开关(feature flag)、设备能力和厂商(vendor)配置控制。观察到内存曲线变化时,要区分公开 App API 能确认的事实与 Android 核心系统服务进程 system_server 的源码解释。PSS(Proportional Set Size,按共享页面比例分摊后的驻留内存)曲线本身不能证明某项系统策略已经触发。
1. 系统级指标采集:StatsD 的能力与权限边界
Android 17 的 StatsD 模块位于 packages/modules/StatsD。statsd 后台守护进程(daemon)接收 atoms.proto 定义的 pushed atom,即事件发生方主动推送的原子事件;它也会按照采集配置(config)向已注册的数据提供方请求 pulled atom。后者是由 statsd 发起拉取、提供方通过回调函数(callback)返回数据。聚合结果由具备权限的客户端通过报告或受限查询 API 读取。
1.1 StatsManager 客户端 API
Android 17 (android-17.0.0_r1) 中,与性能指标订阅、读取和 pull 数据提供相关的 StatsManager 公开 API 主要是三组方法:
// packages/modules/StatsD/framework/java/android/app/StatsManager.java
// Android 17 公开 API 子集
// 注册 pull atom 数据提供方(客户端向 statsd 提供自定义 pulled atom)
public void setPullAtomCallback(int atomTag, @Nullable PullAtomMetadata metadata,
@NonNull @CallbackExecutor Executor executor,
@NonNull StatsPullAtomCallback callback)
// 注册 StatsdConfig 订阅(返回 void)
public void addConfig(long configId, byte[] config)
throws StatsUnavailableException
// 读取已收集报告(主查询路径)
public byte[] getReports(long configId)
throws StatsUnavailableException
App 侧不能通过 StatsManager 写入事件。android.util.StatsLog.logStart/logStop/logEvent(int) 是公开的 breadcrumb API;breadcrumb 在这里指用于标记操作起止或单次事件的轻量记录,它只能写入 APP_BREADCRUMB_REPORTED。任意 StatsEvent 写入路径 StatsLog.write(StatsEvent) 属于受限的 @SystemApi,系统服务通常使用生成的 FrameworkStatsLog / StatsdStatsLog。
setPullAtomCallback() 用于客户端向 statsd 提供自定义 pulled atom 数据,读取 statsd 聚合指标要使用其他接口。当 statsd 需要拉取某个原子事件时,它会调用已注册的 StatsPullAtomCallback.onPullAtom(int atomTag, List<StatsEvent> data);其中 atom tag 是原子事件编号。客户端把 StatsEvent 加入 data 列表并返回 RESULT_SUCCESS / RESULT_SKIP 等结果,StatsManager 内部的 PullAtomCallbackInternal 再调用 resultReceiver.pullFinished() 把结果交回 statsd。PullAtomMetadata 的默认冷却间隔为 1000 ms,超时为 1500 ms,用于限制按需拉取频率,并不形成周期性定时回调。
addConfig() 返回 void(而非 boolean),用于向 statsd 注册 StatsdConfig;该配置描述要收集的原子事件与聚合方式。getReports(long configId) 用于读取 statsd 已收集的报告,是特权 App 获取聚合数据的主要接口。query() 需要 READ_RESTRICTED_STATS 权限,签名为 query(long configKey, String configPackage, StatsQuery query, Executor executor, OutcomeReceiver<StatsCursor, StatsQueryException> outcomeReceiver)。
权限要求:
- 注册 pull atom 数据提供方(
setPullAtomCallback)需要REGISTER_STATS_PULL_ATOM权限(位于frameworks/base/core/res/AndroidManifest.xml) - 注册 config 和读取报告(
addConfig、getReports)同时需要DUMP和PACKAGE_USAGE_STATS权限 - SQL 查询路径
query()需要READ_RESTRICTED_STATS权限 - AndroidManifest 中不存在
READ_PRECISE_STATS权限;当前 StatsD 权限模型以REGISTER_STATS_PULL_ATOM、DUMP、PACKAGE_USAGE_STATS、READ_RESTRICTED_STATS为主
1.2 数据流路径
整体数据流分三条路径:
下面的路径表示 pushed atom 和公开 breadcrumb 如何进入 statsd。
系统事件入站(系统服务 → statsd daemon):
App breadcrumb / system service → StatsLog / FrameworkStatsLog / StatsdStatsLog → libstatssocket → statsd daemon (本地 socket)
StatsLog 在 packages/modules/StatsD/framework/java/android/util/StatsLog.java 中定义。普通 App 可调用 logStart/logStop/logEvent(int) 写 breadcrumb;系统服务写入框架原子通常走生成的 FrameworkStatsLog.write() / StatsdStatsLog.write(),最终通过 libstatssocket 的本地 socket 写入 statsd daemon。普通 App 不能用这条路径写任意系统 atom。
下面的路径表示特权客户端注册 config 并读取聚合报告。
Config 订阅与报告读取(statsd ⇄ 特权 App):
StatsManager.addConfig(configId, config) → statsd daemon 按 config 聚合
StatsManager.getReports(configId) ← statsd daemon 返回已收集报告
特权 App 通过 addConfig() 向 statsd 注册 StatsdConfig(定义要收集哪些 atom、聚合方式),statsd 按 config 持续收集并聚合。App 通过 getReports() 读取聚合结果——这是获取 statsd 系统健康指标的主路径。
下面的路径表示 statsd 向特权数据提供方发起 pull。
Pull atom 数据提供(特权组件 → statsd):
StatsPullAtomCallback.onPullAtom(int atomTag, List<StatsEvent> data) → statsd 向客户端拉取自定义 pulled atom
setPullAtomCallback() 注册的是数据提供方:当 statsd 的 config 中包含 pulled atom 时,statsd 回调已注册的 callback,由客户端向 List<StatsEvent> 中填充指标数据。这不是 App 从 statsd 拉取系统聚合指标的通道——读取聚合指标应走 addConfig + getReports 路径。
当前 atoms.proto(android-17.0.0_r1)中定义了大量系统健康原子(如 AppStartOccurred(ID 48)、AnrOccurred、BatteryLevelChanged 等),但没有统一的 "性能指标大 Atom"。App 侧如需收集系统级指标,优先通过 AndroidX API(如 JankStats)和 Debug.MemoryInfo 等公开接口,而非直接依赖 StatsD。
StatsCompanionService 的职责:
StatsCompanionService 运行在 system_server 中,是通过 IStatsd 接口与 statsd 守护进程交互的辅助服务,主要处理配置管理和数据拉取方注册。事件写入由 StatsLog 通过 libstatssocket 直接连接 statsd 的本地套接字(socket),不经过 StatsCompanionService;因此,它也不是从 /dev/socket/statsdw 读取事件流的 JNI(Java Native Interface,Java 原生接口)桥接层。
1.3 系统指标采集边界与 App 侧替代方案
StatsD 的 pull atom 数据提供、config 注册/报告读取、SQL query 都是特权路径,分别需要 REGISTER_STATS_PULL_ATOM、DUMP + PACKAGE_USAGE_STATS、READ_RESTRICTED_STATS。普通 App 无法直接使用 StatsD 获取系统级性能指标。实际工程中,App 侧采集系统级指标的三条可用路径为:
路径 1:AndroidX JankStats — 帧级实时诊断
下面的最小示例读取帧时长、卡顿判定和 UI 状态。
// AndroidX metrics-performance 库
JankStats jankStats = JankStats.createAndTrack(window, frameData -> {
long frameDurationNs = frameData.getFrameDurationUiNanos();
boolean isJank = frameData.isJank();
// frameData.getStates() 返回 UI 状态列表
for (int i = 0; i < frameData.getStates().size(); i++) {
StateInfo state = frameData.getStates().get(i);
String stateKey = state.getKey();
String stateValue = state.getValue();
}
});
回调会在每帧触发,业务不能在这里直接序列化或上传;对象复用与线程边界详见 §6。
路径 2:Debug.MemoryInfo — 进程级内存采集
下面的最小示例读取当前进程的总 PSS。
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
Debug.MemoryInfo[] info = am.getProcessMemoryInfo(new int[]{Process.myPid()});
int totalPss = info[0].getTotalPss();
返回值的 UID(用户标识)与频率限制详见 §3,不能用这段代码轮询其他应用。
路径 3:Firebase Performance / 自建 APM SDK
网络耗时、启动耗时、自定义业务指标由 APM SDK 在 App 进程中直接采集,无需经过 StatsD。详见 §5、§9。
特权系统组件若要提供 pulled atom,需要为已经定义并获准使用的 atom tag 调用 setPullAtomCallback()。statsd 调用 StatsPullAtomCallback.onPullAtom() 时,提供方把符合数据结构定义(schema)的 StatsEvent 加入列表并返回结果码。atom ID、字段顺序、字段类型和可累加字段(additive field)必须来自同一次平台发布的 schema,不能用业务自定义常量代替平台定义。
AppStartOccurred 在 Android 17 atoms.proto 中的 ID 为 48(app_start_occurred = 48),字段包含 transition_delay_millis、starting_window_delay_millis、bind_application_delay_millis、windows_drawn_delay_millis 等,没有 latencyMillis 字段。
2. 电池感知采样:App 层实现策略
StatsD 在 daemon 层不根据电池状态自动调节采样率。电池感知采样需要 App 侧基于 PowerManager / BatteryManager 自行实现。
2.1 状态只参与策略,不直接决定固定百分比
采样策略可以读取 PowerManager.isPowerSaveMode()、BatteryManager.isCharging() 和电量信息,但采样率没有跨应用通用的固定值。它取决于单次采集成本、指标价值、产品流量、网络条件和服务端统计所需精度。把“低电量”等同于某个固定百分比,会让不同版本的样本分布发生隐式变化。
更合适的做法是把策略输入和动作分开:
| 策略输入 | 可选动作 | 统计要求 |
|---|---|---|
| 省电模式、未充电、计量网络 | 暂停高成本 trace,降低普通样本纳入率,延后大附件 | 记录策略版本和实际纳入概率 |
| 前台关键路径 | 保留低成本计数与时延摘要 | 不因电池状态丢失分母 |
| 充电且网络不计量 | 处理积压批次或执行已授权的高成本诊断 | 仍受温度、存储和用户设置约束 |
| 远程诊断命令 | 在有效期、配额和同意范围内临时调整 | 记录命令来源、有效期与审计 ID |
同一用户或会话是否被采样,宜使用稳定哈希做确定性分配,即让同一输入始终得到相同的采样结果,避免每次事件随机选择造成会话不完整。若再按机型、版本或场景分层,服务端必须知道各层纳入概率,才能计算可比较的总体指标。
2.2 配置与失效保护
采样配置属于 App/APM 的业务配置,不应借用 StatsD 内部的 DeviceConfig 命名空间(namespace,即配置项的分组范围)。配置快照至少包含版本、签名或完整性校验、启用范围、过期时间、每类指标配额和回退值。配置拉取失败时沿用最近一份有效快照;快照过期后回到保守策略,而不是默认扩大采集。
电量 API 也需要处理不可用值和设备差异。策略层不要把某次电量读取失败当成“满电”,也不要让电量信号改变 Crash、ANR 等事件的事实计数;它更适合控制附件、trace 和上传时机。
3. 内存监控:公开 API 与 Android 17 系统策略
App 可见的 PSS、Java/Native 堆和 GC(Garbage Collection,垃圾回收)统计,与 system_server 的 Compaction、Freezer、MemoryLimiter 分属不同观察层。前一层可以通过公开 API 采集;后一层通常只能通过系统 trace(按时间记录系统行为的追踪数据)、dumpsys 诊断输出或源码解释。两层可以做时间关联,但不能把一次 PSS 变化直接标记成某个系统事件。
3.1 Debug.MemoryInfo 进程级采集
下面的示例读取当前进程的 PSS 与 private dirty 分类。private dirty 指进程私有且已被修改、无法直接与其他进程共享的内存页。
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
Debug.MemoryInfo[] memInfoArray =
activityManager.getProcessMemoryInfo(new int[]{android.os.Process.myPid()});
Debug.MemoryInfo memInfo = memInfoArray[0];
// 分类内存统计(单位:KB)
int dalvikPss = memInfo.dalvikPss;
int nativePss = memInfo.nativePss;
int totalPss = memInfo.getTotalPss();
int dalvikPrivateDirty = memInfo.dalvikPrivateDirty;
getProcessMemoryInfo() 返回的 Debug.MemoryInfo 包含 getMemoryStat(String) 方法(API 23+),可按 summary.java-heap、summary.native-heap、summary.code、summary.stack、summary.graphics 等关键字查询子类明细。ART(Android Runtime,Android 运行时)的 GC 行为通过 Debug.getRuntimeStat() 查询(如 art.gc.gc-count、art.gc.gc-time),getMemoryStat() 只覆盖进程级分类,不含 ART 内部堆分区明细。
Android 10 起,普通 App 只能取得与调用者相同 UID 的进程数据;该 API 还会限制采样频率,调用过快时可能返回与上次相同的数据。它适合低频诊断和趋势采样,不适合用紧密轮询近似实时 RSS(Resident Set Size,进程当前驻留在物理内存中的页面总量,不按共享比例分摊)。
3.2 Native 内存与运行时统计
下面的示例分别读取 Native heap 已分配字节数和 ART GC 累计统计。
// Native 堆已分配(API 1)
long nativeAllocated = Debug.getNativeHeapAllocatedSize();
// ART 运行时 GC 统计
String gcCount = Debug.getRuntimeStat("art.gc.gc-count");
String gcTime = Debug.getRuntimeStat("art.gc.gc-time");
getNativeHeapAllocatedSize() 返回 Native heap allocator 统计,不等于进程全部 Native 映射;getRuntimeStat() 在键不受支持时可以返回 null。
3.3 Compaction、Freezer 与 Android 17 MemoryLimiter
Android 17 的 CachedAppOptimizer 仍把 Compaction 和 Freezer 的构建默认值设为开启,但这两项机制并非 Android 17 新增,运行时状态也会受配置与设备条件影响。Android 17 新出现的 MemoryLimiter 则有独立功能开关、/vendor/etc/memory-limiter-config.xml 厂商配置文件和设备内存条件;源码存在不表示所有 Android 17 设备都会启用。
Compaction 状态机与 RSS 节流
CachedAppOptimizer 默认同时开启 Compaction 和 Freezer:
// CachedAppOptimizer.java: android-17.0.0_r1
@VisibleForTesting static final boolean DEFAULT_USE_COMPACTION = true;
@VisibleForTesting static final boolean DEFAULT_USE_FREEZER = true;
enum CompactProfile {
NONE, SOME, ANON, FULL
}
源码中的四个默认节流值分别约束 Some→Some、Some→Full、Full→Some 和 Full→Full 请求,它们是最短间隔,不是固定调度周期。PSS 轮询只能看到采样点之间的结果变化,无法确认变化发生时刻,也无法仅凭曲线判断是否由 compaction 引起;系统级验证需要结合对应 trace、dumpsys 或日志事件。
Freezer 冻结器子系统
CachedAppOptimizer 最终通过 cgroup freezer 冻结缓存进程。cgroup 是 Linux 按进程组控制资源的机制,freezer 是其中的冻结控制;源码状态记录为 opt.setFrozen(true)。它不等同于 Linux 中表示不可中断睡眠的 D 状态。被冻结的 App 进程不能继续运行自己的采样线程,因此无法在冻结期间持续采集自身 RSS。特权观察者或离线 trace 能够观察冻结和解冻事件;普通 App 更适合在恢复后根据生命周期、采样时间间隔和 ApplicationExitInfo 退出记录判断数据是否中断。
MemoryLimiter:内存控制组的内核级节流
Android 17 的 MemoryLimiter 由 system_server 的 Java 控制层与 JNI / Native 监控层组成。启用时,它为目标进程配置 cgroup v2 内存控制组(memcg)的 memory.high、memory.swap.high 等限制,并监听越界事件。该能力要求功能开关开启、代码运行在系统用户标识(system UID)下、存在有效厂商配置且设备内存满足条件。
内核语义以 android17-6.18-2026-06_r6 为锚点。该版本的 cgroup v2 文档 把 memory.high 定义为内存使用节流边界:超过后进程承受回收压力,但内核不会仅因越过 memory.high 就调用 OOM killer(内存耗尽时选择进程终止的内核机制)。MemoryLimiter 在此基础上监控事件,并在匿名内存与交换空间总量(anon+swap)分支由 system_server 另行安排进程终止;两条行为不能合并描述成“内核越界后自动终止”。
对监控采集的三类影响:
- 指标口径不同:PSS 按共享页面比例分摊;cgroup memory charge(记入该控制组的内存用量)、anon(匿名内存)与 swap(换出到交换空间的内存)采用另一套归属规则。PSS 未达到某个数值,不能证明进程没有触及限制。
- 越界后可能先采性能剖析数据再终止:
LIMIT_TYPE_ANON_SWAP分支会触发异常性能剖析(anomaly profiling),并通过KILL_DELAY_MS延迟终止请求,为系统剖析器留出完成时间。这个延迟不属于 App 的轮询或上传时限。 - 退出原因要读记录,不要预设:进程恢复后查询
ApplicationExitInfo,记录实际reason、status、描述和可用 trace,再与 SDK 样本关联。不能把所有 MemoryLimiter 终止预先写成REASON_LOW_MEMORY。
3.4 Android 17 原生内存跟踪架构
两条路径协同工作:
下面的结构展示 framework 汇总 memtrack 图形内存时使用的三个分类。
struct graphics_memory_pss {
int graphics; // 图形内存(SurfaceFlinger 等)
int gl; // GL 内存(OpenGL/Vulkan)
int other; // 其他内存(Ashmem 等)
};
这三个值来自 memtrack HAL 和驱动实现。HAL(Hardware Abstraction Layer,硬件抽象层)把设备相关实现接到 Android 框架;因此,分类精度和可用性取决于设备,不能当作跨机型恒定口径。
下面的片段展示 ProcMemInfo 优先读取 smaps_rollup 并在不可用时回退到 smaps 的意图。
::android::meminfo::ProcMemInfo proc_mem(pid);
::android::meminfo::MemUsage stats;
if (proc_mem.SmapsOrRollup(&stats)) {
pss += stats.pss;
// 失败时自动回退到传统 smaps
}
回退能让旧读取方式继续工作,但不保证各厂商图形内存分类具有相同精度。
| Android 版本 | 内存分类口径 | 说明 |
|---|---|---|
| Android 14–17 | graphics / gl / other 三类 memtrack PSS | android_os_Debug.cpp 中 graphics_memory_pss 与 memtrack_proc_graphics_pss() / memtrack_proc_gl_pss() / memtrack_proc_other_pss() 在这些版本均存在 |
| Android 17 | 同一分类口径,叠加 MemoryLimiter / Freezer 影响 | 三分类不是 Android 17 新增能力;精度取决于 HAL / 驱动实现,这些源码没有给出 ±5% 平台保证 |
3.5 libmeminfo 与 libmemevents 的系统侧边界
Debug.getMemoryInfo() 的 JNI 最终使用 libmeminfo 汇总进程内存。ProcMemInfo::SmapsOrRollup() 优先读取 smaps_rollup,不可用时退回逐 VMA 的 smaps;VMA(Virtual Memory Area,虚拟内存区域)是进程地址空间中属性连续的一段映射。平台组件还可以使用 pagemap、kpageflags 与 kpagecount 做成本更高的 VMA 或 working set(近期活跃内存页集合)诊断。这些 Native 类面向平台代码、厂商组件与 APEX(可独立更新的系统组件包格式)模块集成,不是普通 App 的对象跟踪 API。应用侧继续以 Debug.MemoryInfo、ActivityManager.MemoryInfo 和调试工具为兼容边界。
Android 14 相关源码中的 libmemevents 使用 eBPF 环形缓冲区接收 OOM killer 选中的进程、直接内存回收、内核回收线程 kswapd 与厂商低内存终止机制(LMK)等事件,Android 17 仍保留这条系统路径。eBPF 是在内核受控环境中运行观测程序的机制,环形缓冲区用于把事件按固定容量传给读取方。该路径依赖内核 tracepoint(事件观测点)、BPF 能力、受信任的加载器和 SELinux(Android 强制访问控制机制)策略;设备存在该源码不表示普通 App 可以订阅,也不表示每台 Android 14–17 设备暴露相同事件。线上 SDK 只能保存自身公开指标,平台或设备厂商(OEM)组件才可把这类事件与 App 快照放到同一时间线。
内存监控可以按成本分两层:常态使用聚合快照发现 PSS、nativePss、GC 或后台回落异常;命中诊断条件后,再在受控设备或受信系统组件中使用 VMA 扫描、libmemevents、Perfetto、heap dump(堆转储)或 Perfetto 的 Native 堆分析器 heapprofd。聚合值用于发现趋势,深度工具用于解释来源,二者不能互相替代。
4. Battery Historian 与性能指标整合
Battery Historian 是离线功耗分析工具,它读取 Batterystats 记录的系统电量账目和 bugreport(系统诊断包),再把设备的电池历史转换为 HTML 时间线。时间线适合关联唤醒锁(wakelock,阻止 CPU 进入休眠的持锁记录)、Android 后台任务调度器 JobScheduler 的任务、进程状态和网络活动。Android 官方已经注明该工具不再积极维护;新分析优先考虑 Android Studio 功耗分析器 Power Profiler、AndroidX 宏基准测试 Macrobenchmark 的功耗指标或系统追踪,历史问题和已有流程仍可使用 Battery Historian。
4.1 采集与分析边界
Battery Historian 不等同于 App 线上 APM,也不通过 StatsManager 给普通 App 持续返回性能报告。它分析的是开发者从测试设备导出的系统级记录。采集前应清理或标记历史窗口,复现场景后再生成 bugreport,避免把前一次测试残留事件混入结论。
下面的命令通过 ADB(Android Debug Bridge,Android 调试桥)生成 Battery Historian 可读取的 bugreport。
adb bugreport bugreport.zip
命令完成后,应在本地或受控环境运行官方 Battery Historian 镜像并上传该 zip。bugreport 可能包含设备和用户敏感信息,不宜交给未审计的在线站点。
4.2 与 App 侧监控的配合
App APM 用于持续记录受控的低成本指标;Battery Historian、Power Profiler 和 Perfetto 用于抽样复现与深入归因。线上告警先确定版本、设备和场景,再在可复现设备上采集系统级证据。StatsD config/report 仍是特权路径,不能因为 Battery Historian 能读取 bugreport,就推导出普通 App 能在线读取同一批系统数据。
5. 网络性能指标:App 层自建策略
StatsD 的 atoms.proto 中没有定义通用的 url_pattern、request_bytes、response_bytes 等 HTTP 性能字段。网络性能指标需要 App 通过自建方案或第三方 SDK(如 Firebase Performance)采集。
5.1 URL 模式归一化
App 侧对 URL 做模板归一化,按 endpoint(表示同一类业务接口的路径模板)聚合:
原始 URL: https://api.example.com/v2/user/12345/order/67890
归一化: /v2/user/{id}/order/{id}
Firebase Performance Monitoring 会先匹配项目配置的 custom URL pattern,再回退到自动 pattern。其自定义语法使用单段通配符 * 和后缀通配符 **,并不是 {id}。自建系统也要让规则版本化;规则版本发生变化时,历史数据不会自动变成同一口径。
5.2 HTTP 状态码分组
按 endpoint pattern 统计 DNS 域名解析、连接建立、TLS 加密握手、请求体发送、响应首字节、响应体读取、总耗时和状态码分布。成功率不能统一定义为“非 5xx”:登录接口的 401 可能是预期结果,也可能表示会话刷新故障;下单接口的 409 可能是业务冲突;429 则常常意味着容量或客户端重试策略存在问题。
每个 endpoint 应维护版本化的成功码规则,并单独统计无 HTTP 状态码的 DNS、连接、TLS、取消和超时错误。Firebase Performance 也允许为 URL pattern 自定义哪些响应码算成功。成功口径属于接口语义,不是 HTTP 大类的固定映射。
5.3 传输体积(Payload Size)统计
记录网络层实际可观测的请求与响应字节数,按 endpoint 聚合后输出:
- 响应体大小分布:识别 API 返回数据膨胀与偶发大包
- 总传输量:按 endpoint × 时间段统计
- 缓存命中与重试放大:区分一次逻辑请求对应的网络尝试次数
- 编码信息:记录
Content-Encoding,仅在同时掌握编码前后字节数时计算压缩比
Content-Length 描述消息中的内容长度,其含义还会受编码、分块传输和客户端解码位置影响,不能直接拿它与接收字节数相除后宣称是压缩比。OkHttp 可通过 EventListener 的请求体和响应体事件取得相应字节数;自定义拦截器还要避免读取或复制流式传输的数据体。
6. JankStats 与系统级指标分界
JankStats(AndroidX metrics-performance 库)和 StatsD 在数据分工上有明确边界。Jank 指未在目标时间内完成绘制、造成视觉卡顿的帧;JankStats 会逐帧报告耗时、卡顿判定和当时的 UI 状态。
6.1 职责分工对比
| 维度 | JankStats(端侧) | StatsD(系统级) |
|---|---|---|
| 采集粒度 | 每帧(OnFrameListener 回调) | 按 StatsdConfig 配置聚合周期 |
| 数据内容 | frameDurationUiNanos、isJank、UI 状态 | 订阅的 atom 字段(见 atoms.proto) |
| 运行位置 | App 进程内,AndroidX 库 | statsd daemon 进程 |
| 状态绑定 | 绑定 UI 状态(Activity/Fragment/滚动状态) | 不绑定 UI 状态 |
| 适用场景 | 端侧实时帧诊断,单用户问题复现 | 系统健康指标聚合,版本/设备维度对比 |
| 权限要求 | 无特殊权限 | 按操作分别需要 REGISTER_STATS_PULL_ATOM、DUMP + PACKAGE_USAGE_STATS 或 READ_RESTRICTED_STATS |
6.2 需要自采补充的场景
1. 帧数据与页面状态关联:
StatsD 的系统 atom 不会自动变成当前 App 的页面级 JankStats 数据。JankStats 按 Window 回调每帧数据,并通过 PerformanceMetricsState 附带页面、列表滚动或业务阶段等状态。
下面的示例只在回调中读取必要字段,并在需要跨出回调保存时复制 FrameData。
// AndroidX metrics-performance: 创建 JankStats 时传入 OnFrameListener
JankStats jankStats = JankStats.createAndTrack(window, frameData -> {
// frameData.getFrameDurationUiNanos() — 帧耗时 (ns)
// frameData.isJank() — 是否判定为卡顿
// frameData.getStates() — UI 状态列表 (通过 PerformanceMetricsState 绑定)
long frameDurationNs = frameData.getFrameDurationUiNanos();
boolean isJank = frameData.isJank();
if (isJank) {
FrameData snapshot = frameData.copy();
enqueueJankSnapshot(snapshot);
}
});
JankStats 会复用传给回调监听器(listener)的 FrameData,以减少每帧创建对象的开销,因此不能在回调返回后继续持有原对象。即使复制了对象,回调里也只应入队,聚合、序列化和网络上报要在其他执行窗口完成。
2. 帧耗时组成:
FrameData.frameDurationUiNanos 主要描述 UI 部分,不包含渲染线程 RenderThread 的全部耗时。API 31 及以上的回调对象可能是 FrameDataApi31,做 instanceof 检查后可读取 frameOverrunNanos,用于观察超过帧完成期限(frame deadline)的时间;分析时要记录具体字段,不能把所有值都标成“整帧耗时”。
3. 优化前后验证:
使用相同设备层、刷新率、页面状态和采样规则,对比卡顿帧比例与超时(overrun)分布。isJank 是 JankStats 基于平台信息和内部启发式规则给出的判定;阈值策略升级时要记录库版本,避免把口径变化误认为优化结果。
6.3 实际接入建议
每个需要观察的 Window 创建一个 JankStats 实例,在 Activity 可交互期间启用,并在进入后台时停用。createAndTrack() 要求 Window 已有非空 DecorView,过早调用会抛出 IllegalStateException。
listener 每帧都会收到回调。API 24 及以上通常由 FrameMetrics 使用的线程交付,更早版本可能在主线程交付;两种情况下都要快速返回。采样宜作用于“是否保留或上传完整 frame 明细”,页面级总帧数、jank 数等分母仍需使用一致口径。电池或网络策略变化时,记录策略版本和纳入概率,不写死全项目通用阈值。
7. 内存泄漏检测
标准 Android SDK 不会直接告诉应用“哪个对象泄漏”。LeakCanary 适合在开发和测试构建中检测 retained object(生命周期已经结束、却仍被强引用而无法回收的对象),并执行堆转储与引用链分析;它与低开销的线上内存趋势监控用途不同。
7.1 LeakCanary 2.x 工作流程
LeakCanary 官方建议通过 debugImplementation 引入完整的 leakcanary-android,标准 Activity、Fragment、Fragment view、ViewModel 和 Service 的观察无需业务初始化代码。对自定义生命周期对象,可以在对象不再使用时调用 AppWatcher.objectWatcher.watch()。
工作链分为四步:
ObjectWatcher用弱引用观察已结束生命周期的对象;弱引用不会为了观察而阻止对象被垃圾回收。- 默认等待一段时间并触发 GC;对象仍存在时,将其标为 retained 候选,不会立即断言它就是泄漏根因。
- retained 数量达到配置阈值后生成 HPROF(Java 堆快照文件);生成堆转储会暂停应用。
- LeakCanary 的 Shark 分析器检查引用图,并给出从 GC root(垃圾回收器视为始终可达的根对象)到 retained object 的引用路径。
7.2 架构特点
- 自动安装:库通过 AndroidX Startup 和应用清单(manifest)组件完成默认观察器安装,正常接入不要求在
Application.onCreate()前手动调用配置。 - 分析与停顿分开看:Shark 可以在后台执行分析,但生成堆转储时会暂停虚拟机(VM),不能描述成全程“非阻塞”。
- 结果需要人工解释:retained object 是候选证据;已知库泄漏(library leak)、测试框架持有和应用自身泄漏需要结合引用链分类。
7.3 线上边界
LeakCanary 官方不建议把完整 leakcanary-android 放入发布构建(release build),库还会阻止误装到不可调试 APK。生产环境若只需要 retained object 计数,可单独评估 leakcanary-object-watcher-android,并用远程开关、设备条件和隐私规则限制范围。生产环境生成堆转储会造成明显停顿,也可能包含敏感数据,因此必须采用专门的授权诊断方案,不能照用调试构建的默认配置。
8. 线上采集的边界条件
8.1 性能开销
性能监控本身带来的开销需要控制在可接受范围。
异步采集:
不能把所有采集都机械地切到后台线程:UI 状态和帧回调有明确的线程语义,跨线程读取 View 反而会出错。应在回调线程读取最小且线程安全的快照,再把聚合、压缩、写入存储和上传交给后台执行器。StatsManager.setPullAtomCallback() 使用调用方指定的 executor(任务执行器);JankStats.OnFrameListener 的交付线程随平台机制变化,两者都需要快速返回。
批量处理:
StatsD 的 config 订阅机制按 StatsdConfig 定义的周期聚合原子事件,getReports() 返回的是聚合后的报告。App 自建的指标缓冲也应该按周期批量写入上报通道,而不是每采集一点就发一次网络请求。
分配与所有权: 高频路径使用有界缓冲区和紧凑事件结构,避免无上限字符串、堆栈和标签。对象池是重复使用预先创建对象的机制,但它会增加同步、生命周期和旧数据残留风险,不应默认启用。JankStats 已复用回调对象,需要跨回调保存时按文档复制;其他对象是否复用应以基准测试为准。
开销门禁: 不要把固定 CPU 或内存比例当成所有设备的安全线。对关闭与开启监控的两组构建执行 Macrobenchmark 或可重复的内部基准,比较 CPU 时间、内存分配、启动、帧超时、耗电和上传字节;再按设备层与场景设定预算。SDK 版本或采样策略改变后重新测量,超出预算就减少字段、附件或样本纳入率。
8.2 权限边界
Android 17 的性能监控权限分层明确:
StatsD config/report/query 路径(需要特权权限):
REGISTER_STATS_PULL_ATOM:注册 pull 回调DUMP+PACKAGE_USAGE_STATS:注册 config、读取getReports()READ_RESTRICTED_STATS:使用query()查询 SQL 结果
App 自建指标路径(无需特殊权限):
Debug.MemoryInfo:无需额外权限ActivityManager.getProcessMemoryInfo():普通 App 仅能读取同 UID 进程,且调用频率受限Debug.getRuntimeStat()PowerManager.isPowerSaveMode()
网络上报权限:
INTERNET(清单声明即可)
后台持续上传应使用与任务语义匹配的 API。需要在进程重启后继续、且允许延后的批量上传可以交给 WorkManager(Jetpack 持久化后台任务调度器),并通过网络、电量和存储约束(constraint)声明执行条件;前台页面内的短时采样随页面生命周期停止。应明确写出具体 API 和约束,不能只说“Android 17 限制更严格”。
8.3 约束驱动的采集降级状态机
监控 SDK 不能把后台定时器当成持续时钟。缓存进程(cached app)可能被冻结,WorkManager 只保证在约束允许时获得执行机会,内存压力下继续申请大附件还会放大故障。实现时可以把生命周期、任务回调和系统结果转换成下面的状态机:
| 状态 | 采集 | 本地处理 | 上报 |
|---|---|---|---|
| Foreground(前台) | 用户旅程、帧、网络、低频内存与业务指标 | 有界聚合、采样和脱敏 | 批量发送或入队 |
| UI hidden / Background(界面不可见 / 后台) | 停止周期轮询,只保留必要业务事件 | 释放可重建缓存,写小型状态摘要 | 交给受约束的持久化任务 |
| Cached / Frozen(缓存 / 冻结) | 不假设存在用户态执行机会 | 不补造冻结期间样本 | 等待系统解冻 |
| Restart / Resume(重启 / 恢复) | 查询退出记录、注册性能剖析结果监听、读取待传队列 | 标记不可观测区间,去重并恢复 | 先传关键摘要,再按预算传附件 |
| Memory pressure observed(观察到内存压力) | 停止高成本性能剖析与大对象采集 | 缩小缓冲区、拒绝新批次、保存丢弃计数 | 不因压力立即制造额外网络工作 |
状态转换只能由实际生命周期、任务开始/停止和公开系统结果驱动。App 无法可靠查询“当前是否被冻结”,因为被冻结时本身就没有执行代码的机会;恢复后只能承认这段数据缺失,并记录 unsupported、not_scheduled、frozen_gap、budget_exhausted 等原因,不能用零填充。
9. 数据处理与上报策略
以下策略基于工程实践总结,不绑定 Android 17 StatsD 平台的特定配置项。
9.1 本地缓存机制
有界缓冲: 内存缓冲按字节数与事件数设双重上限,进程恢复后仍有价值的数据写入应用私有目录。配额根据设备存储、事件价值和平均上传能力校准,不用固定时间窗口替代容量管理。
中断后续传: 每个批次带稳定 ID、schema 版本、序列范围和内容摘要;内容摘要用于判断数据是否一致。服务端按批次或事件 ID 幂等确认,即重复提交同一数据时只确认一次;客户端只在收到确认后回收已持久化数据。损坏批次进入隔离区,不能让一个坏文件永久阻塞队列。
9.2 上报策略
- 普通性能批次使用持久化调度,并按产品策略选择
CONNECTED(已有网络连接)或UNMETERED(非计量网络)等 WorkManager 网络约束;大附件还可要求充电和存储空间充足。 - Crash、ANR 摘要与性能批次使用不同优先级,但“高优先级”仍不表示崩溃进程必须现场发网,也不绕过用户的数据设置。
- 失败重试采用带随机扰动的退避:连续失败时逐步延长等待时间,并加入随机偏移,避免大量客户端同时重试。客户端还要尊重服务端
Retry-After指定的再次请求等待时间和自身后台配额。认证失败、schema 不兼容等永久错误进入隔离或升级流程,不能无限重试。 - 使用 WorkManager 的命名唯一任务(unique work)或等价队列键,避免网络恢复时重复创建同一上传任务。网络约束失效时让调度器停止并稍后重试,不在网络状态回调
NetworkCallback中直接发起整批上传。
10. 性能监控最佳实践
10.1 监控范围控制
性能监控可以按信息价值和采集成本分级。下面是职责示例,具体纳入率由测量预算和统计目标决定。
事实计数与分母:
- Crash、ANR 与进程退出记录;
- 启动、会话、页面访问等统计分母;
- 监控 SDK 自身的丢弃、上传和配置状态。
这类数据优先保证口径连续,但仍要受用户同意、隐私规则和技术可用性约束。“优先”不等于所有附件都全量上传。
持续性能摘要:
- 帧率 / 卡顿帧(JankStats)
- 内存使用率和 GC 频率
- 网络时延与错误率
摘要按设备层、版本和场景确定性采样,服务端保留纳入概率。
按需诊断附件:
- Perfetto trace、heap dump、详细行为窗口;
- 高频逐帧明细、完整网络阶段数据;
- 设备与进程快照。
这类数据只在预设配额、远程诊断或代表样本中获取。启动“慢”的阈值应基于 Android Vitals 定义、产品 SLO(Service Level Objective,服务等级目标)和分位数基线,不写成无来源的固定秒数。
10.2 隐私保护
- 敏感数据在写入本地缓冲前完成最小化与脱敏。
- 网络上报使用 TLS,并对服务端身份、证书策略和失败模式进行测试。
- 日志、URL、UI 状态标签(如页面或交互阶段)和业务标签不嵌入原始用户标识或用户内容。
- 需要跨事件关联时使用有明确保留期的假名标识;哈希并不自动等于匿名化。
- heap dump、trace 和原始日志采用更严格的授权、访问审计与删除策略。
10.3 数据生命周期
- App 侧原始事件按字节配额、事件价值和已确认状态回收。
- 服务端原始样本只保留完成定位和重算所需的最短周期,访问必须可审计。
- 聚合数据按趋势比较和合规需求设周期,并记录聚合口径版本。
- 删除流程覆盖主存储、索引、缓存和备份到期策略;用户请求删除时能定位关联数据。
保留时长应由数据分类、业务 SLO、地区法规和成本共同确定,不存在适用于所有项目的固定天数。
11. Android 17 内存监控适配建议
11.1 普通 App 的连续监控
Android 14–17 都可以使用 Debug.MemoryInfo 采集同 UID 进程的低频内存快照,不需要为 Android 17 写一条内容相同的版本分支。采样记录至少包含系统日期时间(wall clock)、不受系统校时影响的单调时间 elapsedRealtime、进程启动序列、前后台状态和最近一次内存压力等级(trim level)。长时间没有样本,只能说明进程没有执行采样;原因可能是冻结、调度延迟、进程退出或任务被取消。
ComponentCallbacks2.onTrimMemory() 是内存压力提示,不能当成每次回收或每次 compaction 的通知。收到回调时记录 level 和业务状态,减少可回收缓存;不要通过高频 PSS 轮询决定运行时核心逻辑。
11.2 Android 17 新字段与退出补偿
API 37 在 ActivityManager.MemoryInfo 新增 freeMem,表示未使用 RAM;availMem 还考虑可回收内存,两者含义不同。应用判断系统是否接近低内存时,优先使用 lowMemory、threshold、availMem 和 trim callback,不要把 freeMem 越低直接解释成异常。
进程重启后,使用 ApplicationExitInfo 核对近期退出原因和时间,再与最近一份内存快照关联。该关联只能说明“退出前观察到什么”;没有系统 trace 时,无法证明 MemoryLimiter、LMKD(Low Memory Killer Daemon,Android 低内存终止守护进程)或某次 Compaction 是根因。
11.3 系统组件与实验室诊断
特权组件可以结合 StatsD、dumpsys、Perfetto、cgroup 文件和 MemoryLimiter 事件分析系统策略。普通 App 不应把读取 FREEZER_EVENT、其他进程 /proc 或 cgroup 控制文件作为生产环境采集方式。需要验证 Android 17 内存策略时,在可控设备上同时采集 App 快照与系统 trace,并以 android-17.0.0_r1 的 CachedAppOptimizer.java、MemoryLimiter.java 和 JNI 实现解释事件。
性能采集部分的延伸阅读
- Android Performance Vitals — Google 官方性能指标定义与最佳实践
- ActivityManager —
getProcessMemoryInfo()的 UID 与频率限制 - ActivityManager.MemoryInfo — API 37
freeMem与availMem的口径 - android.os.Debug — Native heap、PSS、RSS 与运行时统计
- Firebase Performance Monitoring — Firebase 性能监控接入指南,含采样率配置与 URL pattern 归一化
- Firebase custom URL patterns — URL pattern 与成功码配置
- ApplicationExitInfo API — Android 11+ 退出原因归因 API
- JankStats — AndroidX metrics-performance — 帧级卡顿检测库官方文档
- JankStats 使用指南 — 回调线程、对象复用和 UI 状态
- Battery Historian 设置 — 维护状态、bugreport 与本地分析
- WorkManager constraints — 持久化上传的网络、电量与存储条件
- LeakCanary Getting Started — Debug 构建接入边界
- Debug.MemoryInfo — 进程内存使用明细 API
- AOSP Android 17:StatsManager.java
- AOSP Android 17:CachedAppOptimizer.java
- AOSP Android 17:MemoryLimiter.java
- AOSP Android 17:MemoryLimiter JNI
- Android common kernel:android17-6.18-2026-06_r6 cgroup v2
- Android common kernel:android17-6.18-2026-06_r6 memcontrol.c
- StatsD atoms.proto — android-17.0.0_r1 中完整原子定义
全文小结
App 可观测性的核心是让指标、日志与追踪共享可解释的事件模型、时钟、采样概率和版本上下文。端侧以低开销入口、有界缓冲、分级存储和受约束上传保存证据,服务端完成聚合、告警、样本查询与修复验证。具体采集能力仍需按权限和用途分层。
Android 14–17 的性能监控由多组权限和用途不同的 API 组成,可以按三层理解:
- 系统层:StatsD 通过
StatsManager.addConfig()+getReports()向特权 App 提供atoms.proto中系统 atom(如AppStartOccurred(ID 48)、AnrOccurred)的聚合报告;setPullAtomCallback()是特权组件向 statsd 提供 pulled atom 数据的入口。ApplicationExitInfo属于 ActivityManager 的独立公开查询模型,位于 StatsD atom 体系之外。权限边界为:setPullAtomCallback()需要REGISTER_STATS_PULL_ATOM,addConfig()/getReports()同时需要DUMP和PACKAGE_USAGE_STATS,query()需要READ_RESTRICTED_STATS。 - 框架层:AndroidX
JankStats负责帧级实时诊断,Debug.MemoryInfo负责进程级内存采集——两者都不需要特殊权限。 - App 层:电池感知采样率、网络指标聚合、上报策略和缓存管理由 App 自行实现或通过 Firebase Performance 等 SDK 接入。
Android 17 增加了 MemoryLimiter 的设备可选能力和 ActivityManager.MemoryInfo.freeMem 字段。Compaction 与 Freezer 在更早版本已经存在;libmeminfo / libmemevents 仍是系统侧能力,没有为普通 App 增加对象跟踪接口。线上系统应保存一致分母、策略版本、纳入概率和缺失原因,并按前台、后台、冻结、恢复与内存压力状态降级;详细 trace、heap dump 与逐帧数据只在成本和授权条件允许时采集。
参考资料
- Android Vitals:Play 质量指标、核心指标与 28 天评估窗口。
- App startup analysis and optimization:TTID、TTFD 与启动追踪。
- Slow rendering:慢帧、冻帧、ANR 与 FrameTimeline。
- Firebase Performance Monitoring:自动追踪、自定义指标、筛选属性与用户数据说明。
- SystemClock API:现实时间、运行时间与经过时间的语义。
- ProfilingManager API:性能剖析类型、限流、回调与结果边界。
- ProfilingTrigger API:Android 16—17 的系统触发类型。
- Android privacy checklist:数据访问审计、可重置标识、告知与敏感日志要求。
- OpenTelemetry logs:使用 trace/span 关联信息连接日志与追踪。
- AOSP
SystemClock.java@ Android 17:平台时钟实现边界。 - AOSP
ProfilingManager.java@ Android 17:线上性能剖析请求、触发器与限流边界。