
最近又在帮同事排查一个启动卡顿问题老规矩systrace 一拉问题基本就现形了。可能有人觉得 systrace 都算是老古董了Perfetto 不是更全可实际工作里systrace 依然是最快、最轻、最“随手可用”的性能初步定位手段没有之一。这篇文章不打算系统复述官方文档而是想从“我拿到一份 systrace 之后是怎么一步步把问题圈出来的”这个视角把问题初步定位这套流程掰碎了讲。看完你至少能回答这几个问题这份 trace 里到底哪个时间段出问题了问题出在主线程、渲染线程还是 Binder 调用下一步应该去哪段代码里深挖1. systrace 到底是什么能帮我们解决什么问题很多人一提到性能问题就想到 Android Studio Profiler 或者 Perfetto但 systrace 的地位从来没被真正替代过。它基于 Linux kernel 的 ftrace 机制把 CPU 调度、线程状态、SurfaceFlinger vsync、Input 事件分发、Binder 事务、进程内存状态这些系统级信息统一变成一条带时间戳的事件流再渲染成浏览器里那份大家熟悉的彩色时间轴。它的核心价值是跨进程的“全局视角”。你在自己的 App 里打日志、加埋点看到的永远只是一小块拼图CPU 是不是被别的进程抢了Binder 调用是不是卡在 system_serverGPU 是不是来不及提交帧这些只有到了系统层面才看得清楚。所以我的习惯是接到一个性能 bug先不写任何代码也不猜直接 systrace 抓一段复现过程先把“案发现场”固定下来。初步定位阶段我不关心每一帧的耗时精确到多少毫秒只关心这四件事发生问题的精确时间段是哪里这段时间内我的进程里哪些线程处于执行、等待、阻塞状态主线程和渲染线程到底被谁拖住有没有明显的 Binder、锁、I/O 同步阻塞。这四个问题用 systrace 都能快速回答这也是它作为“第一定位工具”最大的优势。后面的所有操作都是围绕这四件事展开。2. 抓取准备5 分钟拿到一份可用的 trace2.1 命令行抓取比你想的更省事推荐直接命令行操作简单可控尤其适合需要反复复现的场景。# 抓取基本调用链、CPU调度、频率、Binder 等内容 python3 systrace.py -a com.example.app -b 16384 -t 15 -o trace.html \ sched freq idle binder_driver gfx view wm am input res这里的每个参数都值得说清楚-a com.example.app指定 App 包名否则你在 trace 里看不到这个进程内部线程的完整名字和状态。-b 16384内核 trace buffer 大小。默认太小会导致 trace 被截断我一般至少 16MB 起步如果是复杂场景直接给 32768。-t 15抓取时长 15 秒。太长文件就大了但 15 秒基本足够让问题完整地出现一次。后面的 tag 列表sched调度信息和freqCPU 频率是灵魂gfx和view用来查渲染和 View 层级binder_driver用来查 Binder 调用input负责输入事件。有些版本的系统上python3 systrace.py可能提示no module named那是环境缺依赖装一下six就行。还有个小技巧AI 时代很多人用脚本封装。但如果你只是临时抓一次直接命令行是最稳的不用引入额外工具链。2.2 抓取时的场景控制直接影响定位效率抓 trace 不是把手机扔那儿就去喝咖啡。我见过太多人抓完 trace 后问“这段是不是复现了”仔细观察 trace 之前自己都不知道。为了避免这种情况我有两个固定动作抓之前想清楚触发路径。比如卡顿场景是“快速滑动列表”那就起好 App、做好准备动作等 trace 开始后再稳定操作 8~10 秒操作完立刻停。抓完把trace.html拉到电脑上用 Chrome 打开马上就能看到彩条时间轴。尽量只抓需要的 tag。有人为了图省事直接加了特别多的 tag好处是信息全坏处是 file 变大、解析变慢、时间轴乱成一片真正找问题的时候反而难受。抓的时候最好保持后台应用干净把无关 App 都退掉。systrace 是全局 trace其他进程的调度也会进来后台东西太多会干扰判断。2.3 老版本 Python 环境和新版 Perfetto 的兼容问题如果能正常抓到 trace 就直接用。但如果你用的是比较新的 Android SDK命令行里可能已经找不到systrace.py了因为 Google 已经把大部分能力合并到 PerfettoSDK 里带的是platform-tools里的 perfetto。遇到这种情况不要犹豫直接用./perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace -t 15 \ -b 16384 sched freq gfx view binder_driverPerfetto 的抓取结果可以直接拖到 https://ui.perfetto.dev 打开界面更现代而且同样能看调度状态。我的经验是老项目、老文档里到处都是 systrace你最好两个都会新项目中遇到 perfetto 命令也不要慌底层事件源依然是同一套 ftrace分析思路完全通用。3. 第一次分析把问题时间段“框”出来3.1 先看整体再往下钻拿到 trace 后千万别直接一头扎进密密麻麻的线程条里。我习惯的顺序是第一步打开 Chrome或者 perfetto UI后按住w键放大时间轴先整体扫一遍找“明显不正常的局部区域”。什么叫“不正常”常见的有几种我的进程区间里有一大块线程全部变成了白色没有运行在 CPU 上、深蓝色不可中断睡眠或者橙色被抢占/阻塞而旁边的进程 CPU 占用很高。我的主线程颜色明显和其他交互线程不一样比如整个滑动期间主线程频繁从绿色跳到橙色。SurfaceFlinger 的 vsync 时间轴里出现明显的空隙说明掉帧了。InputDispatcher 的箭头在图里打了个结手指一划但 App 半天没响应。这一步的目标不是精确到微秒而是把时间范围从 15 秒缩小到“出问题的那一两秒”。我通常会把这一两秒的左右边界记住或者用 UI 里的选区功能画一下后续所有分析都在这个窗口内进行。3.2 线程状态颜色必须认识systrace 里的颜色是快速判断线程状态的关键符号。下面是我常用的对应关系记熟了基本能一眼看出海量线程里谁在干活颜色线程状态我的理解绿色Running运行中正在 CPU 上执行代码蓝色Runnable可运行想跑但在等待 CPU 分配调度延迟紫色Sleeping睡眠/阻塞在等某个事件、锁、Binder 返回橙色Uninterruptible Sleep不可中断睡眠多半在做 I/O或内核态被卡住白色未知/无 trace 信息可能是 idle也可能是没开启对应 trace point分析卡顿的时候给主线程建模绿色是干活蓝色是想干活没轮到紫色是等结果橙色往往是最扎心的“I/O 卡住”。如果你的主线程在问题时间段里分布的是大段蓝色那基本就是 CPU 竞争/优先级问题如果是紫色那大概率是等锁或者等 Binder如果是橙色I/O 概率很大如果还是绿色但画面卡顿那问题就不在线程调度层而在渲染管线或者逻辑复杂度上。3.3 一个非常实用的“时间框定”技巧我经常遇到这种情况问题能稳定复现但 trace 里时间线拉得太长了没法直接看出卡顿点在哪里。这时候有个土办法在抓 trace 的进程里同时调用Log.w(TraceMark, START_ISSUE)和Log.w(TraceMark, END_ISSUE)这样在 systrace 时间轴的FrameTimeline或者log区域会留下标记之后用自带的 search 按钮就能定位到问题窗口。没有 root 也基本没问题因为这些日志只要普通权限就能打。如果你有 root那更进一步的做法是直接写入/sys/kernel/debug/tracing/trace_marker写出来的字符串会直接出现在 systrace 的 Timeline 中定位精度比 logcat 还要高。这个技巧在分析一些偶现问题时非常救命我把标记放在关键业务路径的开始和结束位置抓完 trace 一搜就能从 15 秒里精确跳转到问题发生的 200ms 范围。4. 初步定位的关键维度CPU、主线程、渲染、Binder4.1 CPU 是不是被抢了先看调度延迟把问题时间框定之后第一件事是看 CPU 的全局负载。systrace 的最上方会显示每个 CPU 核的 running 情况以及每个进程的 CPU 使用率。如果附近有其他高优进程比如系统里的某个 native 服务、媒体服务正在满负荷运行而你的主线程刚好是蓝色 Runnable那基本可以判断是调度延迟导致的卡顿——也就是说你的线程“想跑”但 CPU 没给它时间片。蓝色 Runnable 是一个非常明显的信号。有一次我分析一个第三方 App 的卡顿问题trace 里主线程整整 60 多毫秒都是蓝色只是在等待 CPU。顺着 CPU 列表一看某个云游戏进程正在后台疯狂占核。把那个进程杀掉后问题立刻消失。这种定位属于 systrace 能最快给出结论的类型不涉及你 App 代码逻辑纯粹是全局调度问题。但注意Runnable 也不一定都是外部进程抢占。如果是主线程频繁被新建线程抢优先级或者自己发了一堆任务进线程池也会表现为蓝色。所以看到蓝色不要急着甩锅先点一下这条线程看右侧详情里调度延迟时间是多少再看看旁边 running 的进程是谁综合分析才稳。4.2 主线程长期紫色等锁还是等 Binder主线程如果大片紫色问题十有八九出在“等待”上。我把这种紫色分成两类Binder 调用型当前线程调用了 Binder但没有立即返回。在 trace 里看线程正下方的Binder transaction区域一般能看见具体调用栈比如 system_server 里正在执行的 AMS/WMS 相关服务再严重些就是进程间死锁。锁等待型局部锁竞争导致线程进入 futex 等待。这个在 systrace 里不如 Binder 直观需要配合 dump 线程栈或者打开locktag。不过初步定位时能判断出“等锁了”就够了具体锁信息后续再深入。我见过一个很典型的例子App 启动时在Application.onCreate里做了一堆异步初始化其中有一个用CountDownLatch等网络配置返回结果网络服务在别的地方卡了 5 秒主线程也就安安静静紫了 5 秒。systrace 拉出来后一条长长的紫色条非常清晰问题代码都不用怎么搜就确定了。4.3 渲染问题从掉帧去看主线程的工作量在 gfx 相关的 tag 下每一个 App 的渲染输出都会在SurfaceView/TextureView/SurfaceFlinger相关区域留下记录。看主线程是否产生了掉帧第一步是找Frame或者Choreographer#doFrame的切片。如果主线程的doFrame时间明显超过 16ms那问题大概率就出在主线程的 measure/layout/draw 上。但还有一种情况主线程 doFrame 很快但渲染线程/GPU 侧跟不上。这类问题在中小型项目中不算多但也别忽略。初步定位时看一点就行在掉帧区间主线程是否长时间保持绿色 running 状态且doFrame切片很长。如果是去代码里查布局和绘制耗时大概率能找到ConstraintLayout嵌套、重复绘制、超大 Bitmap 加载等老问题。4.4 输入事件延迟InputDispatcher 看得明明白白很多用户反馈“点按钮没反应”“滑动不跟手”这类问题在 trace 里也有明确特征。开启input这个 tag 后你会看到 input 事件从 InputDispatcher 到 App 的传递线条。如果事件到达 App 后卡片是白色/灰色停顿后再接上那多半是主线程忙导致 input 没有及时被处理如果是事件本身在 system_server 里就卡住了那就是系统侧的问题。读过一部分 Android 源码的人都知道input 事件的分发有一个“输入事件超时”机制超过一定时间通常是 5 秒会触发 ANR。systrace 里如果看到 input 事件到达应用进程后主线程迟迟没有消费那说明主线程当时的执行时间过长把InputEventReceiver的consumeEvents堵住了。拿着这个结论去看卡顿现场排查范围直接缩短到“主线程在做什么”。5. 从 trace 到代码几个常用定位手法5.1 直接看调用栈切片和函数耗时systrace UI 里支持点击任意 slice底部会显示函数名称、start time、duration。像主线程的 doFrame、onDraw、Binder 调用这类关键 slice点进去除了能看到耗时还能看到当时的方法栈如果抓的时候带了-l或系统支持 stack 采集。我最常用的一个操作是放大到主线程的时间条逐个点击卡顿区间的 slice看哪个函数耗时最突出。比如同样是Choreographer#doFrame点进去发现View.measure占了 40ms那下一步就可以直接去布局文件里看 measure 次数如果点进去发现最终耗时都不大但 doFrame 的总耗时就是长那就要怀疑是不是主线程时间片被调度打散得回头再看 CPU 窗口。5.2 用 trace 的“搜索”功能精确跳转systrace 页面顶部有个搜索框CtrlF可以按 slice 名称搜索。我经常用这个来快速检索关键字。比如想看某个阶段有没有执行特定代码直接在代码里添加TraceCompat.beginSection(MyBiz:doSomeThing); // 业务逻辑 TraceCompat.endSection();重新抓 trace再在搜索框里搜MyBiz就能直接跳到这段代码的执行时间点。这个方法在对抗“偶现问题”时效果奇好你不需要理解整个 trace只要埋好标记问题复现后一条条搜过去定位效率能提好几倍。5.3 文本化导出脚本辅助统计Chrome UI 看时间轴很直观但要统计某些事件的次数、平均耗时或者做批量分析时UI 就不好用了。这时可以选择导出 trace 的文本形式。命令行里加-o时保存 HTML 用于可视化如果想导出文本可以用atrace --async_start和--async_stop直接拉出内核 trace也可以把 HTML 里的 JSON 部分想办法提取出来。更省事的方式是现在的 Perfetto 已经支持导出.pftrace后在 ui.perfetto.dev 里选择 “Query” 或 “SQL” 面板直接写 SQL 分析事件。我拿 SQL 分析干过一件比较出活的事把几个版本里 ListView 的dispatchDraw耗时全部查出来按 P50/P99 排序一下就看出某次提交后绘制耗时明显翻倍定位到同事加的一个全局背景层。这一步属于“初步定位之后的进阶”但对问题最终解决特别有帮助。6. 常见问题速查表与避坑经验6.1 我遇到的问题到底属于哪一类很多人看完时间轴会茫然是因为脑子里没有一个“问题分类模型”。我先帮你把常见的卡顿、慢、掉帧问题挂上钩trace 中的表现深度怀疑方向下一步动作主线程大面积蓝色 RunnableCPU 抢占、调度延迟、线程太多看 CPU 窗口是哪个进程抢占调整优先级/减少线程主线程大面积紫色 SleepingBinder 等待、锁等待、网络/IO 等待点 Binder slice 看调用栈去对应服务查逻辑主线程大片橙色内核态不可中断大概率磁盘/IO检查文件读写、MMAP、SQLite 操作主线程绿色但卡顿应用执行逻辑过重点 doFrame 下钻 measure/layout/draw渲染线程忙碌但 SurfaceFlinger 掉帧渲染复杂度过高/GPU 瓶颈检查过度绘制、大图、特效合成Input 事件到 App 后长时间无响应主线程忙直接主线程方法耗时分析6.2 抓取和分析中容易踩的几个坑第一个坑-a 参数忘写。没有指定进程的话trace 里连主线程是谁都难找所有非系统进程都叫?或者进程号排查效率暴跌。每次抓之前默念三遍包名。第二个坑打开 trace 之后浏览器提示 “was not registered with trace” 之类的话其实不用慌只要能看到自己的进程切片就够了。出现这种提示通常只是你选中的 tag 组合里有 tag 没生效不影响主要工作。第三个坑我见过有人一上来就加freq然后看着频率曲线乱猜 CPU 降频。freq 信息只代表 CPU 当时的运行频率不能直接证明是温控降频。要验证降频得去查 thermal 相关的日志别在 trace 里强行解读。第四个坑新版本 Perfetto 界面和旧 systrace 界面有差异但没有本质区别。如果你打开.perfetto-trace时发现没有传统 systrace 的“线程颜色”在 UI 右上角切换到Sched相关视图或选择Kernel里的调度区域也能看到。别因为界面不熟悉就认为抓出来的 trace 没用。第五个坑别过度解读 “Binder 调用变慢”。Binder 调用慢有时候不是 Binder 本身问题而是 channel 繁忙或者对端进程太忙。具体要看对端线程状态。如果你发现 App 线程在等 Binder而对端 system_server 线程也卡住了那问题就在 system_server 内的某个锁上有权限的话抓一份 system_server 的线程 dump结合 trace 看锁等待原因定位才完整。7. 一个完整的实战思路从 trace 到修复最后分享一个我处理过的典型案例完整看一下“初步定位”是怎么往前推进的。问题是某个页面上滑加载更多时概率性卡住 3 秒左右。复现后抓 trace主线程在 10 分 03 秒附近出现了一段约 2.8 秒的紫色。点开切片Binder 调用目标指向了package manager的某个 service当时脑海里的第一反应是“为什么加载更多会去查 PackageManager”顺着 trace 里的调用树一路点下去发现页面里的 RecyclerView 的 onBindViewHolder 里有一段代码每次加载更多都会去查询应用是否安装了某 SDK 需要的组件用的是PackageManager.queryIntentActivities()。这个调用在某些机型上会触发跨进程 扫描耗时直接把主线程打成紫色块。修改方案也很简单把查询结果缓存到内存里或者移到子线程预加载。整个过程里真正花在 systrace 上的时间不到半小时从看到紫色条到定位到具体函数基本一路都是 trace 在“指路”。这也是我为什么坚持遇到性能问题第一步永远是用 systrace 把现象变成证据而不是打开代码猜测。我自己长期使用下来的体会是systrace 不一定要百分百精通但你会看 CPU 调度、会看线程状态、会看 Binder 调用就能解决 70% 以上的 Android 性能初步定位需求。剩下 30% 再考虑堆栈采样、perfetto SQL、native 内存等更重的工具。如果你刚接触 systrace建议拿一次真实的卡顿按照文中的顺序走一遍抓 trace、框时间、看颜色、查 Binder、找主线程、下钻 slice。一次完整的“初步定位”做完之后你对系统调度的感知会强很多再回头写代码踩坑概率都会低不少。