Matrix、btrace 与 Tracing SDK
androidx.tracing(记录程序运行时间线) 提供应用侧的埋点入口,btrace/RheaTrace 3.x 用运行时 Hook 和同步抓栈补足方法执行现场,Matrix 再把 Trace、资源和稳定性能力组织成客户端框架。三者处在不同抽象层,接入方式和 Android 版本兼容性要分别验证。
应用 Trace API 与埋点边界
线上指标能告诉我们哪个页面慢,要解释慢在哪一段代码,就得靠应用自己在系统 trace 里留下业务语义。这一节从 androidx.tracing 的埋点入口讲起:哪些位置值得标记、三种事件各管什么、命名怎么定,以及这些 slice 在 Perfetto 里怎么读。
它给系统 trace 增加业务语义
系统 trace 把系统执行活动按时间排列,线程调度、Binder 进程间通信、I/O、渲染与锁等待都在里面;但它自己分不清某段应用代码是在解析首页数据,还是在提交支付结果。androidx.tracing 2.0.0 保留的经典 Trace.* / trace {} API 补的就是这一层:给应用代码区间加上稳定名称,让 Perfetto、Android Studio System Trace 和 Macrobenchmark(在真实 App 进程外驱动启动、滚动等场景的基准测试) 采集的 trace 里出现应用自定义 slice(时间线中有起止时间的区间)。
这里我们要把三件事分开:
- trace slice 是有开始和结束的命名时间片,记录区间何时发生、落在哪条 track(时间线轨道) 上。
- 性能指标负责统计分位数、失败率、慢帧率等聚合结果。
- 根因分析要把 slice 与线程状态、CPU 调度、Binder、I/O、帧时间线放在一起读。
所以经典 androidx.tracing 事件适合回答“慢发生在哪个业务阶段”;上传数据和解释慢因是另外两件事,要靠别的环节。读 slice 时长时还要记住:它记的是墙上时间(wall time),也就是现实中从开始到结束经过的时间,CPU 执行、锁等待、I/O 等待和被调度器换出的时间都可能混在里面;线程实际占用 CPU 的那部分,要另外看 CPU time(线程实际占用 CPU 的时间)。
核验基线
截至 2026-10-26,最新发布状态与本文固定机制分析版本分别如下:
| 对象 | 核验锚点 | 已核实的边界 |
|---|---|---|
| Android 平台 | Android 17 / API 37 / android-17.0.0_r1 | android.os.Trace 的同步 section、异步 section、counter 与 Java Native Interface(JNI)路径 |
| AndroidX 当前稳定线 / 固定分析线 | 当前 2.0.5;下文固定 2.0.0 | 2.0.5 发布于 2026-10-21;2.0.0 发布于 2026-08-12,其 Android 制品最低 API 23,包含经典 API 与进程内 tracing API |
| AndroidX 兼容线 | androidx.tracing:tracing:1.3.0 | 发布于 2025-04-23;Android 变体最低 API 21,供仍需覆盖 API 21-22 的应用使用 |
| Tracing Wire | androidx.tracing:tracing-wire:2.0.0 | 给 2.0 进程内 API 提供 Perfetto 格式的 TraceSink 实现 |
| Tracing Perfetto | androidx.tracing:tracing-perfetto:1.0.1 | 独立的 Perfetto SDK 接入组件,不等同于经典 Trace.beginSection(),也不等同于 tracing-wire |
| AndroidX Benchmark | 当前稳定版 1.5.0;预发布版 1.6.0-alpha01;本文固定研究 1.4.1 的指标实现 | 1.5.0 于 2026-09-09 稳定;1.5 线可采集并合并目标包的进程内 trace |
| Kernel | android17-6.18-2026-06_r6 | 仅在联读 CPU 调度、唤醒等 kernel(操作系统内核)事件时作为源码锚点 |
API 23+ 项目当前稳定坐标为 androidx.tracing:tracing:2.0.5;下文的源码、签名和 SHA 检查固定在 2.0.0,不把新版行为直接套到旧制品上。如果应用仍需运行在 API 21-22,可继续使用 1.3.0 兼容线;下面保留的是这种低版本依赖写法。
dependencies {
implementation("androidx.tracing:tracing:1.3.0")
}
固定 2.0.0 AAR 的内联函数字节码目标为 JVM 11;消费模块的 Kotlin jvmTarget 至少要匹配 11,并与 Java 编译目标一致。这是构建字节码目标,与 Android API 下限分属两件事,别混写。
Gradle 会为 Android 目标选择对应版本的 tracing-android variant(针对 Android 的发布变体),这里的 artifact 指 Maven 仓库中的发布组件。自 1.3.0 起,原 tracing-ktx 实现已并入主 artifact;用 1.3.0 或 2.0.0 都不必再单独添加 tracing-ktx。
哪些位置值得手动标记
判断一个位置值不值得打点,标准是这一段区间是否有助于缩小排查范围,函数本身重不重要倒是其次。通常值得长期保留的点包括:
- 启动:配置读取、依赖初始化、首屏数据准备、首个可交互状态。
- 列表:diff(新旧列表的差量) 计算、分页合并、批量 bind(把数据绑定到列表项)、预取和图片解码。
- 存储:关键数据库查询、事务提交、缓存反序列化。
- 交互:页面转场准备、支付提交、播放器 prepare、相机初始化。
- 跨线程任务:从业务请求开始,到后台工作完成或 UI 提交结束的逻辑跨度。
- Native 重任务:编解码、图像处理、渲染资源准备和大块数据转换。
下面这些位置通常不该逐次打点:
onDraw()、像素循环、序列化字段循环等高频内层。- 已被一个清晰父 slice 覆盖、且拆开后没有诊断价值的小函数。
- 只为临时观察而加入、问题结束后不再使用的细碎标记。
- 名称必须携带用户 ID、URL、搜索词或订单号才看得懂的事件。
一条长期 slice 应当对应一个稳定业务阶段。需要知道循环处理了多少项时,用 counter 或离线统计;不要为每一项创建不同名称的 slice。
三种事件:同步 slice、异步 slice、counter
同步 slice:同一线程上的嵌套区间
同步 section 遵循线程内的栈语义:beginSection() 与 endSection() 必须在同一线程配对,嵌套顺序后进先出。写 Kotlin 时我们优先用 trace {},它借 finally 关闭区间。
下面的例子用于区分主线程上的列表提交和其中的 diff 计算。
import androidx.tracing.trace
fun submitFeed(items: List<FeedItem>) {
trace("Home#submitFeed") {
val result = trace("Home#calculateDiff") {
calculateDiff(items)
}
adapter.dispatchUpdates(result)
}
}
在 Perfetto 中,到调用 submitFeed() 的线程轨道上找 Home#submitFeed;Home#calculateDiff 是它的子 slice。父 slice 很长而子 slice 很短时,剩下的时间花在 diff 之外,归因时把这两部分分开算。
Java 代码可以直接调用 Trace.beginSection() / endSection(),但必须用 try-finally 保证异常路径也闭合。
import androidx.tracing.Trace;
void decodeThumbnail(byte[] encoded) {
Trace.beginSection("Image#decodeThumbnail");
try {
decoder.decode(encoded);
} finally {
Trace.endSection();
}
}
如果这段方法在解码线程执行,slice 就出现在该解码线程,而不会自动出现在主线程。漏掉 finally 会破坏当前线程后续 section 的嵌套关系。
异步 slice:跨线程、跨挂起点的逻辑跨度
异步 section 靠“名称 + Int cookie”配对:cookie 是区分同名并发任务的整数标识,开始和结束可以落在不同线程,也不要求按栈嵌套,只要同名且时间重叠的任务使用不同 cookie。一次跨线程加载、一次远端请求、一段会挂起的协程任务,都适合用它表达。
AndroidX Tracing 2.0.0 保留了 suspend traceAsync();suspend 表示这个 Kotlin 函数可以挂起后再恢复。下面的例子让一个异步 span(跨线程表达同一逻辑工作的区间) 覆盖请求、取得数据和调用列表提交接口的过程,线程内部的工作仍由 I/O 线程和主线程各自的同步 slice 记录。
import androidx.tracing.trace
import androidx.tracing.traceAsync
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private val nextTraceCookie = AtomicInteger(1)
suspend fun loadFirstFeed(): List<FeedItem> {
val cookie = nextTraceCookie.getAndIncrement()
return traceAsync("Home#loadFirstFeed", cookie) {
val items = withContext(Dispatchers.IO) {
trace("Home#requestFirstFeed") {
repository.loadFirstFeedBlocking()
}
}
withContext(Dispatchers.Main.immediate) {
trace("Home#submitFirstFeed") {
adapter.submitList(items)
}
}
items
}
}
traceAsync() 的 2.0.0 源码仍在 try-finally 中结束异步 section,所以正常返回、异常和协程取消都会走关闭路径。Perfetto 里,Home#loadFirstFeed 给出的是整个墙上时间跨度;Home#requestFirstFeed 要到 I/O 线程的轨道上查看,Home#submitFirstFeed 要到主线程的轨道上查看。整体跨度之外,线程内的工作还得分别回到这两条轨道确认。 adapter.submitList() 可能异步计算 diff 和应用结果,因此这里的终点只是提交调用返回,不是列表提交完成或首屏可见。若要测量后者,应另定义完成信号和 async 生命周期;被后续列表取代的提交,其 commit callback 可能不执行,把它当所有路径唯一的关闭点,这些提交就可能漏关。

图:异步跨度覆盖请求、线程切换和提交接口返回,线程内工作另作同步标记。 submitList返回只到提交调用为止,diff完成与画面可见要另行确认;关闭点须覆盖成功、失败、取消和拒绝。
同步 trace {} 只适合包住不含挂起点的代码;一旦代码里有 delay()、withContext() 或其他挂起点,协程恢复后可能换线程,而同步 section 要求 begin/end 位于同一线程。即使总恢复到同一线程,挂起期间其他协程也可能插入 section,破坏栈嵌套;同线程恢复仍不足以保证同步 section 的正确性。
线程池、回调、消息队列也是同样的规则:
- 提交任务前生成 cookie 并开始异步 section。
- 在任务的唯一终态关闭同名、同 cookie 的 section。
- 提交被拒绝、任务取消、回调不再投递时也要关闭。
- 每条实际执行线程再用同步 section 标记执行区间;其中的 CPU 工作仍要联读调度数据。
如果现有抽象保证不了“每个 begin 恰好对应一次 end”,我们先修正任务生命周期,再加 async trace;用多个 catch 和回调分别关闭,很容易漏关或重复关闭。
Counter:观察随时间变化的值
counter 记录某个时刻的数值,一段耗时要看 slice。队列深度、缓存条目数、在途请求数这类低频状态适合用它观察。
下面的例子在解码队列变化时写入固定名称的 counter。
import androidx.tracing.Trace
fun onDecodeQueueChanged(pendingCount: Int) {
Trace.setCounter("Image#decodeQueueDepth", pendingCount)
}
在 Perfetto 中搜索 Image#decodeQueueDepth,应当看到一条随时间变化的 counter 轨道。AndroidX 1.3.0 只有 Int 参数;2.0.0 保留这个重载,并增加 API 29+ 可用的 Long 重载。Android 17 平台 android.os.Trace.setCounter() 接受 long。写公共库时要注明所依赖的 AndroidX 版本和最低 API,别把三者混写成同一个签名。
命名是一份可维护的诊断协议
建议统一使用 Module#Action,必要时再加一个固定层级,如 Module#Stage.Step。名称描述业务阶段,不绑定类名、方法名或当前架构。
| 场景 | 推荐名称 | 不推荐 | 原因 |
|---|---|---|---|
| 应用启动 | Startup# | ConfigRepositoryImpl | 实现重构后仍可比较 |
| 首页首屏 | Home# | feed_ | 避免高基数,即避免名称数量随请求或内容数量增长 |
| 详情页 | Detail# | Detail# | 不写业务 ID |
| 支付提交 | Checkout# | pay/ | 不写用户数据 |
| 图片解码 | Image# | 完整图片 URL | 避免隐私与超长名称 |
| 数据库查询 | Database# | 完整 SQL | 防止敏感参数进入 trace |
团队可以按下面四条规则评审名称:
- 稳定:相同阶段跨版本保留同名,指标才能纵向比较。
- 有界:动态变化只能来自少量固定枚举,如
thumbnail、preview、full。 - 可定位:名称能指向业务阶段,但无需暴露类和方法实现。
- 无敏感数据:trace 文件可能被分享、上传或附在缺陷单中。
平台 Trace.beginSection() 限制名称最多 127 个 Unicode code unit。这里的 code unit 是 Java String.length 使用的 UTF-16 计数单位,一个可见字符不一定只占一个单位。采集已启用时,直接向平台 API 传入超长名称会抛 IllegalArgumentException;AndroidX 1.3.0 与 2.0.0 都会先截断到 127。截断仍可能让不同名称变得相同,所以名称在设计时就要保持短而稳定。|、换行和空字符是底层保留字符,命名时避开。
Android 17 的同步调用链
以 android-17.0.0_r1 为准,AndroidX 2.0.0 经典 trace {} API 的同步调用链可以概括为:
androidx.tracing.trace("Home#submitFeed")
-> androidx.tracing.Trace.beginSection()
-> android.os.Trace.beginSection()
-> nativeTraceBegin(TRACE_TAG_APP, name)
-> tracing_perfetto::traceBegin(tag, name)
这条链交代了两件事:
- AndroidX 负责兼容封装、异常安全辅助和名称截断,平台仍会检查
TRACE_TAG_APP是否启用。 - Android 17 的
android_os_Trace.cpp已把 JNI(Java Native Interface) 调用交给tracing_perfetto,旧版“每次直接向trace_marker执行一次write()”的概括到这里就对不上了。

图:固定A17把经典同步标记从AndroidX经平台JNI交给tracing_perfetto。 图表示调用关系,没有声明每次直接write(trace_marker)或固定纳秒开销。
同理,旧版实现测出的固定纳秒开销、锁竞争方式或系统调用次数,推不到 Android 17 上。要判断打点是否影响热路径(高频或对延迟敏感的执行路径),就在目标构建、目标设备和实际采集配置上运行 Microbenchmark(针对一小段代码的重复性能测试),同时比较 trace 开启与关闭两种状态。
版本与采集可见性
Trace.isEnabled() 为 true 需要同时满足两件事:当前存在能接收应用事件的 trace 会话,并且该进程被允许写 app trace。Android 12 以后应用 tracing 默认放行,但这只解决权限,系统并不会因此一直在后台记录。
下表中的 non-debuggable 表示应用关闭调试能力,profileable 表示应用允许 shell 等受控工具进行性能采集。
| 平台范围 | 非 debuggable 进程的允许条件 | AndroidX 经典 async/counter 路径 |
|---|---|---|
| API 21-22 | 只能使用 1.3.0;应用启动早期调用 Trace. | 反射平台隐藏的 asyncTraceBegin、asyncTraceEnd、traceCounter |
| API 23-28 | 1.3.0 或 2.0.0;应用启动早期调用 Trace. | 反射同一组平台隐藏方法 |
| API 29-30 | manifest 设置 <profileable android:shell,或调用 forceEnableAppTracing() | 调用 API 29 新增的公开平台方法 |
| API 31-37 | 默认允许;显式设置 <profileable android:enabled 或 android:shell= 会限制该能力 | 调用公开平台方法;forceEnableAppTracing() 在这一范围无操作 |
固定检查的 tracing-android:2.0.0 制品 minSdkVersion 是 23;兼容线 1.3.0 的下限是 21。两版源码都保留了更低平台的兼容分支说明,但发布物 manifest 会先把依赖范围限住,别据此宣称 artifact 支持 API 18。
采集侧也要包含目标应用:
- Macrobenchmark 会自动采集目标应用的自定义 trace point。
- 旧 systrace 命令需要用
-a <package>指定应用。 - 自定义 Perfetto 配置要把目标包加入相应的 atrace app 配置;atrace 是 Perfetto 用来接收 Android 系统与应用 trace point 的数据源之一。
API 29 以前的 forceEnableAppTracing() 和 async/counter 兼容分支依赖反射,可能受隐藏 API 策略或厂商实现影响;表中有入口不保证目标机上可用。
正式性能结论宜来自 non-debuggable、profileable 且接近发布配置的构建。debuggable 包的调试设施、编译选项和运行时行为可能改变时间分布。
开销控制:少而稳定,动态内容延迟计算
打点开销没有适用于所有设备的固定数字,它随是否采集、平台实现、事件类型、名称处理、缓冲区压力和调用频率变化。工程上我们用下面几条约束它:
- 把 section 提到循环外,观察一次批处理,不给每个元素打点。
- 优先使用静态名称;只有诊断需要且取值空间很小时才构造动态名称。
- 用
Trace.isEnabled()避免只为 trace 做昂贵计算。 - 用同一套 Macrobenchmark 或 Microbenchmark 对比打点前后,关注 p50/p95 与 trace 丢失情况;p95 表示 95% 的观测值不超过该数值。
- 父 slice 已足够定位时,删除重复的子 slice。
AndroidX 2.0.0 保留 lazy label(延迟计算名称)重载,下面的例子只在 tracing 启用时才构造带固定枚举的名称。
import androidx.tracing.trace
fun bindCard(card: Card, viewType: CardViewType) {
trace(lazyLabel = { "Feed#bind/${viewType.traceName}" }) {
renderer.bind(card)
}
}
traceName 应是受控枚举,如 text、image、video,别返回 item ID。该 slice 位于执行 bindCard() 的线程;如果它每帧出现很多次,应改为在批量 bind 外只保留一个父 section。
Native 标注:与 Java 使用同一套名称
NDK(Native Development Kit) 的 <android/trace.h> 从 API 23 提供同步 section,API 29 增加异步 section 与 counter。Native 同步 section 同样要求在同一线程正确嵌套。
下面的 RAII(Resource Acquisition Is Initialization,以对象生命周期管理资源) 封装保证 early return(函数提前返回) 或 C++ 异常都不会漏掉 ATrace_endSection()。
#include <android/trace.h>
class ScopedTrace final {
public:
explicit ScopedTrace(const char* name) {
ATrace_beginSection(name);
}
~ScopedTrace() {
ATrace_endSection();
}
ScopedTrace(const ScopedTrace&) = delete;
ScopedTrace& operator=(const ScopedTrace&) = delete;
};
void DecodeFrame(const EncodedFrame& frame) {
ScopedTrace trace("Video#decodeFrame");
DecodeOneFrame(frame);
}
Perfetto 会把 Video#decodeFrame 放在执行 DecodeFrame() 的 native 线程轨道上。Java 外层若有 Video#processFrame,两者只有在同一线程同步调用时才形成自然嵌套;JNI 前后换了线程时,要用 async span 或 Perfetto flow(连接两个 slice 的箭头) 表达逻辑联系。
ATrace_isEnabled() 可以帮我们跳过只服务于 tracing 的昂贵名称计算。调用它时保持业务逻辑原样:无论 tracing 状态如何,业务逻辑都必须执行。
与 Perfetto SDK、btrace、Macrobenchmark 的分工
这些工具都能产出或消费 trace 信息,但抽象层级不同。
| 工具 | 适合的任务 | 关键边界 |
|---|---|---|
经典 Trace.* / trace {}(2.0.0;API 21-22 用 1.3.0) | 长期保留少量业务 section、async span、counter | 写入系统 trace;不上传、不聚合;官方仍推荐用于低频事件 |
Tracer + tracing-wire:2.0.0 | 进程内 Perfetto 格式 trace、分类、metadata(键值附注)、flow 与协程上下文传播 | 最低 API 23;要设计 TraceDriver、TraceSink、文件和丢事件生命周期 |
tracing-perfetto:1.0.1 | 通过独立 Perfetto SDK 组件记录应用事件 | 有自己的 native binary 与启用握手(handshake),与经典 API、tracing-wire 都是不同接入路径 |
| btrace / RheaTrace | 专项抓取Hook附近的方法栈样本与上下文 | 数据量和侵入面更大,适合定向诊断 |
| Macrobenchmark:当前 1.5.0;固定指标分析 1.4.1 | 在测试侧启动、交互并采集 trace;1.5 线可合并进程内 trace | 下文指标签名、实验标记和默认 mode 按固定 1.4.1;升级后重新核对 |
| Perfetto UI / Trace Processor | 人工或 SQL 联读系统与应用证据 | 工具负责展示和查询,不替应用补业务语义 |
AndroidX 2.0.0 的新 Tracer API 与经典 Trace.beginSection() 是两条用途不同的接口面。经典 API 仍写系统 trace,也没有被弃用。
新路径上,核心模块声明 AbstractTraceDriver、AbstractTraceSink 和 Tracer;固定 tracing-wire:2.0.0 提供 androidx.tracing.wire.TraceDriver / TraceSink 具体实现。driver 持有 tracer 并管理生命周期,sink 决定事件如何序列化和输出;这两组同名接口一组在核心抽象层、一组在 wire 实现层,读源码时要分开对号。筛选事件靠 category(要启用的一组 trace 事件),metadata 给事件附加键值信息,instant event 表示没有持续时间的瞬时点;counter、Perfetto flow 和 traceCoroutine() 也都在支持之列。
Android Studio System Trace 目前不会直接采集这条进程内路径,Benchmark 1.5 线的 PerfettoCapture / PerfettoTraceRule 可以采集并在事后合并目标包的进程内 trace。本文固定的 2.0.0 用于机制复核;当前发布说明记录了 2.0.4 的 flush 竞态修复,以及 2.0.5 的 pool 管理竞态/潜在死锁修复,接入时应评估当前稳定线。仍要设计采集启动方式、文件生命周期、丢事件策略、体积与工具兼容性。
固定分析版本 Benchmark 1.4.1 的 TraceSectionMetric(sectionName) 仍是实验 API,默认使用 Mode.Sum,输出全部匹配 section 的总时长与次数;只有显式选择 Mode.First 才取第一条。这里的 mode 是把多条同名 section 汇成指标的方式。它默认只读取目标包。用于基准指标的 section 要把生命周期和 mode 写清楚,并用生成的 trace 复核所选区间。
与线上指标建立同名索引
经典 androidx.tracing 不会把事件送到线上;2.0 进程内 API 也只负责本地记录,上传仍需应用自行设计。我们可以用一张受版本控制的阶段表,让 Perfetto slice、JankStats context 和 APM(应用性能监控)span 指向同一个业务语义。
| 业务阶段 | Perfetto slice | JankStats state | APM 事件 |
|---|---|---|---|
| 应用初始化 | Startup# | phase= | startup. |
| 首屏数据 | Home# | phase= | home. |
| 首页提交 | Home# | phase= | home. |
| 详情内容可见 | Detail# | phase= | detail. |
| 支付提交 | Checkout# | phase= | checkout. |
映射表只保存稳定阶段。请求 ID、trace ID 等关联字段可进入受控的日志或 APM(Application Performance Monitoring,应用性能监控) 字段,别拼进 Perfetto slice 名称。
JankStats 的 PerformanceMetricsState 是状态,不是瞬时事件。进入阶段时写入,离开阶段时移除或替换;若忘记清理,后续帧会继续携带过期上下文。线上看到 phase=first_feed_submit 的慢帧后,可以在线下复现并搜索 Home#submitFirstFeed,再用主线程与帧时间线确认慢因。

图:同一阶段词典关联slice、JankStats上下文和APM事件,状态在离开阶段时清理。 同名语义帮助复现和检索,线上样本与线下trace依旧是两份独立证据。
在 Perfetto 里的阅读顺序
拿到一份 trace 后,我们按固定顺序过一遍:
- 搜索精确名称:确认目标 slice 是否出现,以及出现了几次。
- 确认轨道:同步 slice 看所在的 thread track(每条线程的时间线轨道);异步 span 看它连接的逻辑轨道和两端事件。
- 展开嵌套:父 slice 的墙上时间只能说明范围,子 slice 才能继续分段归因。
- 对齐帧或启动窗口:慢帧对齐 FrameTimeline(记录每帧生命周期的系统时间线),启动对齐进程创建、activity launch 和首帧。
- 查看线程状态:Running 表示正在占用 CPU,Runnable 表示可运行但未获 CPU,Sleeping 常见于等待;再结合锁、Binder、I/O 和 wakeup(线程被唤醒) 事件往下查。
- 检查 gap:gap 是两个业务 slice 之间的空白,可能来自等待,也可能来自没打标记的代码,先别凭空白下结论。
需要批量核对同名 slice 时,可以在 Perfetto Trace Processor(SQL 查询引擎)中执行下面的查询。
SELECT
s.ts / 1e6 AS start_ms,
CASE WHEN s.dur >= 0 THEN s.dur / 1e6 END AS duration_ms,
s.dur AS raw_duration_ns,
p.upid, p.pid, p.name AS process_name,
t.tid, t.name AS thread_name
FROM slice AS s
JOIN thread_track AS tt ON s.track_id = tt.id
JOIN thread AS t ON tt.utid = t.utid
LEFT JOIN process AS p ON t.upid = p.upid
WHERE s.name = 'Home#submitFirstFeed'
ORDER BY s.ts;
这条查询只列出 thread track 上的同步 slice。ts 是 trace 时钟域中的时间戳,不是 Unix 墙钟日期。ts 和有效 dur 的单位为纳秒,除以 1e6 后得到毫秒;未闭合的 dur = -1 在查询中保留原值并把 duration_ms 留空,不输出负耗时。多进程或多应用 trace 应另按已确认的 upid 筛选;utid 是 Trace Processor 内部的线程关联键,不等同于 Linux tid。异步 slice 可能落在其他 track,这条查询查不到它,先别当成事件没有被采到。dur = -1 通常意味着区间未闭合,或采集结束时仍在进行,这时要回查 begin/end 生命周期。
CPU 调度与唤醒的线索来自系统和 kernel trace 数据源。查这类实现时,kernel 侧以 android17-6.18-2026-06_r6 为准;应用自定义 slice 的 Java/JNI 行为以 android-17.0.0_r1 为准,两套源码对号入座,别混着用。
常见错误与修正
| 错误 | trace 中的表现 | 修正 |
|---|---|---|
| 名称带 ID、URL、SQL 或用户数据 | 名称爆炸,无法聚合,并有泄露风险 | 固定名称;动态字段进入受控日志或 APM |
Java begin/end 没放进 try-finally | 后续 slice 嵌套到错误父区间,或出现未闭合区间 | Kotlin 用 trace {};Java 强制 try-finally |
| 同步 section 跨协程挂起点 | begin/end 可能跨线程,线程栈语义失效 | 用 suspend traceAsync() 表示逻辑跨度,线程内工作另加同步 slice |
| async span 复用并发 cookie | 同名任务互相错配 | cookie 在同名重叠任务间保持唯一 |
| 只标任务提交 | 只看到 execute()、post() 的提交耗时 | 在执行线程标记工作,并用 async span 串起生命周期 |
| 每个循环元素都打点 | 视图充满碎片,缓冲区压力上升 | 把 section 移到循环外;数量用 counter |
| 把 slice 时长当 CPU time | 把等待和调度延迟误算为执行耗时 | 联读 thread state、sched(调度事件)、Binder、I/O |
| 认为 API 31+ 一直在采集 | 没抓 trace 时仍构造昂贵名称 | 用 lazy label 或 Trace.isEnabled() |
把 2.0 Tracer 当成经典 Trace.* 的直接替换 | 采集和文件生命周期设计缺失 | 将进程内 tracing 作为独立路径评估;低频系统事件可继续用经典 API |
| 把 trace 当线上统计系统 | 没有分位数、采样和上传策略 | 指标交给 JankStats/APM/Benchmark,trace 用于解释现场 |
源码核验记录
相关结论来自官方文档、AOSP 源码和已发布 artifact 的交叉核对:
- AndroidX Tracing 发布说明与 Google Maven metadata:当前稳定 2.0.5,固定研究 2.0.0、1.3.0 兼容线,以及 Tracing Perfetto 1.0.1。
android-17.0.0_r1的android.os.Trace:127 code unit 限制、同步同线程约束、异步 name/cookie 配对和公开 API。android-17.0.0_r1的android_os_Trace.cpp:JNI 进入tracing_perfetto::traceBegin()、traceEnd()、traceAsyncBegin()等实现。tracing-android:2.0.0sources:AndroidX 名称截断、API 29 前反射兼容路径、forceEnableAppTracing()、Longcounter 重载与 suspendtraceAsync()的finally配对。- AndroidX Tracing 2.0 发布说明与
tracing-wirePOM:进程内Tracer、Perfetto 格式TraceSink、Studio System Trace 的采集范围,以及 Benchmark 1.5 的事后合并能力。 - AndroidX Benchmark 发布说明:当前稳定 1.5.0、预发布 1.6.0-alpha01;1.4.1 的实际 sources 用于复核本文指标实验标记和 mode 语义。
- Android NDK tracing reference:同步 API 从 23 可用,异步与 counter 从 29 可用。
为便于复核,下载件的 SHA-256 如下:
| 文件 | SHA-256 |
|---|---|
tracing-android-2.0.0.aar | b21800b83ed0e3e2 |
tracing-android-2.0.0-sources.jar | 3e8834bc14486803 |
tracing-android-1.3.0.aar | f59782bbe3470a6b |
tracing-android-1.3.0-sources.jar | a3a9c1b46707208a |
tracing-android-2.0.0-beta01.aar | a5596bdc3870afce |
tracing-android-2.0.0-beta01-sources.jar | 3e8834bc14486803 |
2.0.0 与 beta01 的 sources JAR 内容哈希相同,已发布 AAR(Android 库归档) 的哈希不同;表中同时保留两组值,避免复核历史文章时把两个产物混为一谈。
btrace 运行时 Hook 与方法采样
显式埋点适合关键业务区间;想看到更多方法现场时,可以评估 btrace 的运行时采样。我们能采到什么,由 Hook 覆盖、采样间隔和 ART 私有接口兼容性共同决定;btrace 2.0 那套编译期全量插桩的模型在这里已经不适用。
官方产物在 Android 17 上的兼容现状
btrace 是字节跳动开源的跨平台 tracing 项目,Android 侧代码仍沿用 RheaTrace3、rhea-inhouse 等历史命名。它把应用线程的 Java 方法栈样本转换成 Perfetto protobuf(Protocol Buffers 编码的 trace 数据),并在 perfetto 模式下与系统 trace 合并,方法现场和调度、Binder、I/O、渲染时间线就落在同一条时钟轴上。
截至 2026-10-26,仓库最新 tag 和 GitHub release 仍是 v3.1.0,对应 2026-06-09 的 commit 2ae621f2d84ad54b2811d675d86ba2815006adb3。这个 tag 加入了 HarmonyOS,Android 子工程的 POM_VERSION_NAME、README 依赖和脚本表仍停在 3.0.0。Maven Central 元数据显示 Android 产物最新是 3.0.1-alpha01,稳定版仍是 3.0.0。仓库 tag 与 Android Maven 版本是两套发布状态,Android 依赖没有 3.1.0 这个版本可写。
当前开源 Android 实现离 Android 17 / API 37 兼容还有一段距离。我们把原因分成源码和产物两端来看:
- btrace 写死了旧 C++ 符号
_ZN3art12StackVisitor9WalkStackILNS0_16CountTransitionsE0EEEvb,其中只有一个模板参数且枚举值为 0。按固定 Android 17stack.h的枚举声明,0 对应CountTransitions::kYes,不是kNo。Android 17 的StackVisitor已给WalkStack增加第二个模板参数。android-17.0.0_r1显式实例化的新 mangled symbol(编译器编码后的符号名)与 btrace 写死的旧名称不同。若目标 ART 二进制没有兼容的旧符号,核心StackVisitor::init()会返回 false,后续方法采样失败。固定源码说明了符号和 ABI 风险;逐台的厂商 ART 符号表还要单独核对。 - 上游只声明 Java 对象创建监控未适配 Android 15+,这不是唯一的高版本风险。同步抓栈、JNI、锁、GC(垃圾回收)、park/wait 等路径都会 Hook 或解析 ART 私有实现,没有公开 SDK/NDK ABI(应用二进制接口) 的稳定保证。
- Android 子工程仍用 compileSdk 30、targetSdk 30、AGP(Android Gradle Plugin) 4.1.0、NDK
21.1.6352462。这组版本只描述上游构建环境,证明不了 API 37 行为。 - 静态检查实际
3.0.1-alpha01和3.0.0AAR 中 arm64-v8a、armeabi-v7a 的librheatrace.so(ELF 共享库)及随包libc++_shared.so,ELFLOADsegment(加载段) 都是2**12,即 4096 字节对齐。npth_dl.c还用0xfff/0x1000计算 ELF section 的mmap(内存映射) 偏移。这些是 16 KB 兼容性整改项。静态检查未运行目标设备,也未检查最终 APK 的 ZIP 对齐;所有兼容模式下的加载结果,要留到目标设备上验证。
在 Android 17 设备上,我们可能仍会得到一份只有系统轨道的 .pb,这样一份文件证明不了 btrace 方法采样成功。采用方需要维护 source fork(自有维护分支),修复 ART 符号、16 KB ELF 对齐和页大小假设,再把“采样记录数大于零、方法能正确符号化”设为自动验收条件。符号化就是把方法指针或地址还原成可读的方法名。团队没有维护私有 ART Hook 的能力时,可直接使用 Perfetto、androidx.tracing、Perfetto SDK、simpleperf 或 Android Studio Profiler。
3.0 的接入侧没有 Gradle 插件或 Transform API 字节码任务,现代 AGP(Android Gradle Plugin,Android 构建插件) 工程可以把它当普通 AAR 解析。这只排除了旧版编译期插桩 API 与构建链不兼容这一项风险。重建 fork 仍要升级上游 AGP/Gradle/NDK;最终 App 还要验证 manifest 合并、libc++_shared.so 冲突、ABI 筛选和 16 KB 打包结果。
3.0 的运行时 Hook 与同步抓栈
btrace 2.0 依靠 Gradle 插件和编译期字节码插桩记录方法进入与退出。3.0 删除了应用方法的编译期全量插桩,改为“运行时动态 Hook + 当前线程同步抓栈”。同步抓栈表示由目标线程在经过 Hook 点时读取自己的调用栈:
- ShadowHook(用于替换 native 函数入口的 Hook 库) 负责替换 native 函数入口,它和 JNI Hook 一起拦截一批高频或可能阻塞的 ART 路径,例如对象分配、JNI 调用、Monitor 对象锁、GC(garbage collection,垃圾回收)、
Object.wait()、Unsafe.park()。 - Hook 点在目标线程上调用 ART 私有
StackVisitor,只保存ArtMethod*和时间、线程、rusage(线程资源使用计数)等轻量信息。 - 停止采集后,再对去重的方法指针批量符号化,转换成 Perfetto 的 stack/slice 数据。
- PC 端把应用样本追加到系统 trace,输出
.pb。
同步抓栈省掉了“采样线程暂停目标线程、抓栈、恢复线程”这套固定成本,还能在锁、wait、park 等 Hook 点同时记下 wall time(现实经过时间) 和 thread CPU time(线程实际占用 CPU 的时间)。代价是它依赖 Hook 点:线程在两次 Hook 之间执行的短方法记不全;线程长期阻塞在未覆盖的入口,样本也可能稀疏。而且这里的 slice 由样本和 Hook 上下文重建,别把它当成每个方法精确的 enter/exit 计时。

图:目标线程经过Hook点时按间隔门槛抓栈,停止后再符号化并重建现场。 1ms是默认最小间隔,不是周期定时器;手动force入口可绕过间隔,重建slice不是精确enter/exit。
这也解释了两个常见配置误区:
- btrace 3.0 没有 2.0 那种方法 include/exclude 插桩表。
-m接收的是 ProGuard/R8 mapping,即“混淆后名称 → 原始类/方法名”的映射,用于反混淆;它不是 btrace 2.0 的 method mapping。 -sampleInterval是同一线程两次同步抓栈之间的最小间隔,默认1,000,000 ns,也就是 1 ms。它不会启动一个严格每 1 ms 唤醒的定时采样器。常规自动样本依赖 Hook 点;SDK 还提供手动captureStackTrace(force),其中force可以绕过间隔检查,二者要分别解释。
从构建到 Perfetto 的采集链
采集过程跨 App、adb、PC 脚本和 Perfetto 四段,任何一段失败都可能出现“文件已经生成,数据却不完整”。我们顺着两条路径看:PC 控制路径负责启动、停止和下载,App 样本路径负责采样和解码,最后在 PC 侧与系统 trace 合并;下图按这两条路径展开。

图:PC控制启动/停止和下载;App样本解码后才与独立系统trace合并。 设备端口、server、dump、mapping与系统文件分别验收;完成回调0也可能掩盖单项dump失败。
冷启动时,PC 先写 debug.rhea3.startWhenAppLaunch=1 系统属性,再用 force-stop 终止现有 App 进程并启动 Launcher Activity(点击桌面图标时进入的 Activity);App 在 attachBaseContext() 读取属性并开始采样。
非重启采集则通过 adb forward 把 PC 端口转发到设备端,再访问 App 内嵌的 NanoHTTPD 轻量 HTTP server,发送 start/stop/download。设备端口记录、HTTP server、采样文件、mapping 文件或系统 trace 任一缺失,PC 都可能超时或只留下部分数据。固定 TraceManager.stopTracing() 对单个 ability 的 dump 失败只记日志,循环结束后仍可通知 onTraceDumpFinished(0, ...);所以拿到成功回调、看到文件存在,都只说明流程走完,内容完整性要单独检查。
接入包要有明确开关
上游建议在真实实现与 no-op AAR 之间切换。下面的模板保留这种做法,并把版本固定在稳定版:
dependencies {
if (providers.gradleProperty("enableBtrace").orNull == "true") {
implementation("com.bytedance.btrace:rhea-inhouse:3.0.0")
} else {
implementation("com.bytedance.btrace:rhea-inhouse-noop:3.0.0")
}
}
no-op 版本保留同名 API,但方法不执行真实采集,适合让普通构建不携带 Hook 行为。Android 17 fork 若直接复用上面的官方坐标,等于冒充兼容版;应使用自有 Maven group/version,并记录上游 commit、ART tag、NDK、ShadowHook 和 16 KB 测试结果。
App 需要尽早初始化,下面的调用放在主进程 Application.attachBaseContext():
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
RheaTrace3.init(base);
}
RheaTrace3.init() 会直接跳过非主进程,所以此官方入口不覆盖 remote Service(运行在独立进程中的 Android Service)。初始化还会启动 App 内的 HTTP server,入口没有重复初始化保护,应用应保证只调用一次;真实依赖别在所有生产构建中无条件常驻,只放进 internal(内部测试) 测试包或专项诊断包,更容易限制可访问范围和性能变量。
一条可复现的冷启动命令
这条命令用于采集主进程 10 秒冷启动,并显式要求调度事件:
java -jar rhea-trace-processor-3.0.0.jar \
-a com.example.app \
-t 10 \
-o startup-api34.pb \
-m app-release-mapping.txt \
-r \
sched
-r 表示重启 App,sched 是传给系统 trace 脚本的 category(要启用的 trace 事件组),两者作用不同。源码在没有提供 category 时也会补 sched,但复现记录里显式写出更清楚。采集前我们先用 --list 查看设备支持的 category;要分析 Binder、磁盘或 CPU 频率,还要确认对应事件确已进入 trace。看到 .pb 文件存在,先别认定所需轨道齐全。
perfetto 与 simple 模式
| 项目 | perfetto | simple |
|---|---|---|
| PC 侧实现 | PerfettoCapture 启动 record_android_trace 系统采集脚本,随后追加应用样本 | LiteCapture 只等待采集结束并写应用样本 |
| 自动选择 | PC 源码在 API 28+ 默认选择 | API 27 及以下默认选择 |
| 系统信息 | 可包含 atrace(Android framework/应用事件)、ftrace(Linux 内核事件)、调度和频率等,取决于 category 与设备权限 | 当前源码没有采系统 trace |
| 输出 | 系统 trace + btrace 样本的 .pb | 仅 btrace 样本转换的 .pb |
| 诊断能力 | 能区分 Running、Runnable、阻塞与业务栈样本 | 只能看到 App 方法样本,等待原因容易误判 |
README 把 Android 8.1 写成 perfetto 模式的默认分界,但 Main.getSystemLevelCapture() 的实现用的是 SDK_INT >= 28,也就是 Android 9。API 27 若要显式尝试 -mode perfetto,必须在目标设备上验证脚本与 tracing service(系统 trace 服务);文档描述代替不了测试。
README 还说 simple 模式能带系统 atrace,当前 LiteCapture 源码只调用 SamplingTraceDecoder,没有启动 atrace。遇到这类文档与实现冲突,应以所用 commit 的代码和产物测试为准。
参数决定“能看到什么”
| 参数 | 精确含义 | 容易踩的坑 |
|---|---|---|
-a | 包名;App 内只采主进程 | 不能靠包名覆盖同包多进程 |
-t | PC 等待的采集时长,单位秒 | 过长更容易覆盖 ring buffer(容量固定、写满后覆盖旧记录的环形缓冲区);过短可能漏掉首帧后任务 |
-o | 最终 .pb 路径 | 同时保留设备、API、App 版本和场景信息 |
-r | 设置启动采集属性并重启 Launcher Activity | 多 Launcher 时要核对选择结果,必要时指定 -launcher |
-m | ProGuard/R8 混淆映射文件 | 必须与 APK 构建完全匹配;3.0 不再使用 2.0 method mapping |
-mode | 强制 perfetto 或 simple | perfetto 启动失败时会缺系统文件;simple 没有调度证据 |
-sampleInterval | 同线程抓栈最小间隔,纳秒 | 调小会增加 CPU 和 buffer 压力,也不能保证固定周期 |
-maxAppTrace | ring buffer 可保存的抓栈条数,默认 200000 | 满后旧记录被覆盖;停止时要检查脚本打印的 usage |
-waitTraceTimeout | 等待端侧 dump 完成的秒数,默认 20 | 加大只改变等待时间,不能修复端侧 dump 失败 |
-s | 指定 adb serial(设备序列号) | 由 Adb.init() 单独读取并附加到后续 adb 命令;多设备环境应显式指定 |
上游 README 还列出 -mainThreadOnly,当前 Arguments.Parser 没有解析它,也没有把它转成设备端配置。写入自动化脚本前,别指望这个参数生效。若只需要主线程,应在 fork 中实现可测试的配置,或在后处理阶段筛选线程。
Perfetto UI 要从线程状态读起
打开一份合并好的 trace,btrace 样本给出某个 Hook 点附近的 Java 栈,系统轨道则回答线程有没有运行、在哪个 CPU 上运行、为何被唤醒。我们按下面的顺序读:
- 用用户操作、启动点或手工 trace marker(取值数量有限、名称稳定的时间线标记) 圈定问题窗口。
- 查看 Main thread 与 RenderThread 的 thread state(线程状态)。
Running表示正在 CPU 上执行;Runnable表示已经可以运行,却还在等待调度。只有Running时间才累计 on-CPU 工作。 - 对照 CPU 轨道和 CPU frequency(运行频率)。多条后台线程同时
Runnable时,主线程慢段可能来自 CPU 竞争。 - 再展开 btrace 重建的方法栈,看样本是否连续、是否出现
[unknown],以及 buffer 是否覆盖。 - 若线程进入
Sleeping或Uninterruptible Sleep,继续找 Binder、futex(用户态锁的内核等待机制)、I/O、GC 或唤醒者;缺对应 category 时重新采集。 - 渲染问题继续对照 FrameTimeline(帧是否按期限完成的系统时间线)、Choreographer、RenderThread、SurfaceFlinger/GPU 轨道。btrace 的 Java 栈回答不了 GPU 何时完成。
一段 30 ms 的“方法 slice”里,CPU 可能只执行了其中 4 ms,其余时间耗在 Runnable、sleep、Binder 或 I/O 里。修复记录因此要分别写明 wall duration(现实经过时长)、on-CPU duration(真正占用 CPU 的时长)、线程状态和阻塞依据。
sched 数据来自目标设备的 ftrace 能力,btrace 不自带内核探针。GKI(Generic Kernel Image) 是 Android 通用内核架构;知识库当前的 Android 17 common kernel 源码基线是 android17-6.18-2026-06_r39,此前核验使用的 android17-6.18-2026-06_r6 继续保留,便于复现旧基线。
量产设备仍可能使用厂商分支和不同内核配置;轨道缺失时,应先检查目标设备支持的 category 与 trace 配置。
冷启动案例
以下是诊断推演示例,数字不是本次设备测量结果。假设版本升级后热启动稳定,冷启动 P95 增加 300 ms,慢的那一端被拉长了。采集时使用同一台设备和一致的冷启动定义,运行 10~15 秒 perfetto 模式,并保留 sched、CPU 频率和所需 atrace category。
Zygote fork 出应用进程后,ActivityThread 从 bindApplication 开始把新进程绑定到应用;此时 btrace SDK 还没全部初始化,冷启动前半段必须依赖系统 trace:
| 阶段 | btrace 能提供的证据 | 系统 trace 要补的证据 |
|---|---|---|
| Zygote fork / 进程创建 | SDK 尚未加载,基本没有方法样本 | 进程创建、调度与 CPU frequency |
bindApplication 到 attachBaseContext() | 只能覆盖初始化后的部分 | ActivityThread/framework slice 与线程状态 |
| Provider 安装 | 初始化足够早时可见业务/SDK 栈样本 | Binder、I/O、锁等待 |
Application.onCreate() | 方法栈、对象分配/锁等 Hook 上下文 | on-CPU 时间、GC、调度竞争 |
| Activity 创建 | inflate、Fragment、首屏业务栈候选 | 主线程状态与资源读取 |
| 首帧 | Java 提交前的方法候选 | FrameTimeline、RenderThread、SurfaceFlinger |
| 首帧后 | 延迟任务与后台并发 | 主线程是否被后台工作抢占 |
判断时,我们先看增加的 300 ms 落在哪个阶段。若 Application.onCreate() 的 wall time 增长,主线程却长期 Runnable,方法名只代表采样时所在的调用栈;下一步可减少启动并发或调整线程优先级,并用只改变“后台任务是否开启”这一项的 A/B trace 做对照。若该窗口的 on-CPU 时间也明显增加,多个样本稳定落在同一路径,再检查实际 CPU 工作与相关调用;若样本在同步 I/O 路径,还须分别核对运行和阻塞区间。StrictMode(主线程磁盘/网络违规检测)、文件事件或局部计时可帮助确认调用,方法名本身不证明存储等待。

图:先由系统trace覆盖进程创建与bind,再由初始化后的方法样本补业务候选。 300ms是推演;SDK不覆盖全部fork前后路径,方法名与时间接近只是候选,新增耗时原因还要对照确认。
列表滑动案例
另一个推演示例:120 Hz 设备滑动列表时连续掉帧。8.33 ms 是固定 120 Hz 的显示周期近似值;实际帧 deadline 与流水线安排要看 FrameTimeline,把这一周期一律当成主线程独占预算是误读。采集前先在关键业务位置加几个低基数 trace marker,也就是名称稳定、取值有限的时间线标记,方便把 btrace 样本对应到具体列表阶段:
trace("Feed#submitList") {
adapter.submitList(items)
}
trace("Feed#bindViewHolder") {
bind(item)
}
这些 marker 只标记代码块的实际调用区间:Feed#submitList 覆盖提交调用返回前的时间,异步 diff、结果应用和呈现可能在之后;Feed#bindViewHolder 覆盖的也只是 bind 这一步,完整显示另计。btrace 在可覆盖的区间补方法栈候选。marker 名不要带用户 ID、URL 或 item 内容,否则名称会大量增殖、变成高基数,还可能把隐私数据写入 trace。
读图时按帧边界处理:
- Main thread 的
doFrame/FrameTimeline 是否越过 deadline(这一帧应完成的期限);bindViewHolder、diff、图片解码回调和曝光逻辑是否反复出现在超时窗口。 - RenderThread 是否及时拿到 CPU,还是在同步、提交或等待 buffer。RenderThread 慢和 Java bind 慢是两回事。
- 后台解码、JSON、数据库任务是否同时占满大核;主线程若 Runnable 却不上 CPU,应先处理并发竞争。
- 同一操作可先抓 5~10 次作探索,再比较卡顿帧与正常帧;这只是起始规划,不保证尾部分位数精度或稀有问题覆盖。单个样本里的最长栈,代表不了稳定根因。
修复后用同样的数据集、手势脚本、刷新率和设备复测,并比较 missed frame(未按期限完成的帧)、Main/RenderThread on-CPU 时间和对应方法样本占比。
工具怎么搭配
| 工具 | 更擅长回答的问题 | 主要边界 |
|---|---|---|
| btrace 3.0 | 系统时间线中的 Java 栈候选、锁/wait/park 等 Hook 上下文 | 私有 ART 符号、事件驱动采样、官方初始化只采主进程;固定源码存在 API 37 符号/ABI 风险 |
androidx.tracing / Trace | 已知业务区间何时开始、结束 | 需要手工标注,未标注的方法没有语义 |
| Perfetto SDK | 自定义结构化 track event(时间线事件)与应用数据源 | 需要设计 schema(字段结构)和采集策略,不自动生成完整方法栈 |
| Perfetto 系统 trace | 调度、频率、Binder、I/O、FrameTimeline 等全局现场 | 方法语义取决于已有 atrace/SDK marker |
| simpleperf | CPU 热点、采样调用栈、硬件计数器 | 采样结果不等于方法 wall time,Java/JIT(运行时即时编译)符号能力要按设备验证 |
| Android Studio Profiler | 本地交互式 CPU/内存分析 | 工具开销和配置会改变时序,适合可控复现 |
| Matrix Trace Canary 2.x | 对选中方法做编译期 enter/exit 插桩 | 构建链与插桩开销明显,当前上游 AGP 兼容性陈旧 |
常见顺序是:先用线上指标或 Macrobenchmark 固定异常场景,再用 Perfetto 系统 trace 判断 CPU、调度、I/O 或渲染方向;需要业务语义时补 androidx.tracing / Perfetto SDK,需要方法候选时在受支持的测试系统上用 btrace、Profiler 或 simpleperf。缺什么补什么,工具跟着缺口走。
Android 17 移植与验收
如果团队决定维护 btrace source fork(从官方源码派生的自有分支),至少要完成下面这些工作:
- 固定
btrace@2ae621f2与platform/art@android-17.0.0_r1,列出每个npth_dlsym(btrace 自带的动态符号查找)、ShadowHook 和 JNI Hook 在 API 37 上要解析或代理的函数。 - 更新
StackVisitor::WalkStack的函数类型与 mangled symbol(编译器编码后的 C++ 符号名),并在量产设备使用的 user/release 构建上验证。user/release 是关闭大部分调试能力的系统/App 构建,只在便于调试的 AOSP 构建里搜同名函数是不够的。 - 对
StackVisitor构造、vptr(指向虚函数表的指针) 替换和 2048-byte holder(为私有对象预留的内存) 做 ABI、对齐、析构与 inlined frame(被编译器内联的方法帧) 测试。vptr 指向虚函数表,holder 是为私有对象预留的内存,inlined frame 是被编译器内联的方法帧,而私有类布局没有稳定承诺。 - 单独适配或关闭 Java object allocation listener(对象分配监听器);别把“API 30+ 使用同一 listener vtable(虚函数表)”直接延伸到 API 37。
- 用 NDK r28+ 重编
librheatrace.so和 C++ runtime,移除npth_dl.c的 4 KB 常量,所有映射偏移都按getpagesize()或sysconf(_SC_PAGESIZE)返回的运行时 page size 计算。 - 对最终 APK 中的
librheatrace.so、libc++_shared.so、ShadowHook 和其他 native 库执行 16 KB ELF/ZIP 对齐检查,并在 Android 17 的fatal模式下关闭 16 KB 兼容后运行;该模式针对 16 KB 不兼容二进制触发立即中止;它不代替 ART Hook 行为测试。 - 启动后主动采一条已知 Java 栈,断言记录数、栈深、方法名、线程名和时间戳;解析失败必须明确报错,只输出系统 trace 不算通过。
- 覆盖冷启动、前台采集、停止、再次采集、buffer 覆盖、mapping 错配、设备端 dump 超时、adb 中断和多 Launcher 场景。
- 量化关闭/开启采集时的启动耗时、帧时间、CPU、内存、包体和 crash/ANR(Application Not Responding,应用无响应)。1 ms 是同一线程两次采样的默认最小间隔,不是通用安全配置。
- 把真实 SDK 仅放在 internal 或受控诊断包。当前开源流程依赖 PC、adb 和 App 内 HTTP server;
INTRODUCTION中的 online support(线上支持) 列在后续规划里,还算不上已交付的远程采集能力。
检查 fork 的 16 KB ELF 与最终 APK 时,可使用以下两条命令:
llvm-objdump -p path/to/librheatrace.so | grep LOAD
zipalign -c -P 16 -v 4 app-internal.apk
目标 16 KB 对齐的 LOAD segment alignment 应至少为 2**14,也就是 16384 字节;ZIP 检查要针对最终安装的 APK。这两项检查验证的只是相应 ELF/ZIP 静态对齐,装载和运行条件要另行覆盖;ART 私有符号和 Hook 行为仍要在 API 37 真机或模拟器单独验收。
Matrix 的插件架构与诊断能力
埋点和方法采样解决“看到现场”,Matrix 解决的是另一件事:把卡顿、资源、IO 和稳定性检测组织进统一的客户端生命周期。我们这一节看它的模块分工、Android 17 上的接入门槛,以及报告如何落成稳定协议。
2026 年接入结论
Matrix 是微信团队开源的客户端 APM 框架,提供可选的 plugin(插件)、数据采集器和部分离线分析工具;由供应商维护的上传、查询、聚合、告警、工单服务不在其中。接入方拿到的是一组可改造的客户端组件,服务端的数据协议、存储和查询系统要自行建设。
截至 2026-10-26,Maven Central 中 matrix-android-lib 的最新正式版仍是 2.1.0,元数据更新时间为 2023-03-21。GitHub master 顶部提交仍是 2023-07-31 的 README 更新;仓库元数据记录的最后 push 时间是 2024-07-23。提交时间指代码历史中的具体变更,push 时间还可能来自其他分支或标签,两者要分开读;放在一起看,公开维护已经明显放缓。官方 README 只声明 Matrix Gradle 插件可配合 AGP 3.5.0、4.0.0、4.1.0 使用。因此,采用时应更保守:
- 已有 Matrix 项目可以继续维护,但要把自有分支、工具链升级和设备验证视为产品代码的一部分。
- 新项目若采用 AGP 8+,把官方 2.1.0 插件直接加进构建就指望它编译通过,是行不通的;运行时模块与构建插件要分开评估。
- 只引入某个运行时模块,也要核对它是否依赖旧系统实现、native hook(在本地代码层改写函数调用入口) 或旧版预编译
.so。
平台侧检查以 Android 17 / API 37 / android-17.0.0_r1 为准。Matrix 位于应用进程,没有对应的 AOSP 或 android17-6.18-2026-06_r39 内核实现;内核基线只在 Perfetto 的调度、锁等待和 I/O 分析里充当系统侧参照。
按“数据来源”理解模块
下面这张表把模块、观测来源和适用范围放在一起:知道数据怎么产生,我们才判断得出一份报告能证明什么。
| 模块 | 观测来源 | 适合回答 | 不能单独证明 |
|---|---|---|---|
| Trace Canary | 编译期方法插桩(修改字节码并加入探针)、AppMethodBeat、主线程 Looper(消息循环)与帧回调 | 哪段主线程调用路径耗时、启动阶段分布、FPS(每秒帧数)分桶、部分 ANR(Application Not Responding,应用无响应)现场 | 线程为何没获得 CPU、Binder(Android 跨进程调用机制)对端为何慢、锁由谁长期持有 |
| IO Canary | 对指定 Java 运行库的 open/read/write/close 做 PLT hook;替换 CloseGuard reporter | 主线程文件 I/O 的路径、Java 栈、次数、大小、耗时;未关闭的 Closeable | 任意 native 库或后台线程的全部 I/O;SQL 语句质量 |
| Resource Canary | Activity 销毁后用弱引用重检;可选 Hprof dump/分析 | 哪类 Activity 销毁后长期存活、小比例样本中的引用链 | 所有 Fragment/View 泄漏;一次存活必然是永久泄漏 |
| 重复 Bitmap 分析 | Hprof 离线分析器 | 堆快照中内容相同的 Bitmap 与引用链 | 图片为何重复解码、线上每次分配的调用现场 |
| SQLite Lint | 独立 SQLite Lint 插件,hook 或业务回调提供 SQL | SQL 规则问题、索引与查询质量风险 | 某次页面卡顿一定由该 SQL 引起 |
| Battery Canary | 线程活动、系统 API 使用和 HealthStats(系统累计的进程健康计数)等采样 | WakeLock、Alarm、定位、网络、线程等可疑行为 | 单个行为对应的精确耗电量、系统归因的因果关系 |
| Memory Hook | alloc/free(申请/释放 native 内存)的 PLT hook 与 native backtrace(本地调用栈回溯) | native 分配未释放的候选与聚合 | 每个候选一定是泄漏 |
| MemGuard | Matrix 自己实现的 GWP-ASan 风格抽样保护与 PLT hook | 越界访问、use-after-free(释放后使用)、double free(重复释放) | 全量内存安全覆盖;平台 GWP-ASan 已经启用 |
| Pthread Hook | pthread 生命周期的 PLT hook | Java/native 线程泄漏候选、32 位进程线程栈裁剪 | 线程业务逻辑是否正确 |
| APK Checker | 构建产物离线扫描 | 包体构成、资源与 native 库问题 | 运行时性能 |
先弄清表里反复出现的几个机制。PLT(过程链接表)保存共享库调用外部函数时使用的跳转入口,这里的 hook 就是把入口改到 Matrix 的代理函数。Hprof 是 Java/ART 堆快照,记录某一时刻的对象和引用关系,完整的分配事件时间线不在其中。GWP-ASan 是抽样式 native 内存错误检测思路,常用 guard page(故意设为不可访问的保护页)让越界或释放后访问尽快崩溃并留下现场。
Resource Canary 的“重复 Bitmap”能力主要位于 matrix-resource-canary-analyzer-cli 的 Hprof 分析路径,Activity watcher 不会在运行时自动给出每次重复解码栈。SQLite Lint 也有自己的安装配置和 SQL 输入路径;看到数据库文件 I/O,顶多提示我们去查 SQL、索引和事务,生成不了一条 SQLite Lint 结论。
Battery Canary、Memory Hook、MemGuard 和 Pthread Hook 都不宜面向全部用户长期启用。它们会增加采样、回调、栈回溯或 hook 路径。MemGuard 源码还限定它要在 MemoryHook 调用 commit()、让 hook 配置生效之前安装;commit 是 Matrix API 的状态转换,与 Git 提交无关。生产使用要为每个模块准备独立开关、进程范围、采样比例、目标 .so 名称正则表达式,以及停止采集后的恢复验证。
Android 17 下有两个接入门槛
构建插件的 AGP 兼容性
AGP 8.0 删除了整个 com.android.build.api.transform 包。Matrix 2.1.0 的构建插件仍依赖以下旧接口:
MatrixPlugin把 Gradle 中名为android的 extension(扩展配置对象) 强制转换为旧的AppExtension。MatrixTraceInjection.inject()先无条件调用injectTransparentTransform(),注册旧 API 的MatrixTraceTransform;之后才在afterEvaluate中按配置选择 task injection 或 transform injection。切换 task 模式不会免除这次旧 Transform 注册。MatrixTraceTransform继承已经删除的Transform,并使用未承诺兼容性的 AGP 内部 pipeline 类型。- 源码中的 task injection(把自定义任务直接接到旧 variant/task 对象上) 仍把自定义任务直接挂到旧的 variant(如
debug、release)/task 对象上,依赖BaseVariant、DexArchiveBuilderTask等旧 API,也没有改用 Android Components Instrumentation API。
所以,切换到 task injection 仍不足以支持 AGP 8。官方仓库中也没有 MatrixTraceClassVisitorFactory 之类的迁移类。现代项目有三种可复核的选择:
- 继续使用官方明确覆盖的旧构建环境,并接受旧工具链的维护代价。
- 采用持续维护的 fork(基于官方源码继续演进的派生分支),逐项检查它是否已迁到 Android Components API,并在目标 AGP、R8、Kotlin 和动态特性模块上做回归测试。
- 自己移植插桩器。逐类 ASM(Java 字节码读写库)插桩可用 Instrumentation API;若任务必须同时查看全程序的类,再评估 Scoped Artifacts API。迁移工作不止是替换一个注册方法名。
移植必须保留类过滤、忽略方法规则、方法 ID 分配、methodMapping.txt、R8 mapping 读取、增量构建,以及 debug、release 等 variant 的产物隔离。methodMapping.txt 保存“整数方法 ID → 方法签名”的映射,R8 mapping 保存混淆前后的名称映射,两份文件承担不同职责。方法 ID 不会天然跨构建保持稳定:若没有正确使用并保存 baseMethodMapFile,同一个方法在下一次构建中可能换 ID。

图:固定inject先注册透明Transform,再选择后续注入模式,task模式仍依赖已删除API。 AGP8移除了Transform;现代迁移还需保持过滤、映射、增量与variant产物合同。
预编译 native 库与 16 KB page size
page size(内存页大小)是内核管理虚拟内存映射的基本粒度。Android 15 起,AOSP 支持采用 16 KB page size 的设备;Android 17 还能把 16 KB 兼容模式设为 fatal,让不兼容的二进制在加载时立即终止,适合在测试环境暴露问题。Matrix 的 IO Canary、SQLite Lint、Memory Hook、MemGuard、Pthread Hook、Backtrace 等模块都包含 native 代码,Java 层初始化成功证明不了这些本地库兼容 16 KB。本次对实际 Matrix 2.1.0 AAR 的静态检查覆盖 Trace Canary 的 2 个和 IO Canary 的 4 个 .so,其全部 LOAD alignment 都为 4096;其他模块未在本次逐一检查,各自的取值要另行测得。
采用 2023 年发布的预编译产物前,要检查 APK/AAB 中每个 Matrix .so 的 ELF LOAD segment alignment(加载段的内存对齐值)和包内 ZIP alignment(未压缩 .so 在 APK 中的起始位置对齐值),再在 16 KB 模式的 Android 17 设备或模拟器上覆盖安装、启动、hook、停止 hook 和异常回调。Android 官方当前建议使用 AGP 8.5.1 以上处理打包对齐,NDK r28 以上默认生成 16 KB 对齐的 ELF;预编译依赖仍需逐个验证。这与 Matrix 官方 Gradle 插件依赖旧 AGP 的现状直接冲突。工程上通常要分别处理:移植构建插件并重编 native 模块,或选用已经提供现代工具链产物和验证记录的维护分支。
接入结构:注册、build 初始化、安装单例、启动
运行时顺序是:Matrix.Builder.plugin() 把实例加入 HashSet,pluginListener() 设置接收 Issue(统一报告对象)的监听器;builder.build() 构造 Matrix,同时初始化 lifecycle、ProcessSupervisor 并逐个执行 plugin.init();Matrix.init() 只安装进程内单例,随后才启动插件。集合不保证注册或启动顺序,有依赖的插件应显式安排。源码以 2.1.0 的 Matrix.java 为准:其中没有 patchListener(),准确 API 是 pluginListener()。

图:注册、build初始化、安装单例和start是独立阶段,重复build会先产生副作用。 插件集合为HashSet,无注册/启动顺序保证;示例守卫和锁只保护单一应用入口。
下面的骨架只演示 Matrix 2.1.0 中存在的运行时 API。它假设调用方已经完成进程筛选,并且构建期 Trace 插桩器已在当前工具链上通过验证:
public final class MatrixInstaller {
public static synchronized void install(
Application app,
IDynamicConfig dynamicConfig) {
if (Matrix.isInstalled()) {
return;
}
TraceConfig traceConfig = new TraceConfig.Builder()
.dynamicConfig(dynamicConfig)
.enableFPS(true)
.enableEvilMethodTrace(true)
.enableAnrTrace(true)
.enableStartup(true)
.isDebug(false)
.isDevEnv(false)
.build();
TracePlugin tracePlugin = new TracePlugin(traceConfig);
IOCanaryPlugin ioPlugin = new IOCanaryPlugin(
new IOConfig.Builder()
.dynamicConfig(dynamicConfig)
.build());
Matrix.Builder builder = new Matrix.Builder(app)
.pluginListener(new DefaultPluginListener(app) {
@Override
public void onReportIssue(Issue issue) {
super.onReportIssue(issue);
MatrixReportQueue.enqueue(issue);
}
})
.plugin(tracePlugin)
.plugin(ioPlugin);
Matrix.init(builder.build());
Matrix.with().startAllPlugins();
}
}
这里的 MatrixReportQueue 是应用自建的上传队列,不属于 Matrix API。应用应把此安装器作为主线程上的唯一安装入口;这里的锁只保护此入口。重复调用 Matrix.init() 会忽略新实例,但新实例先经 build() 产生的初始化副作用不会回滚。插件成员资格也有实际约束:未加入 builder.plugin(...) 的实例不会进入 Matrix 的插件集合,getPluginByClass() 也找不到它。示例没有表达采样和远程开关;项目代码应在构造插件前完成进程允许列表和实验分组,并让动态配置接口 IDynamicConfig 返回当前生效的采集策略。
多进程应用不要在每个 Application 中照搬同一配置。主进程可开启 Trace;WebView、推送、下载或短命进程只选择能回答该进程问题的模块。还要分别记录“未安装”“安装失败”“已停止”三种状态,否则没有报告时,分不清是没发现问题,还是监控压根没在工作。
Trace Canary:插桩记录与 Looper 窗口如何配合
构建期的 MethodCollector 为选中的方法分配整数 ID,并写出 methodMapping.txt;修改后的字节码在方法进入和退出处分别调用 AppMethodBeat.i(id) 与 AppMethodBeat.o(id)。运行时的 AppMethodBeat 主要向主线程 ring buffer 记录 ID、进出标志和相对时间;非主线程不会进入这条相同的 buffer 记录分支。打包格式使用第 63 位记录进入标志、从第 43 位起编码方法 ID,低 43 位保存相对时间。时间来自缓存的 uptimeMillis() - sDiffTime,由更新时间线程与 dispatch 边界更新,不是逐方法读取纳秒时钟。主线程 Looper 每次 dispatch 的开始与结束,则界定了一条消息的分析窗口。
构建期映射与运行时消息窗口共同决定方法树的可还原性,下图按构建产物、记录窗口和报告解码展开。

图:构建映射与插桩产物进入主线程记录,再按Looper窗口分析和解码。 固定AppMethodBeat使用缓存uptime毫秒时间,不是逐方法纳秒计时;缓冲、状态与插桩失败可缺记录。
这条依赖链暴露了两个常见故障。第一,没保存与该 App 构建一一对应的 methodMapping.txt,服务端就还原不出可靠的方法名。第二,插桩器失效后,Looper/FPS 信号可能仍然存在,方法树却会缺失;这时别下“主线程没执行过业务方法”的结论。
Trace Canary 会生成不同的 tag(报告类别标识)。2.1.0 源码中包括 Trace_FPS、Trace_EvilMethod 和 Trace_StartUp;payload(报告正文)常见字段有 scene、cost、stack、stackKey、detail 和启动阶段耗时。服务端应按 tag + type + payload schema version 解码,其中 schema version 表示字段结构版本;所有 Issue 不会共享完全相同的字段集合。
阈值应按场景配置,不要把一个固定毫秒数写成通用标准。卡顿窗口、冷启动、热启动和 FPS 分桶使用不同信号。方法插桩替代不了逐帧指标:JankStats/FrameMetrics 适合确认 jank(未按显示节奏完成的慢帧) 及当时的界面状态,Trace Canary 的方法树适合解释较长的主线程工作;需要判断调度、锁、Binder 或 I/O 等系统原因时,再转到 Perfetto。
IO Canary 的覆盖范围
2.1.0 的 native 实现通过 xHook(改写 ELF 函数导入入口的库)查找 libopenjdkjvm.so、libjavacore.so、libopenjdk.so。它会在这些库中代理 open/open64/close;read/write 及其 _chk 变体只在 libjavacore.so 分支处理。代理函数发现当前线程不在主线程时会直接调用原函数,不进入收集器。因此,它主要覆盖经这些 Java 运行库路径发生的主线程文件 I/O,覆盖范围小于“进程中任意库、任意线程的 I/O 审计”。
一次成功且可跟踪的 open 保存路径、线程名和当时的 Java 栈;read/write 累加操作次数、请求大小与耗时;close 时补文件大小并运行三类 detector。opSize 累加的是请求大小,不是每次实际返回字节数;opType 只记录首次读写类型,代表不了混合操作序列。文件从 open 到 close 的跨度与 read/write 调用累计耗时也要分开解释:
- Main-thread detector(主线程检测器)关注单次很慢或连续读写超过阈值的 I/O。
- Small-buffer detector(小缓冲检测器)按操作次数、平均请求大小和连续读写耗时判断。
- Repeat-read detector(重复读取检测器)比较路径、线程、Java 栈、文件大小和读取大小,在短窗口内发现重复读取。

图:指定Java运行库的主线程IO被代理,成功open建立生命周期,close汇总检测。 请求大小不是实际返回字节,首次opType不是混合序列;open栈也不是每次read/write栈。
Closeable 泄漏走另一条路径:CloseGuardHooker 通过反射替换 dalvik.system.CloseGuard$Reporter,再把 Throwable 栈转换成 type 4 的 Issue。这依赖 Android 隐藏实现。hook() 失败后仍置尝试标记,这个标记只说明尝试过,成功与否要另外确认;unHook() 虽尝试恢复 reporter,却把 CloseGuard enabled 设为 false,并没有保存/恢复原 enabled 值。必须验证与其他使用者共存、失败重试及停止后的完整状态。源码虽预留 network I/O、cursor leak 的常量,常量本身证明不了 2.1.0 已完整实现对应 detector。

图:CloseGuard反射替换reporter与文件IO代理是独立路径,安装和恢复需分别观察。 尝试标记失败后也可置位;unHook恢复reporter却强制enabled=false,未恢复原enabled值。
Perfetto 与 IO Canary 给我们的信息不同。数据源和权限允许时,Perfetto 能显示调度、I/O、文件描述符或系统调用线索;Matrix 报告保留的是应用层路径、Java 栈以及一次文件生命周期内的聚合字段。一次主线程 I/O 报告可按以下顺序读:
- 用插件线程字段及接入层补充的
scene(业务场景标识)、时间窗口判断它是否处在启动或交互路径;不要假定 native IO payload 原生含有 scene。 - 查看
path、opType、op、opSize、buffer、cost与repeat,区分单次慢、连续小块操作和重复读取。 - 从 Java 栈找到调用入口,但不要把
open时的栈当作每次read/write的精确栈。 - 在 Perfetto 中检查相同窗口内主线程是在运行、等待 I/O、等待锁、等待 Binder,还是因调度压力未及时运行。
- 若路径属于 SQLite,转去检查 SQL、索引、事务和 SQLite Lint 结果;文件路径本身指不出哪条 SQL 有问题。
上报前不要上传原始私有目录、数据库名、账号、URL query(问号后的查询参数) 或缓存 key。受控的路径类型与稳定哈希通常已经足够聚合同类问题;原始路径只留在用户授权的本地调试或受限的小比例测试环境。
Resource Canary:Activity 观察和 Hprof 分开看
ActivityRefWatcher 在 Activity 销毁后保存 weak reference(弱引用),后台任务按间隔请求 GC 并重检。2.1.0 默认最多重检 10 次;达到上限且对象仍存活后,才交给选定的 leak processor(泄漏处理器)。这个过程可以减少对象暂时未回收造成的误报,但 Runtime.getRuntime().gc() 只是一次请求,重检次数在数学上证明不了对象会永久泄漏。
ResourceConfig.DumpMode 提供 NO_DUMP、AUTO_DUMP、MANUAL_DUMP、SILENCE_ANALYSE、FORK_DUMP、FORK_ANALYSE、LAZY_FORK_ANALYZE。dump 指生成堆快照,analyse 指解析快照,fork 类模式会借助子进程降低主进程影响;各模式的暂停时间、磁盘占用和 Android 版本支持范围并不相同。官方 2.1.0 release note 只明确提到 ResourcePlugin 对 API 31 的兼容改动,由此推不出它已经验证到 API 37。
生产环境可把发现和分析分开:
- 面向全部样本的统计只上报 Activity 类名、进程、版本、次数、ref key、dump mode 和分析状态。
- Hprof 只在受控设备、充电/空闲条件或内部测试中生成;设置目录配额、超时和 LRU(Least Recently Used,优先清理最久未使用文件) 清理策略。
- 上传前评估对象数据的隐私风险。Hprof 可能含用户输入、登录 token、URL 和业务对象。
- Fragment、View、listener 等引用问题,可在本地用 LeakCanary 或 heap analyzer 补足引用链;Resource Canary 的 watcher 入口以 Activity 为中心。

图:Activity销毁后弱引用复检产生候选,是否dump和分析由独立模式及预算决定。 默认10次复检与GC请求不证明永久泄漏;fork/dump支持需按系统版本验证。
重复 Bitmap 是 Hprof analyzer 的独立分析结果。它适合指出堆中存在内容相同的 bitmap buffer(像素数据缓冲区)及其引用链,随后再检查图片缓存 key、变换参数、尺寸和生命周期。不要把结果改写成“同一文件被解码了多少次”,Hprof 没有保留完整的解码事件时间线。
Battery、Memory Hook、MemGuard 与 Pthread Hook
这些模块靠近系统 API 或 native 分配/线程路径,启用前应先写出假设和退出条件。
| 模块 | 建议的启用范围 | 关键风险与校验 |
|---|---|---|
| Battery Canary | 低比例样本、后台行为异常的版本、专项实验 | 观察行为不等于精确能耗归因;对照 Battery Historian、Perfetto、dumpsys batterystats 与复现实验 |
| Memory Hook | 指定进程与指定 .so,短时采集 | alloc/free hook 和回溯有成本;检查未释放聚合能否稳定复现 |
| MemGuard | 内部设备或小比例用户,限定目标 .so 与分配尺寸 | 抽样覆盖、guard page 的额外内存、潜在对齐影响;不能在 Memory Hook 已 commit() 后安装 |
| Pthread Hook | 线程数量异常增长或 32 位虚拟地址空间专项 | pthread hook 兼容性、缩小线程栈对深层调用的影响、停止 hook 后状态 |
Matrix MemGuard 标注 “based on GWP-ASan”,表示两者采用相近的抽样保护思路;单凭这个标注就认定 Android 平台自带的 GWP-ASan 配置已生效,是没有依据的。它可以用正则表达式选择目标库,选项还包含最大分配尺寸、最大受保护分配数和跳过分配数,因此覆盖范围由采样配置决定。没有报告只表示本次采样未捕获问题。
报告入库:原始 Issue 之外再建稳定协议
Matrix 的 Issue 只有 type、tag、key、content 和 plugin。进程、发生时间、应用版本、构建 ID、采样策略、隐私级别与上传状态应由接入方补齐。不要让服务端直接依赖每个插件各自变化的 content;先在外层定义一份带版本号的稳定协议,至少保留以下字段:
| 字段 | 用途 |
|---|---|
schema_version | 标识字段结构与解析规则版本,避免用新代码误读旧 payload |
matrix_version / matrix_fork_revision | 定位官方版本,以及派生分支所用的准确提交 |
issue_tag / issue_type | 保留 Matrix 原始路由信息 |
process_name / thread_name / tid | 区分进程、线程名和系统线程 ID |
scene / page | 业务入口 |
duration_ms | 接入层统一时间单位,并保留原字段 |
stack_signature | 去除地址和动态 ID 后,用于聚合同类栈的稳定摘要 |
app_build_id | 关联 APK、R8 mapping 和 native symbols(native 调试符号文件) |
method_map_id | 关联 Trace methodMapping.txt |
sample_policy_id | 解释样本如何被选中 |
payload_object_id | 大栈、Hprof 或 dump 文件在独立对象存储中的索引 |
privacy_class | 路径、URL、对象数据的处理规则 |
下面的 JSON 只定义自建平台协议,字段名不是 Matrix 2.1.0 的原生 payload:
{
"schema_version": 3,
"matrix_version": "2.1.0+company.12",
"issue_tag": "Trace_EvilMethod",
"issue_type": 0,
"process_name": "com.example.app",
"thread_name": "main",
"scene": "HomeActivity",
"duration_ms": 1280,
"stack_signature": "sha256:77c0...",
"app_build_id": "8.3.0-370412-release",
"method_map_id": "sha256:aa91...",
"sample_policy_id": "trace-prod-2026-07",
"payload_object_id": "matrix/trace/2026/07/25/0001",
"privacy_class": "internal-pseudonymized"
}
服务端收到它后,要按 app_build_id + method_map_id 找到同一构建的映射文件,再解码 Matrix 的整数方法栈。stack_signature 只用于聚合同类样本,代替不了原始栈;sample_policy_id 记录样本选择规则;总体发生率还需要相同窗口的合格人群或机会数分母,并解释未采样、采集失败和上传丢失。
IO 事件可以复用同一外层协议,在 payload 中只放经过筛选的字段。下面是路径分类、哈希和单位归一化后的合成协议示例,数值不是本次采集结果;cost_ms 必须明确映射哪个原字段并按原单位转换:
{
"issue_tag": "io",
"issue_type": 3,
"process_name": "com.example.app",
"thread_name": "main",
"scene": "ColdStart",
"path_type": "shared_prefs",
"path_hash": "sha256:8d31...",
"op_type": "read",
"op_count": 42,
"op_size_bytes": 5376,
"max_buffer_bytes": 128,
"cost_ms": 86,
"repeat_count": 6
}
如果真实数据满足相同采集范围和字段合同,这组字段可提示“冷启动主线程在短窗口内反复小块请求读取某类文件”;示例本身不证明发生过该事件。它证明不了 86 ms 全部消耗在存储等待上,也保证不了增大 buffer 就能消除首屏慢;下结论仍要结合调用栈、缓存策略和系统时间线。
从 Matrix 样本到系统时间线
Matrix 给出应用侧的方法、场景和文件路径,Perfetto 给出同一时间窗内的系统执行状态。联合诊断时,别只找一个时间相近的 slice,还要检验互相竞争的解释:
- 慢方法的 wall time 很长,但 CPU time 很短:检查锁、Binder、I/O 和调度等待。
- wall time 与 CPU time 都长:查看 CPU 频点、核心分配、同机并发负载和方法内部工作量。
- Matrix 报主线程 I/O:核对系统调用或 I/O 事件是否与该窗口重合,同时检查 page fault(缺页处理)、锁和 Binder。
- 启动慢:核对进程创建、
bindApplication、ContentProvider、Application、Activity launch 与首帧,不要把 Matrix 的“启动总时长”当成单一函数耗时。 - ANR:Matrix 的主线程栈只是一个观察点;还要查看 Binder 对端、锁持有线程、CPU 饥饿(线程长时间拿不到 CPU)、系统服务与平台保存的 ANR trace。
Perfetto 复现不到线上样本时,保留 Matrix 的 scene、构建 ID、设备、进程、发生时间和实验分组,再从相同入口制造可比较样本。输入和环境控制不住时,一条 trace 与一条线上 Issue 即使栈相似,也不足以建立因果关系。
上线前检查表
- 明确使用官方 2.1.0、哪个 fork revision(派生分支提交),以及每个补丁的维护人。
- 当前 AGP、Gradle、Kotlin、R8、Java、动态特性模块和增量构建均有 CI(持续集成)回归覆盖。
- Trace 插桩失败会让构建失败或产生显式诊断,不会静默发布空方法栈。
-
每个 release 的 APK/AAB、R8 mapping、
methodMapping.txt、native symbols 可由同一app_build_id找回。 -
所有 Matrix
.so通过 16 KB ELF 与 zip alignment 检查,并在 Android 17 的严格 16 KB 模式运行。 - hook 模块覆盖安装、启用、禁用、重复初始化、异常回调、进程退出和版本回滚。
- 各进程的模块允许列表明确;WebView、推送、下载和短命进程没有继承主进程配置。
- 插桩方法数、APK 体积、冷/热启动、帧耗时、CPU、内存、线程数和耗电有对照实验。
- 本地队列有条数、字节数、文件数和保留时长上限;上传失败不会制造新的主线程 I/O。
- 路径、SQL、URL、Hprof、native dump 与日志按隐私等级裁剪、哈希、加密和授权。
- 每个模块有远程停止方式,并验证停止后 hook、listener、线程和文件状态。
- 后端区分“无问题”“未采样”“安装失败”“采集被关闭”“上传失败”。
源码与版本依据
- Matrix v2.1.0 release
- Matrix
master顶部提交(2023-07-31) - Matrix 仓库元数据
- Maven Central: matrix-android-lib metadata
- Maven Central 主镜像:matrix-android-lib metadata
- Matrix README(v2.1.0)
- Matrix.java(v2.1.0)
- Issue.java(v2.1.0)
- MatrixPlugin.kt(v2.1.0)
- MatrixTraceInjection.kt(v2.1.0)
- AppMethodBeat.java(v2.1.0)
- IO Canary native hook(v2.1.0)
- ActivityRefWatcher.java(v2.1.0)
- ResourceConfig.java(v2.1.0)
- MemGuard.java(v2.1.0)
- Android Gradle plugin API updates
- Android 16 KB page size compatibility
- AOSP Android 17 平台标签
- Android 17 common kernel
r39标签