稳定性性能系列之八——系统性能分析基础:Systrace与Perfetto入门

0 阅读15分钟

性能优化的第一步,不是猜测问题在哪里,而是用数据说话。Trace工具就是那个让性能瓶颈无所遁形的"显微镜"。

引言:从3秒到1秒的优化历程

那是去年Q2的一个周一早晨,产品经理神色凝重地找到我:"用户反馈我们的App启动太慢了,平均3秒才能看到首页,竞品只要1秒。老板说如果这个季度优化不到1.5秒以内,项目可能要黄。"

我打开App试了试,确实慢得让人抓狂。但问题在哪里呢?Application初始化?Activity加载?网络请求?还是图片渲染?

凭感觉优化?那只是在盲人摸象。

于是我打开Systrace,抓取了一份启动Trace。当Chrome打开那个HTML文件的瞬间,整个启动流程一览无遗:

Application.onCreate(): 450ms
  ├─ SDK初始化: 280ms    ← 元凶!
  ├─ 数据库迁移: 120ms
  └─ 其他初始化: 50ms

Activity.onCreate(): 380ms
  ├─ setContentView(): 80ms
  ├─ 网络请求: 200ms      ← 在主线程!
  └─ RecyclerView首屏渲染: 100ms

问题一目了然:SDK初始化和网络请求占据了启动时间的一半。

经过两周的优化(SDK延迟初始化、网络请求移到后台、预加载优化),启动时间降到了0.9秒。产品经理再也没提过启动慢的问题,项目也顺利上线了。

这次经历让我明白:性能优化不是靠经验和直觉,而是靠数据。而Trace工具,就是获取这些数据的最强武器。

本文将带你全面掌握Systrace和Perfetto两大性能分析工具。读完本文,你将能够:

  1. 理解Systrace和Perfetto的工作原理和区别
  2. 掌握Trace抓取的完整流程和常用配置
  3. 学会分析Trace文件,快速定位性能瓶颈
  4. 使用代码插桩技术精确追踪关键路径
  5. 运用SQL查询进行高级性能分析

一、Trace工具概述

1.1 什么是Trace

Trace(跟踪) 是一种记录系统运行时事件序列的技术。简单来说,就是给系统"装个监控摄像头",记录下指定时间段内发生的所有关键事件。

Trace能记录什么?

类型内容用途
函数调用方法进入/退出时间分析哪个函数耗时长
线程调度线程运行/等待/睡眠状态定位线程阻塞问题
CPU调度哪个线程在哪个核心上运行CPU占用分析
渲染事件VSYNC、SurfaceFlinger绘制卡顿和掉帧分析
I/O操作文件读写、网络请求I/O性能分析
GC事件垃圾回收的时间和频率内存抖动分析
Binder调用跨进程通信IPC性能分析

Trace能解决什么问题?

启动慢 - 精确定位启动过程中的耗时操作
滑动卡顿 - 找出导致掉帧的函数调用
动画不流畅 - 分析渲染Pipeline的瓶颈
CPU占用高 - 识别CPU密集型操作
功耗高 - 定位频繁唤醒和长时间运行的任务

1.2 Systrace简介

Systrace是Google早期推出的系统级性能分析工具,从Android 4.1开始引入。

核心特点:

  • 基于Linux ftrace机制
  • 轻量级,对系统性能影响小
  • 以HTML文件呈现,使用Chrome Trace Viewer查看
  • 支持Android 4.1到Android 12的所有版本

工作原理:

08-01-trace-workflow.png

优势:

  • ✅ 简单易用,一行命令即可抓取
  • ✅ 兼容性好,支持老版本Android
  • ✅ 无需额外安装工具

局限性:

  • ❌ UI体验一般(基于Chrome插件)
  • ❌ 不支持SQL查询
  • ❌ 大文件加载慢
  • ❌ 功能相对基础

1.3 Perfetto简介

Perfetto 是Google从Android 10开始推出的新一代性能分析平台,旨在取代Systrace。

核心特点:

  • 现代化的Web UI(ui.perfetto.dev/)
  • 强大的SQL查询引擎
  • 支持Protobuf二进制格式,文件更小
  • 更丰富的数据源(内存、电量、网络等)
  • 向后兼容,可以打开Systrace文件

工作原理:

08-02-perfetto-architecture.png

优势:

  • ✅ 现代化UI,交互体验极佳
  • ✅ SQL查询引擎,支持复杂分析
  • ✅ 更丰富的数据源
  • ✅ 文件更小,加载更快
  • ✅ 持续更新,功能不断增强

局限性:

  • ⚠️ 主要支持Android 10+
  • ⚠️ 学习曲线稍陡(SQL查询)

1.4 工具对比

特性SystracePerfetto
推出时间Android 4.1 (2012)Android 10 (2019)
UI体验Chrome Trace Viewer现代Web UI ⭐⭐⭐⭐⭐
功能丰富度基础强大 ⭐⭐⭐⭐⭐
数据格式HTMLProtobuf (更小)
查询能力SQL查询 ⭐⭐⭐⭐⭐
数据源ftraceftrace + 内存 + 电量 + 网络
兼容性Android 4.1 - 12Android 9+ (最好10+)
文件大小较大更小
学习曲线简单中等
推荐使用Android 9及以下Android 10+ ⭐推荐

选择建议:

  • 如果你的项目主要支持Android 10+,强烈推荐Perfetto
  • 如果需要兼容Android 9及以下,使用Systrace
  • 两者可以共存,根据场景灵活选择

二、Systrace使用指南

2.1 环境准备

前置条件:

  1. ✅ Python 2.7+ 或 Python 3.x
  2. ✅ Android SDK Platform-Tools (adb命令)
  3. ✅ Chrome浏览器

Systrace位置:

# Android SDK中的位置
$ANDROID_HOME/platform-tools/systrace/systrace.py

# 验证Python环境
python --version  # 或 python3 --version

# 验证adb连接
adb devices

设备权限设置:

# 1. 开启开发者选项
# 设置 → 关于手机 → 连续点击"版本号"7次

# 2. 开启USB调试
# 设置 → 开发者选项 → USB调试

# 3. 授予adb root权限(如需要)
adb root
adb remount

2.2 抓取Systrace

基础用法:

# 进入systrace目录
cd $ANDROID_HOME/platform-tools/systrace

# 抓取10秒的trace
python systrace.py -o trace.html -t 10 -b 32768 \
  gfx input view webview wm am sm audio video camera hal \
  app res dalvik rs bionic power pm ss database network

# 参数说明:
# -o trace.html   : 输出文件名
# -t 10           : 抓取时长(秒)
# -b 32768        : buffer大小(KB),建议32MB以上
# -a package_name : 指定应用包名(可选)
# 后面是category列表

推荐配置(通用场景):

python systrace.py \
  -o trace.html \
  -t 10 \
  -b 32768 \
  sched freq idle am wm gfx view binder_driver hal \
  dalvik input res

2.3 常用Category说明

Category说明典型用途
schedCPU调度事件必选,分析线程运行状态
freqCPU频率变化CPU性能分析
idleCPU idle状态功耗分析
gfxSurfaceFlinger, VSYNC, Texture渲染分析必选
inputInput事件分发触摸响应分析
viewView绘制(measure/layout/draw)卡顿分析必选
webviewWebView性能H5页面分析
wmWindow Manager窗口管理分析
amActivity Manager生命周期启动分析必选
smSync Manager同步操作分析
audioAudio播放音频卡顿分析
videoVideo解码视频性能分析
cameraCamera操作相机性能分析
halHAL层事件底层硬件调用
app应用自定义trace插桩分析必选
res资源加载资源加载性能
dalvikGC事件内存抖动分析必选
rsRenderScript图像处理分析
bionicBionic库调用Native性能分析
powerPower HAL电量分析
pmPackage Manager安装/卸载分析
ssSystem Server系统服务分析
databaseSQLite操作数据库性能分析
network网络I/O网络性能分析
binder_driverBinder驱动IPC分析必选

场景推荐配置:

# 启动性能分析
python systrace.py -o launch.html -t 10 -a com.example.app \
  sched freq idle am wm gfx view binder_driver hal dalvik input res

# 滑动卡顿分析
python systrace.py -o scroll.html -t 10 -a com.example.app \
  sched gfx view input wm dalvik app

# 渲染性能分析
python systrace.py -o render.html -t 10 -a com.example.app \
  sched freq gfx view wm

# 内存抖动分析
python systrace.py -o memory.html -t 10 -a com.example.app \
  sched dalvik app

# 全面分析(文件会很大)
python systrace.py -o full.html -t 10 -a com.example.app \
  sched freq idle am wm gfx view webview binder_driver hal \
  dalvik input res power database network app

2.4 实战演练:抓取应用启动Trace

完整的启动Trace抓取流程:

# Step 1: 确定应用包名
adb shell pm list packages | grep example
# 输出: package:com.example.myapp

# Step 2: 清理应用数据(模拟首次启动)
adb shell pm clear com.example.myapp

# Step 3: 启动Systrace抓取(不要立即启动App)
cd $ANDROID_HOME/platform-tools/systrace
python systrace.py -o launch.html -t 10 -a com.example.myapp \
  sched freq idle am wm gfx view binder_driver hal dalvik input res &

# 注意: 最后的 & 让命令在后台运行

# Step 4: 等待2秒,让systrace准备好
sleep 2

# Step 5: 启动应用并记录启动时间
adb shell am start -W com.example.myapp/.MainActivity

# 输出示例:
# Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.myapp/.MainActivity }
# Status: ok
# Activity: com.example.myapp/.MainActivity
# ThisTime: 1245      ← Activity启动时间
# TotalTime: 1245     ← 总启动时间
# WaitTime: 1267      ← 等待时间
# Complete

# Step 6: 等待systrace抓取完成(10秒)
# 会自动生成 launch.html

# Step 7: 使用Chrome打开trace文件
google-chrome launch.html
# 或
open -a "Google Chrome" launch.html

Tip: 为了抓取到完整的启动流程,建议trace时长设置为15-20秒,确保从应用冷启动到首屏渲染完成都被记录。

2.5 查看Systrace

1. 打开Trace文件:

  • 使用Chrome浏览器打开trace.html
  • 文件较大时可能需要等待几秒加载

2. 界面布局:

┌──────────────────────────────────────────────────┐
│  [?] [W][A][S][D]  搜索:______  时间范围:___     │ 工具栏
├──────────────────────────────────────────────────┤
│  CPU 0                                            │
│  CPU 1CPU核心
│  CPU 2                                            │
│  CPU 3                                            │
├──────────────────────────────────────────────────┤
│  com.example.myapp                                │ 应用进程
│    └─ main                  ████████████████      │ 主线程
│    └─ RenderThread          ██████                │ 渲染线程
│    └─ binder:xxx            ███                   │ Binder线程
├──────────────────────────────────────────────────┤
│  system_server                                    │ 系统进程
│    └─ android.fg            █████████             │
│    └─ android.ui            ████                  │
├──────────────────────────────────────────────────┤
│  surfaceflingerSurfaceFlinger
│    └─ surfaceflinger        ████████████          │
└──────────────────────────────────────────────────┘

3. 基本操作:

操作快捷键/方法说明
放大W 或 鼠标滚轮上滚放大时间轴
缩小S 或 鼠标滚轮下滚缩小时间轴
左移A向左移动时间轴
右移D向右移动时间轴
选择区域鼠标左键拖拽选中一段时间范围
查看详情点击色块查看函数调用详情
搜索Ctrl+F 或 搜索框搜索函数名
高亮点击函数名高亮所有同名函数
标记M添加书签标记

4. 查看函数详情: 点击一个色块后,右侧会显示详细信息:

Title: onCreate
Wall Duration: 450ms         实际耗时
CPU Duration: 420ms          CPU占用时间
Self Time: 50ms              自身耗时(不含子调用)
Start: 1234.567ms            开始时间
Args:                        参数信息
  thread_name: main
  process_name: com.example.myapp

5. 分析技巧:

  • 🔍 先缩小查看整体,识别大块耗时
  • 🔍 再放大查看细节,定位具体函数
  • 🔍 关注主线程的空白区域(可能在等待)
  • 🔍 查看CPU调度,识别线程阻塞
  • 🔍 对比VSYNC信号,分析掉帧原因

三、Perfetto使用指南

3.1 三种使用方式

Perfetto提供了三种抓取和分析trace的方式:

方式1: Web UI (⭐推荐)

优点: 无需安装,功能完整,体验最佳
缺点: 需要网络连接

使用步骤:

1. 打开浏览器访问 https://ui.perfetto.dev/
2. 点击"Record new trace"
3. 连接设备 (会提示授权adb)
4. 配置抓取参数
5. 点击"Start Recording"
6. 执行待分析的操作
7. 点击"Stop"并下载trace文件

方式2: 命令行

优点: 适合自动化脚本,CI集成
缺点: 需要手动配置

使用步骤:

# 1. 下载Perfetto工具(如果设备上没有)
adb shell perfetto --version
# 如果没有,从 https://get.perfetto.dev/perfetto 下载

# 2. 推送配置文件到设备
adb push config.pbtx /data/local/tmp/

# 3. 开始抓取
adb shell perfetto \
  -c /data/local/tmp/config.pbtx \
  -o /data/local/tmp/trace.perfetto-trace

# 4. 导出trace文件
adb pull /data/local/tmp/trace.perfetto-trace ./

方式3: Android Studio集成

优点: 与开发环境集成,操作简单
缺点: 功能相对基础

使用步骤:

1. 打开Android Studio
2. View → Tool Windows → Profiler
3. 点击"+"添加Session
4. 选择设备和进程
5. 点击"CPU" → "System Trace"
6. 点击"Record"开始抓取
7. 执行操作后点击"Stop"
8. 查看trace (会自动用Perfetto UI打开)

3.2 Web UI抓取流程详解

让我们详细讲解最推荐的Web UI方式:

Step 1: 打开Perfetto UI

浏览器访问: https://ui.perfetto.dev/
建议使用Chrome或Edge浏览器

Step 2: 选择Record new trace 点击左上角的"Record new trace"按钮

Step 3: 连接设备

  • 确保adb已连接: adb devices
  • 在"Target"下拉框中选择你的设备
  • 首次使用会提示安装"Perfetto UI ADB Bridge",点击同意

Step 4: 配置抓取参数

# Recording Settings配置项:

Duration: 10000 ms          # 抓取时长
Buffer size: 32768 KB       # 缓冲区大小

# Probes (数据源)选择:

 Scheduling details       # CPU调度(必选)
 CPU frequency and idle states  # CPU频率
 Syscalls                 # 系统调用
 Process tracking         # 进程跟踪
 Frame timeline           # 帧时间线(渲染分析必选)
 Memory (RSS, PSS, etc)   # 内存统计
 Battery drain & power rails  # 电量分析

# Advanced settings:

Atrace categories:          # 选择category
   gfx
   input
   view
   am
   wm
   dalvik

App name: com.example.myapp  # 指定应用包名

Step 5: 开始抓取

  • 点击"Start Recording"按钮
  • 等待初始化(2-3秒)
  • 执行你要分析的操作(启动App、滑动列表等)

Step 6: 停止并下载

  • 点击"Stop and get trace"
  • 等待数据传输(取决于trace大小)
  • Trace会自动在浏览器中打开
  • 可以点击"Download"保存到本地

3.3 命令行抓取

对于自动化场景,命令行抓取更适合。

创建配置文件 config.pbtx:

# Perfetto配置文件格式 (Protocol Buffer Text)

buffers {
  size_kb: 32768           # 缓冲区32MB
  fill_policy: RING_BUFFER # 环形缓冲区策略
}

# 数据源配置
data_sources {
  config {
    name: "linux.ftrace"
    ftrace_config {
      # ftrace事件
      ftrace_events: "sched/sched_switch"
      ftrace_events: "sched/sched_wakeup"
      ftrace_events: "sched/sched_wakeup_new"
      ftrace_events: "sched/sched_waking"
      ftrace_events: "power/suspend_resume"
      ftrace_events: "power/cpu_frequency"
      ftrace_events: "power/cpu_idle"

      # atrace categories
      atrace_categories: "gfx"
      atrace_categories: "view"
      atrace_categories: "am"
      atrace_categories: "wm"
      atrace_categories: "input"
      atrace_categories: "dalvik"

      # 指定应用
      atrace_apps: "com.example.myapp"
    }
  }
}

# 抓取时长
duration_ms: 10000  # 10秒

抓取脚本 capture_trace.sh:

#!/bin/bash

# 配置
APP_PACKAGE="com.example.myapp"
CONFIG_FILE="config.pbtx"
OUTPUT_FILE="trace_$(date +%Y%m%d_%H%M%S).perfetto-trace"

echo "=== Perfetto Trace Capture ==="
echo "App: $APP_PACKAGE"
echo "Output: $OUTPUT_FILE"

# 1. 推送配置文件
echo "[1/4] Pushing config..."
adb push $CONFIG_FILE /data/local/tmp/config.pbtx

# 2. 清理应用数据(可选)
echo "[2/4] Clearing app data..."
adb shell pm clear $APP_PACKAGE

# 3. 开始抓取(后台运行)
echo "[3/4] Starting trace capture..."
adb shell perfetto \
  -c /data/local/tmp/config.pbtx \
  -o /data/local/tmp/trace.perfetto-trace &

PERFETTO_PID=$!

# 等待perfetto启动
sleep 2

# 4. 启动应用
echo "[4/4] Launching app..."
adb shell am start -W $APP_PACKAGE/.MainActivity

# 等待perfetto完成
wait $PERFETTO_PID

# 5. 导出trace
echo "Pulling trace file..."
adb pull /data/local/tmp/trace.perfetto-trace $OUTPUT_FILE

# 清理设备上的文件
adb shell rm /data/local/tmp/trace.perfetto-trace
adb shell rm /data/local/tmp/config.pbtx

echo "✅ Trace saved to: $OUTPUT_FILE"
echo "Open it at: https://ui.perfetto.dev/"

使用脚本:

chmod +x capture_trace.sh
./capture_trace.sh

3.4 Perfetto UI详解

整体界面布局:

08-03-perfetto-ui.png

核心功能区域:

  1. Timeline (时间轴)

    • 显示整个trace的时间范围
    • 可以拖拽选择时间区间
    • 双击可以跳转到该时间点
  2. Pinned Tracks (置顶轨道)

    • Frame Timeline: 每一帧的渲染情况
      • 绿色: 正常帧 (< 16.6ms)
      • 黄色: 轻微掉帧 (16.6-33ms)
      • 红色: 严重掉帧 (> 33ms)
    • 可以添加其他重要轨道
  3. CPU轨道

    • 显示每个CPU核心的使用情况
    • 可以看到线程在哪个核心上运行
    • 颜色代表不同的进程
  4. 进程和线程轨道

    • 每个进程可以展开查看线程
    • 线程上的色块代表函数调用
    • 点击色块查看函数详情
  5. 详情面板 (右侧或底部)

    • 显示选中slice的详细信息
    • 包含Wall Duration、CPU Time、Stack Trace
    • 可以查看参数和属性

常用操作:

操作方法
放大/缩小鼠标滚轮 或 触控板双指缩放
平移按住Shift+拖拽 或 触控板双指滑动
选择区域按住左键拖拽
查看详情点击slice(色块)
搜索点击顶部搜索图标 或 按/
测量时间按住Shift点击两个时间点
添加标记M
回到全景双击时间轴空白处

Frame Timeline分析:

Frame Timeline是Perfetto最强大的功能之一,用于分析渲染性能。

08-04-frame-timeline-stages.png

分析每一帧:

  • 绿色帧: 正常,< 16.6ms
  • 黄色帧: 轻微掉帧,16.6-33ms (掉1帧)
  • 红色帧: 严重掉帧,> 33ms (掉2帧以上)

点击一个帧,可以看到该帧的详细时间分解,快速定位瓶颈在哪个阶段。

3.5 Perfetto SQL查询入门

Perfetto最强大的功能之一就是SQL查询引擎,可以对trace数据进行复杂的分析。

打开SQL查询界面:

  1. 在Perfetto UI中打开trace文件
  2. 点击右上角的"Query (SQL)"按钮
  3. 在SQL编辑器中输入查询语句

常用表结构:

表名说明主要字段
slice所有函数调用ts, dur, name, depth, track_id
thread线程信息utid, tid, name, upid
process进程信息upid, pid, name
thread_track线程轨道映射id, utid
schedCPU调度信息ts, dur, cpu, utid
counter计数器数据ts, value, track_id

示例查询:

-- 查询主线程的所有函数调用
SELECT
  ts / 1e9 AS start_sec,
  dur / 1e6 AS duration_ms,
  name
FROM slice
JOIN thread_track ON slice.track_id = thread_track.id
JOIN thread ON thread_track.utid = thread.utid
WHERE thread.name = 'main'
  AND dur > 10e6  -- 只看耗时超过10ms的
ORDER BY dur DESC
LIMIT 20;

-- 查询GC事件
SELECT
  ts / 1e9 AS time_sec,
  dur / 1e6 AS duration_ms,
  name AS gc_type
FROM slice
WHERE name LIKE 'Heap%' OR name LIKE 'GC%'
ORDER BY ts;

-- 查询CPU使用率
SELECT
  thread.name,
  SUM(sched.dur) / 1e9 AS cpu_time_sec
FROM sched
JOIN thread USING (utid)
WHERE thread.name LIKE '%myapp%'
GROUP BY thread.name
ORDER BY cpu_time_sec DESC;

后续章节我们会详细介绍更多高级查询技巧。


四、Trace分析实战

掌握了工具的使用,接下来就是最核心的部分:如何分析Trace文件,快速定位性能瓶颈

4.1 分析思路框架

不管分析什么性能问题,都可以遵循这个系统化的思路:

08-05-trace-analysis-flow.png

4.2 常见性能指标

在分析Trace时,需要关注以下关键指标:

1. Wall Duration (墙上时间)

  • 函数从开始到结束的实际时间
  • 包含CPU执行时间和等待时间
  • 这是用户感知到的时间

2. CPU Time (CPU时间)

  • 函数实际占用CPU的时间
  • 不包含等待、睡眠等时间
  • 如果 CPU Time << Wall Duration,说明大部分时间在等待

3. Self Time (自身时间)

  • 函数自身的执行时间,不包含调用子函数的时间
  • 用于识别真正的瓶颈函数

4. Thread State (线程状态)

  • Running (R): 正在CPU上执行
  • Runnable (R): 就绪状态,等待CPU调度
  • Sleeping (S): 睡眠,等待事件
  • Uninterruptible Sleep (D): 不可中断睡眠,通常是I/O等待
  • Stopped (T): 停止状态

帧渲染指标:

一帧的理想时间分配 (60fps, 16.6ms):
┌────────────────────────┐
 Input:        2-3ms      处理触摸事件
 Animation:    1-2ms      执行动画
 Measure:      2-3ms      测量View尺寸
 Layout:       1-2ms      布局View位置
 Draw:         2-3ms      绘制View
 RenderThread: 4-5ms      渲染线程处理
 GPU:          2-3ms      GPU渲染
├────────────────────────┤
 Total:       14-18ms     总耗时
└────────────────────────┘

4.3 案例1: 启动性能分析

场景: 一个社交App启动需要2.8秒,需要优化到1.5秒以内。

Step 1: 抓取启动Trace

# 清理数据
adb shell pm clear com.example.social

# 抓取trace
python systrace.py -o launch.html -t 15 -a com.example.social \
  sched freq am wm gfx view dalvik input res app

# 启动应用
adb shell am start -W com.example.social/.MainActivity

Step 2: 打开Trace,定位关键时间点

在Trace中搜索关键事件:

搜索 "activityStart"  → 找到启动开始点 (T0)
搜索 "Choreographer#doFrame" → 找到首帧绘制 (T1)

总启动时间: T1 - T0 = 2850ms

Step 3: 分析主线程时间分布

放大主线程,查看各个阶段:

Timeline:
T0     Application.onCreate()        450ms
       ├─ SDK初始化                   280ms  ← ⚠️ 瓶颈1
       ├─ 数据库初始化                 120ms
       └─ 其他                         50ms

T+450  Activity.onCreate()           380ms
       ├─ setContentView()            80ms
       ├─ loadUserInfo() (网络请求)   200ms  ← ⚠️ 瓶颈2 (主线程!)
       └─ RecyclerView.init()         100ms

T+830  onResume()                     50ms

T+880  首帧测量布局绘制                 150ms

T+1030 RenderThread渲染               80ms

T+1110 等待网络数据                    1600ms ← ⚠️ 瓶颈3
       (主线程在sleep!)

T+2710 刷新UI                         140ms

T+2850 启动完成

Step 4: 根因分析

瓶颈1: SDK初始化 280ms

  • 点击"SDK初始化"slice,查看堆栈
  • 发现是第三方统计SDK在做网络请求
  • 优化方案: 延迟到后台初始化

瓶颈2: 主线程网络请求 200ms

  • loadUserInfo()在主线程执行同步网络请求
  • 优化方案: 改为异步请求,使用缓存数据先展示

瓶颈3: 等待网络数据 1600ms

  • 主线程在等待服务器返回
  • 优化方案: 使用本地缓存,异步刷新

Step 5: 优化后验证

优化后再次抓取trace:

Timeline (优化后):
T0     Application.onCreate()        120ms  ← 减少330ms
       ├─ 关键初始化                   70ms
       └─ 其他                         50ms

T+120  Activity.onCreate()           180ms  ← 减少200ms
       ├─ setContentView()            80ms
       ├─ 从缓存加载用户信息            30ms
       └─ RecyclerView.init()         70ms

T+300  onResume()                     50ms

T+350  首帧测量布局绘制                 150ms

T+500  RenderThread渲染               80ms

T+580  启动完成 (显示缓存数据)
       后台异步更新数据...

总启动时间: 580ms ✅ 达成目标!

优化效果:

  • 启动时间从 2850ms → 580ms
  • 提升了 79.6%
  • 用户体验显著改善

4.4 案例2: 卡顿分析

场景: RecyclerView滑动时经常掉帧,体验很卡。

Step 1: 抓取滑动Trace

python systrace.py -o scroll.html -t 10 -a com.example.social \
  sched gfx view input wm dalvik app

抓取时在App中快速滑动列表。

Step 2: 查看Frame Timeline

在Perfetto中打开trace,查看Frames轨道:

Frame Timeline:
Frame #100: 16ms  ✅ 绿色
Frame #101: 17ms  ⚠️ 黄色 (轻微掉帧)
Frame #102: 34ms  ❌ 红色 (掉1帧)
Frame #103: 18ms  ⚠️ 黄色
Frame #104: 52ms  ❌❌ 红色 (掉2帧)

Step 3: 分析掉帧的Frame

点击Frame #104 (52ms的那一帧),查看详情:

Frame #104 详细分析:
┌──────────────────────────────────────┐
 Expected: 16.6ms                     
 Actual: 52ms (掉3帧!)                
├──────────────────────────────────────┤
 Input:             2ms   ██          
 Animation:         1ms              
 Measure:           3ms   ███         
 Layout:            2ms   ██          
 Draw:              4ms   ████        
 RecyclerView.      35ms  ███████████   ⚠️ 瓶颈!
   onBindViewHolder                   
 RenderThread:      5ms   █████       
└──────────────────────────────────────┘

Step 4: 深入分析onBindViewHolder

点击"onBindViewHolder"slice,查看堆栈:

onBindViewHolder (35ms)
  ├─ loadImage() (20ms)           ← ⚠️ 主线程加载图片
  │   ├─ decodeFile() (18ms)      ← 解码耗时
  │   └─ 其他 (2ms)
  ├─ setText() (8ms)
  │   └─ measureText() (7ms)      ← 复杂文本测量
  └─ 其他 (7ms)

Step 5: 根因与优化

问题1: 主线程加载图片 20ms

// 【优化前】- 主线程同步加载
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val bitmap = BitmapFactory.decodeFile(imagePath)  // ⚠️ 主线程解码!
    holder.imageView.setImageBitmap(bitmap)
}

// 【优化后】- 使用Glide异步加载
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    Glide.with(context)
        .load(imagePath)
        .placeholder(R.drawable.placeholder)
        .into(holder.imageView)
}

问题2: 复杂文本测量 7ms

// 【优化前】- 每次都测量
textView.text = SpannableString(text).apply {
    setSpan(ForegroundColorSpan(color), 0, length, 0)
    setSpan(AbsoluteSizeSpan(20), 0, length, 0)
    // 多个span导致复杂测量
}

// 【优化后】- 缓存测量结果
private val spannableCache = LruCache<String, SpannableString>(100)

fun getSpannable(text: String): SpannableString {
    return spannableCache.get(text) ?: createSpannable(text).also {
        spannableCache.put(text, it)
    }
}

优化后验证:

Frame Timeline (优化后):
Frame #100: 15ms  ✅ 绿色
Frame #101: 16ms  ✅ 绿色
Frame #102: 14ms  ✅ 绿色
Frame #103: 17ms  ⚠️ 黄色 (偶尔)
Frame #104: 16ms  ✅ 绿色

平均帧时间: 16ms
掉帧率: < 5%  ✅ 达成目标!

4.5 案例3: CPU占用分析

场景: App在后台CPU占用高达30%,导致耗电快。

Step 1: 抓取后台运行Trace

# 使用Perfetto抓取,包含CPU和电量数据
# 在ui.perfetto.dev配置:
# - Duration: 30s
# - Probes: Scheduling, CPU frequency, Battery

Step 2: 使用SQL查询分析CPU占用

在Perfetto SQL界面输入:

-- 查询各线程的CPU占用时间
SELECT
  thread.name AS thread_name,
  process.name AS process_name,
  SUM(sched.dur) / 1e9 AS cpu_time_seconds,
  COUNT(*) AS schedule_count
FROM sched
JOIN thread USING (utid)
JOIN process USING (upid)
WHERE process.name = 'com.example.social'
GROUP BY thread.utid
ORDER BY cpu_time_seconds DESC
LIMIT 20;

查询结果:

thread_name              cpu_time_seconds  schedule_count
─────────────────────────────────────────────────────────
BackgroundWorker         8.2               1245          ← ⚠️ 异常!
main                     2.1               856
RenderThread             0.8               234
binder:12345_1           0.5               123
...

Step 3: 分析BackgroundWorker线程

搜索"BackgroundWorker",放大查看:

BackgroundWorker Timeline:
00:00 - 00:02  运行 (处理数据)     2s
00:02 - 00:02  睡眠               0.1s
00:02 - 00:04  运行 (处理数据)     2s
00:04 - 00:04  睡眠               0.1s
00:04 - 00:06  运行 (处理数据)     2s
...
(每隔0.1秒唤醒一次,处理2秒!)

Step 4: 查看源码定位问题

查看堆栈,找到对应代码:

// 【问题代码】
class BackgroundWorker : Thread() {
    override fun run() {
        while (true) {
            processData()  // 耗时操作
            Thread.sleep(100)  // 只睡眠0.1秒!
        }
    }

    private fun processData() {
        // 处理大量数据,耗时2秒
        val data = loadDataFromDB()
        analyzeData(data)
        saveResult(data)
    }
}

问题根因:

  • 后台线程每0.1秒唤醒一次
  • 每次执行耗时2秒的数据处理
  • 实际上这个任务根本不需要这么频繁

Step 5: 优化方案

// 【优化后】
class BackgroundWorker : Thread() {
    override fun run() {
        while (true) {
            processData()
            Thread.sleep(300_000)  // 改为5分钟执行一次
        }
    }
}

// 【更好的方案】使用WorkManager
val workRequest = PeriodicWorkRequestBuilder<DataProcessor>(
    15, TimeUnit.MINUTES  // 每15分钟执行一次
).build()

WorkManager.getInstance(context).enqueue(workRequest)

优化效果:

优化前:
- 后台CPU占用: 30%
- 30秒内CPU时间: 8.2秒
- 电量消耗: 高

优化后:
- 后台CPU占用: < 1%
- 30秒内CPU时间: 0.1秒
- 电量消耗: 正常

五、代码插桩:自定义Trace事件

有时候系统默认的trace信息不够详细,我们需要在自己的代码中添加trace标记,这就是代码插桩(Instrumentation)。

5.1 Java/Kotlin代码插桩

使用android.os.Trace API添加trace标记:

基础用法:

import android.os.Trace

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        Trace.beginSection("MyActivity.onCreate")
        super.onCreate(savedInstanceState)

        Trace.beginSection("InitData")
        initData()
        Trace.endSection()

        Trace.beginSection("SetupUI")
        setupUI()
        Trace.endSection()

        Trace.endSection() // MyActivity.onCreate
    }

    private fun initData() {
        Trace.beginSection("LoadConfig")
        loadConfig()
        Trace.endSection()

        Trace.beginSection("ConnectDB")
        connectDatabase()
        Trace.endSection()
    }
}

注意事项:

  • ⚠️ beginSection()endSection()必须成对出现
  • ⚠️ 必须在同一个线程中调用
  • ⚠️ Section名称最长127个字符
  • ⚠️ 嵌套深度有限制(通常不超过20层)

使用扩展函数简化:

// 定义扩展函数
inline fun <T> trace(sectionName: String, block: () -> T): T {
    Trace.beginSection(sectionName)
    try {
        return block()
    } finally {
        Trace.endSection()
    }
}

// 使用
override fun onCreate(savedInstanceState: Bundle?) {
    trace("MyActivity.onCreate") {
        super.onCreate(savedInstanceState)

        trace("InitData") {
            initData()
        }

        trace("SetupUI") {
            setupUI()
        }
    }
}

5.2 Native代码插桩

对于C/C++代码,使用<cutils/trace.h>头文件:

#include <cutils/trace.h>

class MyNativeClass {
public:
    void expensiveOperation() {
        ATRACE_CALL();  // 自动跟踪整个函数

        ATRACE_BEGIN("LoadData");
        loadData();
        ATRACE_END();

        ATRACE_BEGIN("ProcessData");
        processData();
        ATRACE_END();

        ATRACE_BEGIN("SaveResult");
        saveResult();
        ATRACE_END();
    }

private:
    void loadData() {
        ATRACE_CALL();  // 跟踪loadData函数
        // ... 加载数据
    }

    void processData() {
        ATRACE_CALL();
        // ... 处理数据
    }
};

Android.mk配置:

LOCAL_SHARED_LIBRARIES := libcutils

CMake配置:

target_link_libraries(mynative cutils)

5.3 异步事件跟踪

对于异步操作(如网络请求、图片加载),使用异步trace:

class ImageLoader {
    private var cookie = 0

    fun loadImageAsync(url: String, callback: (Bitmap) -> Unit) {
        val currentCookie = cookie++
        Trace.beginAsyncSection("LoadImage-$url", currentCookie)

        thread {
            try {
                val bitmap = downloadAndDecode(url)

                runOnUiThread {
                    Trace.endAsyncSection("LoadImage-$url", currentCookie)
                    callback(bitmap)
                }
            } catch (e: Exception) {
                Trace.endAsyncSection("LoadImage-$url", currentCookie)
                Log.e(TAG, "Failed to load image", e)
            }
        }
    }
}

在Trace中的展示:

Timeline:
T0    beginAsyncSection("LoadImage", 123)
      |
      |  (主线程继续执行其他操作)
      |
T+200 endAsyncSection("LoadImage", 123)

在Trace中会显示一条横跨200ms的异步slice

5.4 Counter跟踪

跟踪数值变化(如内存使用、队列长度):

class TaskQueue {
    private val queue = ConcurrentLinkedQueue<Task>()

    fun enqueue(task: Task) {
        queue.add(task)
        Trace.setCounter("TaskQueue.size", queue.size.toLong())
    }

    fun dequeue(): Task? {
        val task = queue.poll()
        Trace.setCounter("TaskQueue.size", queue.size.toLong())
        return task
    }
}

在Trace中会显示一条折线图,实时展示队列大小的变化。

5.5 最佳实践

1. 合理命名:

// ❌ 不好的命名
Trace.beginSection("func1")

// ✅ 好的命名
Trace.beginSection("UserRepository.loadProfile")

2. 控制粒度:

// ❌ 太细粒度 (每个小函数都trace)
fun processData(data: List<Item>) {
    Trace.beginSection("processData")
    for (item in data) {
        Trace.beginSection("processItem")  // 不必要
        process(item)
        Trace.endSection()
    }
    Trace.endSection()
}

// ✅ 合适的粒度
fun processData(data: List<Item>) {
    Trace.beginSection("processData")
    for (item in data) {
        process(item)  // 不需要每个item都trace
    }
    Trace.endSection()
}

3. Release版本优化:

// 在release版本中禁用trace以减少开销
inline fun traceDebug(sectionName: String, block: () -> Unit) {
    if (BuildConfig.DEBUG) {
        Trace.beginSection(sectionName)
        try {
            block()
        } finally {
            Trace.endSection()
        }
    } else {
        block()
    }
}

4. 关键路径优先: 优先在以下场景添加trace:

  • ✅ 启动流程的关键步骤
  • ✅ 网络请求的各个阶段
  • ✅ 数据库操作
  • ✅ 图片加载和解码
  • ✅ 复杂计算
  • ✅ RecyclerView的bind操作

六、进阶技巧

6.1 Perfetto高级SQL查询

查询所有GC事件及其耗时:

SELECT
  ts / 1e9 AS time_sec,
  dur / 1e6 AS duration_ms,
  name AS gc_type,
  CASE
    WHEN dur < 5e6 THEN 'Minor'
    WHEN dur < 50e6 THEN 'Major'
    ELSE 'Critical'
  END AS severity
FROM slice
WHERE name GLOB 'Heap*' OR name GLOB 'GC*'
ORDER BY dur DESC;

查询主线程耗时最长的20个方法:

SELECT
  name,
  dur / 1e6 AS duration_ms,
  ts / 1e9 AS start_time_sec
FROM slice
JOIN thread_track ON slice.track_id = thread_track.id
JOIN thread ON thread_track.utid = thread.utid
WHERE
  thread.name = 'main'
  AND depth = 0  -- 只看顶层调用
  AND dur > 10e6  -- 超过10ms
ORDER BY dur DESC
LIMIT 20;

查询Binder调用统计:

SELECT
  thread.name AS caller_thread,
  slice.name AS binder_method,
  COUNT(*) AS call_count,
  SUM(dur) / 1e6 AS total_ms,
  AVG(dur) / 1e6 AS avg_ms,
  MAX(dur) / 1e6 AS max_ms
FROM slice
JOIN thread_track ON slice.track_id = thread_track.id
JOIN thread ON thread_track.utid = thread.utid
WHERE slice.name LIKE 'binder%'
GROUP BY thread.name, slice.name
ORDER BY total_ms DESC;

分析帧耗时分布:

SELECT
  CASE
    WHEN dur < 16.6e6 THEN '正常 (<16.6ms)'
    WHEN dur < 33e6 THEN '轻微掉帧 (16.6-33ms)'
    WHEN dur < 50e6 THEN '中度掉帧 (33-50ms)'
    ELSE '严重掉帧 (>50ms)'
  END AS frame_category,
  COUNT(*) AS frame_count,
  ROUND(100.0 * COUNT(*) / (SELECT COUNT(*) FROM actual_frame_timeline_slice), 2) AS percentage
FROM actual_frame_timeline_slice
GROUP BY frame_category
ORDER BY frame_count DESC;

6.2 自动化分析脚本

使用Python和Perfetto Trace Processor进行自动化分析:

#!/usr/bin/env python3
"""
启动时间自动化分析脚本
"""
from perfetto.trace_processor import TraceProcessor
import sys

def analyze_startup(trace_file):
    tp = TraceProcessor(trace=trace_file)

    # 查询Application.onCreate的耗时
    query = '''
    SELECT
        ts / 1e9 AS start_sec,
        dur / 1e6 AS duration_ms
    FROM slice
    WHERE name = 'Application.onCreate'
    '''

    result = tp.query(query)
    if len(result) > 0:
        app_create_time = result.duration_ms[0]
        print(f"Application.onCreate: {app_create_time:.2f}ms")
    else:
        print("未找到Application.onCreate事件")
        return

    # 查询Activity.onCreate的耗时
    query = '''
    SELECT
        ts / 1e9 AS start_sec,
        dur / 1e6 AS duration_ms
    FROM slice
    WHERE name LIKE '%Activity.onCreate%'
    '''

    result = tp.query(query)
    if len(result) > 0:
        activity_create_time = result.duration_ms[0]
        print(f"Activity.onCreate: {activity_create_time:.2f}ms")
    else:
        print("未找到Activity.onCreate事件")
        return

    # 查询首帧渲染时间
    query = '''
    SELECT MIN(ts) / 1e9 AS first_frame_sec
    FROM actual_frame_timeline_slice
    '''

    result = tp.query(query)
    first_frame_time = result.first_frame_sec[0]

    # 计算总启动时间
    query = '''
    SELECT MIN(ts) / 1e9 AS app_start_sec
    FROM slice
    WHERE name = 'Application.onCreate'
    '''

    result = tp.query(query)
    app_start_time = result.app_start_sec[0]

    total_startup = (first_frame_time - app_start_time) * 1000
    print(f"总启动时间: {total_startup:.2f}ms")

    # 性能评级
    if total_startup < 1000:
        print("✅ 性能评级: 优秀")
    elif total_startup < 2000:
        print("⚠️ 性能评级: 良好")
    elif total_startup < 3000:
        print("⚠️ 性能评级: 一般")
    else:
        print("❌ 性能评级: 需要优化")

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("用法: python analyze_startup.py trace.perfetto-trace")
        sys.exit(1)

    analyze_startup(sys.argv[1])

使用方法:

python analyze_startup.py launch.perfetto-trace

# 输出:
# Application.onCreate: 450.23ms
# Activity.onCreate: 380.45ms
# 总启动时间: 1245.67ms
# ⚠️ 性能评级: 良好

七、常见问题与解决

7.1 Trace抓取失败

问题: 执行systrace/perfetto命令后无输出或报错

可能原因与解决:

  1. 设备未授权
# 检查设备连接
adb devices
# 如果显示 "unauthorized",重新授权
adb kill-server
adb start-server
  1. 权限不足
# 需要root权限
adb root
adb remount
  1. buffer大小不足
# 增大buffer
python systrace.py -b 65536 ...  # 64MB
  1. category不支持
# 查看设备支持的category
adb shell atrace --list_categories

7.2 Trace文件过大

问题: Trace文件几百MB,Chrome打不开或很卡

解决方案:

  1. 减少抓取时长
# 从10秒改为5秒
-t 5
  1. 只选择必要的category
# 不要用 --all,精确指定
python systrace.py ... sched gfx view am
  1. 使用Perfetto代替Systrace Perfetto的protobuf格式文件更小,加载更快

  2. 使用Perfetto的在线查看 不要下载文件,直接在 ui.perfetto.dev 上分析

7.3 找不到关键事件

问题: 在Trace中搜索不到我的函数调用

可能原因:

  1. 没有启用app category
# 确保包含 app category
python systrace.py ... app
  1. 代码没有插桩
// 需要添加 Trace.beginSection()
Trace.beginSection("MyFunction")
myFunction()
Trace.endSection()
  1. 函数执行时间太短
  • 小于0.1ms的函数可能不会显示
  • 放大时间轴查看
  1. 没有指定应用包名
# 指定包名以抓取应用的trace
python systrace.py -a com.example.myapp ...

7.4 Trace看不懂

问题: Trace文件打开了,但不知道怎么分析

建议:

  1. 从宏观到微观

    • 先缩小查看整体
    • 识别大块耗时
    • 再放大查看细节
  2. 关注主线程

    • 主线程的空白区域可能在等待
    • 查看主线程在做什么
  3. 对比VSYNC信号

    • 看渲染是否跟上VSYNC节奏
    • 识别掉帧原因
  4. 搜索关键事件

    • activityStart, onCreate, draw, measure
    • 使用搜索功能快速定位
  5. 查看CPU调度

    • 线程是在运行还是等待
    • CPU是否被其他进程占用

八、工具对比与选择建议

8.1 何时使用Systrace

推荐场景:

  • ✅ Android 9及以下系统
  • ✅ 简单的性能分析(启动、卡顿)
  • ✅ 不需要复杂查询
  • ✅ 团队成员对Chrome Trace Viewer熟悉

优势:

  • 简单易用,一行命令搞定
  • 兼容老版本Android
  • HTML文件可以离线查看

8.2 何时使用Perfetto

推荐场景:

  • Android 10及以上系统 (强烈推荐)
  • ✅ 需要SQL查询进行深度分析
  • ✅ 需要内存、电量等额外数据
  • ✅ 需要自动化分析(CI/CD)
  • ✅ 大型trace文件

优势:

  • 现代化UI,体验极佳
  • SQL查询引擎,分析能力强
  • 文件更小,加载更快
  • 持续更新,功能丰富

8.3 选择建议

场景推荐工具理由
新项目Perfetto功能强大,体验好
老项目(Android 9-)Systrace兼容性
启动优化PerfettoSQL查询方便统计
卡顿分析PerfettoFrame Timeline强大
快速排查Systrace简单快速
深度分析PerfettoSQL查询 + 多数据源
CI集成PerfettoTrace Processor API
学习入门Systrace简单易懂

总结: 如果你的项目主要支持Android 10+,强烈推荐从现在开始使用Perfetto!


九、总结与展望

9.1 核心要点回顾

通过本文的学习,我们全面掌握了Systrace和Perfetto两大性能分析工具:

工具使用:

  • ✅ 理解了Trace的工作原理和应用场景
  • ✅ 掌握了Systrace和Perfetto的抓取方法
  • ✅ 学会了Perfetto UI的各项功能
  • ✅ 了解了两种工具的对比和选择

分析方法:

  • ✅ 建立了系统化的Trace分析思路
  • ✅ 学会了定位启动慢、卡顿、CPU占用等问题
  • ✅ 掌握了Frame Timeline分析技巧
  • ✅ 理解了关键性能指标的含义

实战技能:

  • ✅ 掌握了Java/Kotlin/Native代码插桩
  • ✅ 学会了Perfetto SQL高级查询
  • ✅ 了解了自动化分析和CI集成
  • ✅ 能够独立完成性能问题的排查

9.2 最佳实践总结

DO (推荐做法):

  • ✅ 优先使用Perfetto (Android 10+)
  • ✅ 在关键路径添加trace标记
  • ✅ 定期进行性能回归测试
  • ✅ 建立性能基线和阈值
  • ✅ 使用SQL查询进行深度分析
  • ✅ 保存重要的trace文件以便对比
  • ✅ 团队内分享trace分析经验

DON'T (避免做法):

  • ❌ 不要凭感觉优化,要用数据说话
  • ❌ 不要只优化局部,要看整体
  • ❌ 不要在release版本保留过多trace
  • ❌ 不要忽略小的性能问题
  • ❌ 不要等用户反馈才开始优化

9.3 进一步学习资源

官方文档:

工具:

社区:

  • Android Performance Patterns (YouTube)
  • Android Developers Blog
  • Stack Overflow (标签: android-performance)

相关文章:


记住:性能优化不是一次性的工作,而是一个持续的过程。定期使用Trace工具检查性能,及时发现和解决问题,才能保证应用始终保持最佳状态。

当你下次遇到性能问题时,不要慌张,打开Perfetto,抓个trace,答案就在那里! 📊✨


作者简介: 多年Android系统开发经验,专注于系统稳定性与性能优化领域。欢迎关注本系列,一起深入Android系统的精彩世界!


🎉 感谢关注,让我们一起深入Android系统的精彩世界!

找到我: 个人主页