ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Perfetto与Systrace:Android卡顿定位的完整实战指南

Perfetto与Systrace:Android卡顿定位的完整实战指南 前阵子组里一位同事被线上卡顿问题折腾了快两天日志打了几百MB主线程行为始终对不上用户反馈的“某页面滑动掉帧”。后来大家坐在一起用perfetto重新抓了同一场景五分钟就锁定了问题一个高频Binder调用在持锁状态下阻塞了主线程。那一刻会议室里的空气都变了。这次复盘让我更加确信做卡顿分析perfetto以及它底层的systrace机制不是“可选的辅助工具”而是唯一能还原真相的入口。这篇内容想以实战为主线把perfetto和systrace之间的关系、抓取前的准备、打开trace后的读图顺序、卡顿形态分类、一次完整的问题定位链路以及大项目里怎么控制开销这几个环节完整串一遍。无论你是刚开始接触性能分析的新人还是已经在用perfetto但总感觉找不到关键证据的开发者这篇文章都值得你按顺序看完尤其是第四、五两章基本是把trace从“能打开”带到“能看懂”的临界点。1. 为什么性能工具是排查卡顿的唯一可靠入口1.1 卡顿的本质是“帧边界被突破”不是“日志里某一行报错”Android渲染有一套固定的节拍机制。60Hz刷新率下每一帧只有16.6ms预算120Hz普及后这个预算进一步压缩到8.3ms。系统并不会按照某个线程的“忙碌程度”判断卡顿而是看每一帧的图像是否在VSYNC边界内完成从App处理到SurfaceFlinger合成并送显的全过程。一旦某一环超时用户感知到的就是掉帧、滑动不跟手、点击延迟。所以卡顿本质上是一个“时间段内发生的、跨进程、跨线程的事件串”。它可能起因于主线程芯疼的布局计算也可能发生在RenderThread等待GPU fence的间隙还可能卡在Binder线程等待远端服务响应。这些事件在Logcat里往往毫无痕迹因为日志是针对异常和错误设计的不是针对“某个任务多花了30ms”设计的。这就解释了为什么“打日志排查卡顿”这条路绝大多数时候走不通你根本不知道该在哪一行打日志因为问题根本不是从某一行“报错”开始的。1.2 systrace和perfetto同根生但后者是完整形态很多老开发到现在还在直接调用Android SDK里的systrace.py脚本输入一串命令行参数输出一个html文件然后拖进浏览器里看。这个流程没有错但systrace本质上是perfetto的“简化版前端封装”。从Android 10开始系统的trace底层数据源包括ftrace事件、atrace标记、CPU调度信息、内核频率变化全部由perfetto接管systrace脚本只是负责把perfetto抓到的数据转换成了一个独立的、可被旧版浏览器查看的HTML格式。这说明一个事实你用了perfetto依然拥有systrace的一切能力但反过来不一定成立。perfetto的UI支持SQL查询、多进程轨道过滤、自定义TraceConfig、导入导出自定义数据源这些都是旧systrace html难以做到的。我个人的判断是新项目、新问题排查团队应该统一用perfetto这套工具链。系统老到API 27以下才需要退回去看systrace脚本而实际工作中绝大多数设备已经支持。2. 抓取前的准备工作从下载到配置2.1 下载与安装别用系统自带的老方案直接走Perfetto官方渠道perfetto的获取方式比很多人想象中简单。打开官方站点会看到一个Web UI支持两种方式抓取第一种是Android设备用USB连上电脑浏览器里的Perfetto UI可以直接通过ADB发起抓取第二种是直接在设备端跑一个命令行程序生产一份trace文件再拉到电脑上用UI分析。如果你倾向命令行方式可以在设备的串口或ADB shell中执行adb shell perfetto --config :test --out /data/misc/perfetto-traces/trace.perfetto-trace前提是系统里已经内置了perfetto命令。Android 10以上Pixel设备基本都有部分厂商ROM可能会裁剪需要确认adb shell perfetto --version有输出。没有的话可以从perfetto官方发布页下载对应架构的二进制push进/data/local/tmp后赋予执行权限再通过adb shell调用。注意如果设备没有rootAndroid 11以上的某些trace数据源会受到权限限制比如内核的某些调度事件建议优先使用ro.debuggable1的userdebug固件或root设备抓完整数据。2.2 TraceConfigdiy一个够用且不炸的配置很多人第一次用perfetto的web UI会直接点“Start Recording”默认配置结果抓了30秒文件2GB拖进UI卡死。问题不在工具在于配置里所有数据源全开了。合理的TraceConfig既能拿到关键线索又能控制体积。一个比较平衡的配置长这样// config.pbtx buffers { size_kb: 262144 // 256MB fill_policy: RING_BUFFER } data_sources { config { name: linux.ftrace ftrace_config { ftrace_events: sched/sched_switch ftrace_events: sched/sched_wakeup ftrace_events: sched/sched_waking ftrace_events: power/cpu_frequency ftrace_events: power/cpu_idle ftrace_events: binder/binder_transaction ftrace_events: binder/binder_transaction_received ftrace_events: binder/binder_lock ftrace_events: binder/binder_locked ftrace_events: binder/binder_unlock ftrace_events: tracing/mark_print ftrace_events: mm_vmscan/mm_vmscan_direct_reclaim_begin ftrace_events: mm_vmscan/mm_vmscan_direct_reclaim_end ftrace_events: sched/sched_process_exit } } } data_sources { config { name: android.process_stats } } data_sources { config { name: android.surfaceflinger.frametimeline } } duration_ms: 10000这里的核心思路是只抓sched_switch线程切换、sched_wakeup/waking谁唤醒了谁、cpu_frequencyCPU频率变化、binder_*Binder调用、tracing/mark_print应用自定义Trace标记以及多一个SurfaceFlinger的帧时间线数据源。这套组合基本覆盖了“主线程在做什么、CPU是否降频、Binder对端是谁、帧边界在哪”这四类关键证据。2.3 复现节奏的控制让trace命中那一次卡顿抓取perfetto容易难的是让trace刚好覆盖到卡顿发生的那一帧。以列表滑动为例我常用的手法是在Perfetto UI或命令行里设置duration_ms: 15000也就是15秒先花3秒进入目标页面但不操作让系统把该加载的东西加载完第4秒开始进行“匀速、可重复”的滑动操作滑到第12秒停止最后留3秒收尾观察卡顿恢复的过程。关键是操作必须可重复、动作恒定。如果一边抓trace一边思考“怎么滑动”复现出来的卡顿很可能和用户场景不是同一种。更讲究一点可以在复现前打开开发者选项里“显示Surface更新”或开启“GPU呈现模式分析”作为辅助观察但真正定位还是以perfetto的数据为准。另外开发调试阶段建议把“窗口动画缩放”“过渡动画缩放”“Animator时长缩放”保持默认的1x不要为了省事全部关掉。有些卡顿恰恰发生在动画期间你全关掉就复现不出来了。关动画这个操作只适合在确认“非动画路径的性能问题”时使用。3. 打开trace后的第一件事先读帧再读线程3.1 快捷键和视图布局把工具当“放大镜”而不是“看板”Perfetto UI打开trace文件后默认是一大片密密麻麻的轨道。新手的第一反应往往是鼠标乱滚试图找出“红色的位置”。这个方向完全错误。合理顺序是先看整体帧分布再逐层放大。关键快捷键w放大s缩小a/d左右移动时间窗口m在当前位置打标记多打几个标记就能测量两个标记之间的准确时长1、2、3等数字键切换不同颜色主题的轨道高亮CtrlF搜索slice名称或者进程名。帧时间线通常显示在时间轴顶部附近SurfaceFlinger的Display Composer或者FrameTimeline区域会有每一帧的起止时间。如果是支持FrameTimeline的设备会直接看到jank标记或帧耗时柱状图非常直观。3.2 先回答三个问题卡在哪一帧、哪条线程、什么状态拿到任何一份trace我先要求自己回答以下三个问题回答不上来就继续看不看别的用户感知的“卡顿”具体对应哪一帧到哪一帧帧间隔是多少卡顿发生时目标App的主线程通常是Choreographer所在的UI Thread或进程名里的main在做什么是长时间运行还是在等待如果主线程在等待等待什么是等Binder返回等情况通知还是等锁这三个问题回答完卡顿的大致区域就被框定出来了。剩下的工作才是“找具体函数”“看调用栈”那是精细活。很多人在第一步就没做上来就抓着主线程一个长slice问“这是什么”就像一个人丢了钥匙不去回忆丢钥匙的时间线只盯着手里最后一截钥匙链看当然找不到。线程状态的颜色在这里很有用绿色通常是运行态蓝色是可运行但没被调度到橙色/红色是陷入内核态等待通常是锁、IO等不可中断等待。如果主线程在卡顿时呈现蓝色或者橙红色那么“主线程自身CPU占用高”这个推断就不成立应该立刻把注意力转移到“谁占用了CPU”、“主线程在等谁”上。3.3 常见误区卡顿不等于主线程有个长slice有一种卡顿模式极具迷惑性——主线程没有任何超过50ms的slice但用户就是觉得掉帧。这种时候原因往往在渲染管线下游CPU很快把UI树构建完成并提交但GPU合成跟不上或SurfaceFlinger合成阻塞或因为垂直同步频率和内容刷新不匹配。此时再去主线程里找“大活”注定一无所获。所以“先读帧再读线程”不是说主线程不重要而是强调顺序先知道卡顿发生在哪一帧再看那一帧里的所有关键线程而不是只盯着主线程看“最长的slice”。帧是结果线程是原因顺序反了就会被表象带偏。4. 三种卡顿形态的区分方法4.1 形态A主线程“真大活”——slice又长又深这是最容易被新手识别的卡顿类型。主线程轨道上出现一个或几个持续时间很长的slice颜色通常偏深因为内部的调用栈层次很深。常见原因包括复杂布局的measure/layout耗时过长、大量Bitmap解码、主线程直接做IO、JSON解析大数据、锁竞争导致任意代码块从“微秒级”拖到“几十毫秒级”。判断方法很简单把卡顿帧对齐到主线程轨道找到覆盖帧时间段的最长slice读它的名称。如果slice名称是一个函数名比如performMeasure或LinearLayout.onMeasure那方向基本就定了。进一步确认可以在这个slice上停留查看Wall duration和Self time。Self time越大说明不是子调用拖累而是函数自身逻辑就有问题Self time很小但整体slice长说明是某个子调用最慢继续下一层分析。4.2 形态B主线程在“等”——如果slice短小且伴随wait状态这类的特征是主线程slice都不长但线程状态长期是蓝色runnable但没上CPU或橙色不可中断等待。这里的核心问题是“谁抢占了CPU”或“主线程在等哪个锁/Binder返回”。看这种问题我习惯先把主线程轨道展开看它在卡顿时间段前后的线程状态颜色变化然后使用perfetto的Sched相关的CPU跟踪找到同一时间段内谁在对应的CPU core上运行。如果看到CPU被一个高优先级/同优先级的渲染线程占满那就需要考虑降优先级、减少工作量或迁移线程。如果主线程是橙色则查看内核栈通常能看到mutex_lock或者wait_for_completion之类的字样再配合Binder事件轨道就能顺藤摸瓜找到对端。Binder的场景尤其典型主线程发起一个transact调用slice显示很短但紧接着就是长时间的等待。perfetto的Binder轨道会显示binder_transaction的target进程和线程这时候去target进程的线程栈里看它为什么迟迟不返回往往才是问题真正的现场。4.3 形态C渲染侧卡顿——主线程没毛病RenderThread和SF在忙这类卡顿最容易被误判为“没有问题”。主线程看起来从容不迫但FrameTimeline显示掉帧RenderThread轨道上出现大段的等待或长task。常见的渲染侧锅包括GPU等待上一个帧完成显式/隐式同步、着色器编译引起的首帧卡顿、过度绘制导致GPU片元负载过高、SurfaceView或TextureView的转换开销、SurfaceFlinger那边的HWC合成超时。这一块的分析需要切换到RenderThread轨道看有没有sync、queue、waitForPresent之类的节点。如果能看到GPU完成的fence时间戳对比CPU提交时间就能判断瓶颈在CPU侧还是GPU侧。还有一种情况值得单独提一下当Window是SurfaceView类型时App的绘制并不完全走常规的View渲染管线而是App自己往一个独立的Surface上画。此时卡顿可能来自SurfaceView的缓冲排队——App画完一帧但显示管线未及时取走导致生产者端阻塞。这类问题在视频、相机、游戏场景特别常见排查时记得打开android.surfaceflinger.frametimeline看哪一个Layer的呈现一直不刷新。4.4 形态对照表卡顿形态主线程特征RenderThread特征关键证据在哪个轨道优先排查方向A 主线程大活长slice、深调用栈跟随排队主线程slice详情布局、IO、算法复杂度B 主线程等待slice短、蓝色/橙色状态不一定异常调度轨道、Binder轨道锁竞争、Binder对端、CPU抢占C 渲染侧卡顿无明显长slice长task或等待fenceFrameTimeline、RenderThreadGPU瓶颈、着色器编译、合成延迟这张表是我自己做排查时的速查表。遇到卡顿先按表对照一遍能省掉很多无头苍蝇式搜索。5. 一个完整的卡顿定位实战链路5.1 场景复现列表滑动掉帧、偶现、Logcat无Error讲一个实际发生过的案例。项目里反馈某个二级列表页快速上下滑动时偶现掉帧不是每次都能复现频率大约每五次操作出现一次。日志里没有Exception也没有明显的ANR。一开始大家以为是数据加载的问题在列表adapter里加了一堆日志滑了半小时什么也没抓到。我用perfetto替换了排查方式。TraceConfig按上文那份配置时长15秒滑动了大概8次。打开trace后第一步先在FrameTimeline里找掉帧区段很快就发现一次连续三帧耗时都超过40ms的区间集中在第7秒到第7.5秒之间。5.2 逐步收紧从帧区间到主线程到Binder对端定位到这个区间后我把事件窗口压缩到500ms范围然后把主线程轨道放大。主线程在这500ms里其实非常干净各类slice都不超过2ms说明UI线程没有在做重活。但它的线程状态出现了连续的大段蓝色。这就触发了形态B的判断逻辑。于是我把注意力切到CPU调度轨道。卡顿的这500ms里好几个小核CPU频率都在最低档而大核上有一个名为dex2oat的进程占用了接近整整300ms。这一瞬间原因其实已经很清楚了ART的AOT编译在高负载滑动期间占满了大核同时把CPU频率拉到一个“看似不低但对渲染线程不够友好”的状态主线程虽然是runnable但一直排不上队。顺着这个方向继续查为什么滑到第7秒才触发dex2oat看slice里的进程名、命令行参数确认是com.xxx.app自己的dex2oat在后台执行。原因是应用启动后第二屏里的某个动态特性触发了新Dex文件的加载和编译后台编译器抢占了CPU。5.3 修复与验证限制后台编译、错峰加载问题的修复方案并不需要在代码里“优化布局”或者“减少绘制”而是要从调度策略上下手一类做法是把后台编译任务的优先级调低避免和UI抢占大核另一类是主动预编译热点模块避免在运行时触发dex2oat更简单直接的是在应用启动后、用户开始滑动前先让系统完成一次空闲期的Dex优化或者把相关Dex文件的编译滤波器设置成“speed-profile”。修复后我用同样的TraceConfig重新抓了一次同样的滑动序列FrameTimeline里的掉帧区段消失主线程等待时间大幅缩短。这个case的结论告诉我们卡顿根因不一定在本进程的代码里可能是系统服务的调度策略干扰。如果不用perfetto靠打日志很难定位到这种“跨进程、跨调度”的根因。5.4 证据链的整理怎么让结论可追溯实战里还有一件常被忽略的事——把排查过程变成可追溯的证据链。我的习惯是在perfetto UI里用m打上标记标记出掉帧起始帧号、主线程等待区间的起止时间、dex2oat进程占用的区间然后用Perfetto的下载标记功能把这张trace保存成带注释的版本方便回到办公室继续分析或发给同事确认。更重要的是把结论写进缺陷单。比如在Jira描述里附上这样一段掉帧区间2026-01-10 15:23:07.000 - 15:23:07.500帧号9876。 主线程状态runnable uninterruptible sleep交替无长slice。 CPU占用对象pid 12345 (dex2oat)小核最低频大核100%占用约300ms。 根因运行时Dex编译抢占大核渲染线程调度被延迟。 修复调整Dex编译策略验证后掉帧消失。这种证据链的价值在于哪怕三个月后有人重新回来问“当初这个卡顿到底是怎么解决的”你不需要重新复盘一遍代码只凭trace和注释就能恢复完整现场。6. 大型项目里的抓取与排查技巧6.1 别全开事件按需裁剪大型项目通常功能多、进程多、系统版本杂。如果每个开发都按默认配置抓几台的trace一合并存储和上传都吃不消。我建议给团队订一条规则除非明确要排查跨进程调度或功耗相关问题否则TraceConfig里的事件按需裁剪能关就关。推荐保留的最小集sched_switchsched_wakeup看调度和线程切换cpu_frequencycpu_idle看是否降频锁频tracing/mark_printApp侧自定义slice标记android.surfaceflinger.frametimeline看帧边界binder_transactionbinder_transaction_received看跨进程调用。这组配置抓出来的trace体积小可读性高日常开发排查足够。有些团队还专门维护一份“面向UI线程卡顿排查”的预设配置内部称为“性能问题第一现场包”新人入职先学会抓这个包再看三日内的固定案例上手效率提升明显。6.2 用SQL把trace当成数据库来查perfetto UI自带的Query分析功能是非常值钱的高级能力。它允许你用SQL查询trace里的slice、线程、调度信息把“肉眼搜索”变成“条件过滤”。比如我想找出所有持续时间超过10ms的UI主线程sliceSELECT ts, dur, t.name AS thread_name, s.name AS slice_name, s.depth FROM slice s LEFT JOIN thread_track t ON s.track_id t.id WHERE t.name main AND dur 10e6 ORDER BY dur DESC LIMIT 50;再比如我想查看某个时间窗口内Binder调用的平均耗时和目标进程分布SELECT process.name AS target_process, COUNT(*) AS call_count, AVG(s.dur) / 1e6 AS avg_duration_ms FROM slice s LEFT JOIN thread_track tt ON s.track_id tt.id LEFT JOIN thread th ON tt.utid th.utid LEFT JOIN process ON th.upid process.upid WHERE s.name GLOB binder transaction* GROUP BY process.name ORDER BY avg_duration_ms DESC;这类SQL查询的应用场景非常广泛查GC次数、查某个自定义标记的频率、查长时间持锁的函数等。你完全可以把perfetto当成一个性能数据仓库而不仅仅是图形查看器。团队里如果有人对这个不熟强烈建议花一个下午专门练一练这是性价比极高的一笔投入。6.3 定向抓取同一帧多进程协同分析大型项目跑起来后一次交互往往涉及十几个进程。如果一次全抓不仅文件巨大分析时也会因信息过载而降低效率。此时更推荐定向抓取比如配置android.process_stats和ftrace时通过target_android_process或pid限定只抓目标App、system_server、surfaceflinger这三个进程。但需要注意如果怀疑卡顿和其他App争抢资源有关比如大量后台进程占用CPU或IO则不能只抓目标App否则看不到“谁在抢”。这种情况我通常会用一次不带进程过滤的全量短抓取时长控制在5秒以内专门用于观察“那几秒内到底有哪些进程在活跃”。拿到结论后再回到定向抓取做深度分析。6.4 分享与协作trace文件本身就是沟通语言我发现很多团队在线上问题协作上效率低一个很大原因是沟通时没有共同语言。产品说“感觉卡”QA说“复现不了”开发说“我这边跑着没问题”。而perfetto trace天然是一个客观的、可共享的中间物。在实际操作中我们内部推荐的做法是抓完trace后直接上传到perfetto UI的分享功能或者放到公司内部的共享盘然后给相关同事发一个链接加一段简短的“看哪一帧、哪条线程”指引。对方打开就能看到同一份证据而不是拿着手机录屏反复看。更进阶的玩法是自动化在CI或测试机集群上预先部署一个抓trace的小工具测试工程师一旦发现自己负责的模块出现掉帧一键就会把当前30秒的trace和log一并打包上传。开发拿到后直接做后验分析。这套流程我们在实际项目中运行了近一年挽回的排查时间非常可观。写在最后的一点经验用perfetto包括systrace分析卡顿说到底是一个“取证”的过程不是“猜谜”的过程。工具给到的每一个slice、每一段线程状态、每一次Binder调用都是客观的现场物证。如果你现在还在靠感觉、靠日志、靠代码走读去猜卡顿原因我建议你在下一个问题出现时至少先尝试抓一份trace按照本文的顺序先看帧、再看线程、再区分形态最后落成证据链。相信我一旦习惯了这种工作方式你很难再回到从前那种“看着代码想到底哪里慢”的日子。
返回列表