如何使用 Perfetto 分析应用的主线程阻塞点?

解读

国内面试场景下,这道题常被用来区分“只会用 Android Studio Profiler 点两下”与“能在复杂卡顿工单中快速定位真凶”的候选人。Perfetto 作为 AOSP 官方 Trace 旗舰工具,已取代 Systrace 成为性能根因分析“终极武器”。面试官期望听到:

  1. 能讲清 Trace 采集、解析、可视化全流程;
  2. 能在 10 秒内从 100 M 的 trace 文件里定位到“主线程 Runnable 但长时间未调度”或“Binder 调用被锁”这类阻塞根因;
  3. 能结合国内 ROM 特性(如厂商后台冻结、权限弹窗、MIUI/Flyme 定制服务)解释异常调度;
  4. 最终给出可落地的修复策略(线程优先级、锁粒度、IO 异步化、厂商接口替换等)。

知识点

  1. Perfetto 架构:
    • 内核层:ftrace/atrace 事件源,含 sched_switch、binder、block 等;
    • 用户态: traced 守护进程,支持 SQL 查询、零拷贝 protobuf 输出;
    • UI:ui.perfetto.dev 或 Android Studio Chipmunk+ 内置 Perfetto 插件。
  2. 主线程阻塞常见信号:
    • sched_switch 中 prev_state=D(不可中断睡眠)或 prev_state=R 但长时间未入 cpu;
    • binder 调用对端 TID 阻塞;
    • lock contention 事件(atrace 标签 A|B|C 段内嵌套 lock);
    • 内存回收阻塞:kswapd、ion_heap_shrink;
    • 国内厂商定制:权限弹窗 Activity 拉起导致主线程 waitForActivityStart。
  3. 关键 SQL:
    select ts,dur,track_id,slice.name from slice where slice.name like ‘Choreographer#doFrame%’ and dur>16000000;
  4. 国内采集注意:
    • 需关闭厂商“性能模式”与“内存扩展”,否则调度被改写;
    • 部分 ROM 默认关闭 ftrace,需 adb shell setprop persist.traced.enable 1 并重启;
    • 高版本微信/支付宝自身带 trace whitelist,需加 -a 包名强制注入。
  5. 修复套路:
    • 16 ms 外帧:拆分重计算到 DefaultDispatcher + withContext;
    • Binder 阻塞:缓存系统服务调用结果,或使用 ContentProviderClient 一次性批处理;
    • IO 阻塞:提前预取 + mmap,或转到 IO 线程池;
    • 锁:将 synchronized 改为 StampedLock/readwritelock,或使用 Kotlin Mutex 限定持有时间;
    • 厂商冻结:申请 PROCESS_STATE_TOP 或前台服务,适配各厂商“自启动”白名单。

答案

步骤一:环境准备

  1. 手机:国内零售版需先解锁 adb 权限,关闭“内存扩展”“性能省电”开关;
  2. PC:安装 Android SDK 最新版,确保 adb、perfetto 命令行工具版本 ≥ v34;
  3. 采集命令:
    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

步骤二:可视化定位

  1. 拖入 ui.perfetto.dev,打开 “CPU” 与 “Android App” 轨道;
  2. 搜索 slice.name = ‘Choreographer#doFrame’,按 dur 降序,找到 >16 ms 的帧;
  3. 在该帧时间范围内切到 “CPU Scheduler” 轨道,查看主线程 utid 是否长时间处于 Runnable 但未上核(颜色为灰);
  4. 若 Runnable 持续 >5 ms,查看同一核上正在运行的线程名,若为 kswapd、ion_heap_shrink,则阻塞源为内存回收;若为 binder 线程,则跳转到 “Binder” 轨道,查看对端 TID;
  5. 双击对端 TID,跳转到其 slice,若发现正在执行 SharedPreferencesImpl.writeToFile(),则锁源为 XML 文件写;
  6. 若主线程状态为 D(深紫),查看 “Block” 轨道,确认是否等待 ext4 日志提交;
  7. 记录 ts、dur、阻塞线程名、函数栈(右键 Export SQL → 选中 callstack 表)。

步骤三:根因确认

  1. 将 trace 中得到的阻塞函数与 mapping.txt 反混淆,定位到业务代码;
  2. 结合代码确认:是否主线程直接调用 SharedPreferences.Editor.apply();是否使用 synchronized (Singleton) 做跨线程缓存;是否在 onCreate() 做 Assets 拷贝。

步骤四:修复与验证

  1. 将 IO 操作移至 Dispatchers.IO,apply() 改为 apply{} 并在后台线程提前提交;
  2. 锁改为 Kotlin Mutex,持有时间 <1 ms;
  3. Assets 拷贝使用 WorkManager 一次性任务,完成后发广播刷新 UI;
  4. 重新采集 Perfetto,验证同场景下主线程 Runnable 空窗 <2 ms,帧渲染时间 <10 ms;
  5. 输出报告:包含 trace 链接、SQL 查询、修改 commit id,附在工单中回归测试。

拓展思考

  1. 线上灰度如何无侵入采集?
    利用 AOSP 新特性 TracedPerfettoProvider,通过 Firebase RemoteConfig 动态下发配置,用户端仅 2% 采样、3% 性能损耗,回传 trace 切片到 OSS,再自动化跑 SQL 聚合出 “>50 ms 帧” 的堆栈热图。
  2. 折叠屏 120 Hz 场景下阈值是否仍用 16 ms?
    需按 vsync 周期 8.33 ms 重新设定,Perfetto SQL 改为 dur>8333333,同时关注 gpu 合成线程 “SurfaceFlinger” 的 dequeueBuffer 阻塞,避免因 GPU 调度差异导致掉帧。
  3. 国内厂商后台冻结导致主线程无法上核,如何区分是系统策略还是自身阻塞?
    在 trace 中查看 cpu 频率与 cpuidle 状态,若冻结期间小核频率直接降到 0,且 logcat 出现 “ActivityManager: freeze uidxxx”,则属于厂商省电策略;可在企业合作渠道申请 “后台保活” 白名单,或使用厂商提供的 SDK 申请临时解冻接口。