如何使用 Perfetto 分析应用的主线程阻塞点?
解读
国内面试场景下,这道题常被用来区分“只会用 Android Studio Profiler 点两下”与“能在复杂卡顿工单中快速定位真凶”的候选人。Perfetto 作为 AOSP 官方 Trace 旗舰工具,已取代 Systrace 成为性能根因分析“终极武器”。面试官期望听到:
- 能讲清 Trace 采集、解析、可视化全流程;
- 能在 10 秒内从 100 M 的 trace 文件里定位到“主线程 Runnable 但长时间未调度”或“Binder 调用被锁”这类阻塞根因;
- 能结合国内 ROM 特性(如厂商后台冻结、权限弹窗、MIUI/Flyme 定制服务)解释异常调度;
- 最终给出可落地的修复策略(线程优先级、锁粒度、IO 异步化、厂商接口替换等)。
知识点
- Perfetto 架构:
- 内核层:ftrace/atrace 事件源,含 sched_switch、binder、block 等;
- 用户态: traced 守护进程,支持 SQL 查询、零拷贝 protobuf 输出;
- UI:ui.perfetto.dev 或 Android Studio Chipmunk+ 内置 Perfetto 插件。
- 主线程阻塞常见信号:
- sched_switch 中 prev_state=D(不可中断睡眠)或 prev_state=R 但长时间未入 cpu;
- binder 调用对端 TID 阻塞;
- lock contention 事件(atrace 标签 A|B|C 段内嵌套 lock);
- 内存回收阻塞:kswapd、ion_heap_shrink;
- 国内厂商定制:权限弹窗 Activity 拉起导致主线程 waitForActivityStart。
- 关键 SQL:
select ts,dur,track_id,slice.name from slice where slice.name like ‘Choreographer#doFrame%’ and dur>16000000; - 国内采集注意:
- 需关闭厂商“性能模式”与“内存扩展”,否则调度被改写;
- 部分 ROM 默认关闭 ftrace,需 adb shell setprop persist.traced.enable 1 并重启;
- 高版本微信/支付宝自身带 trace whitelist,需加 -a 包名强制注入。
- 修复套路:
- 16 ms 外帧:拆分重计算到 DefaultDispatcher + withContext;
- Binder 阻塞:缓存系统服务调用结果,或使用 ContentProviderClient 一次性批处理;
- IO 阻塞:提前预取 + mmap,或转到 IO 线程池;
- 锁:将 synchronized 改为 StampedLock/readwritelock,或使用 Kotlin Mutex 限定持有时间;
- 厂商冻结:申请 PROCESS_STATE_TOP 或前台服务,适配各厂商“自启动”白名单。
答案
步骤一:环境准备
- 手机:国内零售版需先解锁 adb 权限,关闭“内存扩展”“性能省电”开关;
- PC:安装 Android SDK 最新版,确保 adb、perfetto 命令行工具版本 ≥ v34;
- 采集命令:
adb shell perfetto \
-o /data/misc/perfetto-traces/trace.perfetto \
-t 10s \
-b 64mb \
--app com.example.demo \
-c - <<EOF
buffers { size_kb: 65536 fill_policy: DISCARD }
data_sources { config { name: "linux.ftrace" ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "power/suspend_resume"
ftrace_events: "binder/binder_transaction"
ftrace_events: "block/block_rq_issue"
atrace_apps: "com.example.demo"
atrace_categories: "gfx" "view" "am" "wm" "dalvik"
}}}
EOF
步骤二:可视化定位
- 拖入 ui.perfetto.dev,打开 “CPU” 与 “Android App” 轨道;
- 搜索 slice.name = ‘Choreographer#doFrame’,按 dur 降序,找到 >16 ms 的帧;
- 在该帧时间范围内切到 “CPU Scheduler” 轨道,查看主线程 utid 是否长时间处于 Runnable 但未上核(颜色为灰);
- 若 Runnable 持续 >5 ms,查看同一核上正在运行的线程名,若为 kswapd、ion_heap_shrink,则阻塞源为内存回收;若为 binder 线程,则跳转到 “Binder” 轨道,查看对端 TID;
- 双击对端 TID,跳转到其 slice,若发现正在执行 SharedPreferencesImpl.writeToFile(),则锁源为 XML 文件写;
- 若主线程状态为 D(深紫),查看 “Block” 轨道,确认是否等待 ext4 日志提交;
- 记录 ts、dur、阻塞线程名、函数栈(右键 Export SQL → 选中 callstack 表)。
步骤三:根因确认
- 将 trace 中得到的阻塞函数与 mapping.txt 反混淆,定位到业务代码;
- 结合代码确认:是否主线程直接调用 SharedPreferences.Editor.apply();是否使用 synchronized (Singleton) 做跨线程缓存;是否在 onCreate() 做 Assets 拷贝。
步骤四:修复与验证
- 将 IO 操作移至 Dispatchers.IO,apply() 改为 apply{} 并在后台线程提前提交;
- 锁改为 Kotlin Mutex,持有时间 <1 ms;
- Assets 拷贝使用 WorkManager 一次性任务,完成后发广播刷新 UI;
- 重新采集 Perfetto,验证同场景下主线程 Runnable 空窗 <2 ms,帧渲染时间 <10 ms;
- 输出报告:包含 trace 链接、SQL 查询、修改 commit id,附在工单中回归测试。
拓展思考
- 线上灰度如何无侵入采集?
利用 AOSP 新特性 TracedPerfettoProvider,通过 Firebase RemoteConfig 动态下发配置,用户端仅 2% 采样、3% 性能损耗,回传 trace 切片到 OSS,再自动化跑 SQL 聚合出 “>50 ms 帧” 的堆栈热图。 - 折叠屏 120 Hz 场景下阈值是否仍用 16 ms?
需按 vsync 周期 8.33 ms 重新设定,Perfetto SQL 改为 dur>8333333,同时关注 gpu 合成线程 “SurfaceFlinger” 的 dequeueBuffer 阻塞,避免因 GPU 调度差异导致掉帧。 - 国内厂商后台冻结导致主线程无法上核,如何区分是系统策略还是自身阻塞?
在 trace 中查看 cpu 频率与 cpuidle 状态,若冻结期间小核频率直接降到 0,且 logcat 出现 “ActivityManager: freeze uidxxx”,则属于厂商省电策略;可在企业合作渠道申请 “后台保活” 白名单,或使用厂商提供的 SDK 申请临时解冻接口。