性能优化与监控
# 16.性能优化与监控
# 目录介绍
- 一、引言:什么是卡顿
- 二、渲染管线与帧率
- 2.1 一帧的渲染流程
- 2.2 RenderThread与GPU渲染管线
- 2.3 掉帧的几种情况
- 三、主线程卡顿的根因分析
- 3.1 常见卡顿原因分类
- 3.2 StrictMode检测
- 四、卡顿监控方案原理
- 4.1 各方案对比
- 五、Looper Printer监控
- 5.1 BlockCanary核心原理
- 5.2 监控实现
- 5.3 方案的局限与改进
- 六、Choreographer帧监控
- 6.1 帧回调监控
- 6.2 FrameMetrics API(Android 7+)
- 七、ANR的触发与监控
- 7.1 ANR触发条件
- 7.2 ANR监控方案
- 7.3 traces.txt分析
- 八、启动速度优化
- 8.1 启动阶段划分
- 8.2 具体优化策略
- 8.3 启动耗时测量
- 九、布局渲染优化
- 9.1 布局性能问题定位
- 9.2 具体优化策略
- 十、IO与线程优化
- 10.1 SharedPreferences的坑
- 10.2 线程优化策略
- 10.3 数据库优化
- 十一、Systrace与Perfetto分析
- 11.1 Systrace使用
- 11.2 Perfetto(Android 10+推荐)
- 11.3 自定义Trace标记
- 11.4 Perfetto Trace分析技巧
- 十二、字节码插桩监控原理
- 12.1 ASM插桩的工作原理
- 12.2 Hook方案(运行时修改)
- 十三、性能监控体系与工程化实践
- 13.1 性能监控体系搭建
- 13.2 性能优化的CI/CD集成
- 13.3 线上监控的数据分析
- 十四、面试高频问题与深度分析
- 14.1 如何检测卡顿?
- 14.2 如何优化RecyclerView的卡顿?
- 14.3 线上ANR如何排查?
- 14.4 性能优化常见面试题
- 十五、总结
# 一、引言:什么是卡顿
用户感知到的"卡顿"本质是帧率下降——显示器需要每16.6ms(60fps)刷新一帧画面,如果某一帧的渲染超时,用户就会感受到不流畅。
帧率与用户感知:
60fps → 16.6ms/帧 → 基本流畅
90fps → 11.1ms/帧 → 丝滑顺畅
120fps → 8.3ms/帧 → 极致流畅
<30fps → >33ms/帧 → 明显卡顿
1
2
3
4
5
2
3
4
5
疑惑:如何自动检测和定位卡顿?BlockCanary、Matrix等工具的监控原理是什么?
# 二、渲染管线与帧率
# 2.1 一帧的渲染流程
一帧16.6ms内需要完成的工作:
VSync信号到来
↓ ┌──── 主线程 ────────────────────────┐
│ │ Input处理(触摸/按键事件) │
│ │ Animation计算(属性动画/转场动画) │
│ │ Measure → Layout → Draw │
│ │ → 构建/更新DisplayList │
│ └──── sync到RenderThread ─────────────┘
│ ┌──── RenderThread ─────────────────────┐
│ │ 处理DisplayList → OpenGL/Vulkan命令 │
│ │ GPU绘制 → swapBuffers │
│ └───────────────────────────────────────┘
↓
下一个VSync信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2.2 RenderThread与GPU渲染管线
RenderThread的工作细节(Android 5.0+):
主线程与RenderThread的协作:
主线程: RenderThread:
┌──────────────────────┐ ┌──────────────────────┐
│ 处理Input事件 │ │ │
│ 执行动画回调 │ │ 等待主线程sync... │
│ measure / layout │ │ │
│ draw (构建DisplayList) │ │ │
│ ────sync────────────→ │ sync │ 接收DisplayList │
│ │ │ │ 遍历RenderNode树 │
│ 可以处理下一帧的Input │ │ │ 转换为GPU命令 │
│ │ │ │ glDrawXxx() │
│ │ │ │ eglSwapBuffers() │
│ │ │ → Buffer提交到SF │
└──────────────────────┘ └──────────────────────┘
sync过程的细节:
主线程将DisplayList树的变更同步到RenderThread
这个过程需要两个线程短暂互斥(类似Stop-the-world)
sync耗时一般 < 1ms
但如果DisplayList太大(复杂布局),sync可能超时
RenderThread的独立优势:
即使主线程被短暂阻塞(如GC)
RenderThread仍在执行上一帧的GPU渲染
→ 部分掉帧可以被掩盖
属性动画(alpha/translation/rotation):
→ RenderThread可以独立执行,不需要主线程参与
→ 即使主线程在做IO,动画仍然流畅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 2.3 掉帧的几种情况
情况1:主线程耗时(最常见)
主线程:|Input|Animation|───耗时操作───|Measure|Layout|Draw|
←──────────── 超过16.6ms ─────────────────────→
情况2:RenderThread耗时
主线程完成了,但GPU绘制太慢 → 帧没能按时提交
情况3:GC停顿
主线程被GC暂停 → 该帧所有工作被延迟
情况4:CPU频率被压制
省电模式/温控 → CPU降频 → 所有操作变慢
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 三、主线程卡顿的根因分析
# 3.1 常见卡顿原因分类
1. CPU密集型
├── 复杂计算(JSON解析、数据处理)
├── 反射调用(Method.invoke开销大)
└── 正则表达式匹配
2. IO阻塞
├── 文件读写(SharedPreferences.commit)
├── 数据库操作(SQLite查询)
└── ContentProvider查询
3. 锁等待
├── synchronized锁竞争
├── 等待子线程结果(Future.get)
└── Binder调用等待
4. 渲染负担
├── 布局太深(measure/layout耗时)
├── 过度绘制(GPU负载高)
└── 大量invalidate
5. 内存相关
├── GC频繁(内存抖动)
└── 大对象分配(触发GC)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.2 StrictMode检测
// 开发阶段使用StrictMode检测主线程违规操作
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.detectCustomSlowCalls()
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.detectActivityLeaks()
.penaltyLog()
.build());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 四、卡顿监控方案原理
# 4.1 各方案对比
┌─────────────────┬────────────────┬──────────────┬──────────────┐
│ 方案 │ 原理 │ 优点 │ 缺点 │
├─────────────────┼────────────────┼──────────────┼──────────────┤
│ Looper Printer │ 消息处理耗时 │ 简单无侵入 │ 精度有限 │
│ Choreographer │ 帧间隔 │ 精确帧率 │ 无堆栈信息 │
│ ANR Watchdog │ 主线程阻塞检测 │ 获取ANR前堆栈 │ 阈值较高 │
│ 字节码插桩 │ 方法级耗时 │ 精确定位慢方法 │ 包体积增加 │
│ Systrace/Perfetto│系统级跟踪 │ 最全面 │ 仅线下分析 │
└─────────────────┴────────────────┴──────────────┴──────────────┘
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
各方案的适用阶段和选择建议:
开发阶段(线下):
首选 Systrace/Perfetto → 最全面的系统级分析
配合 GPU呈现模式 → 快速发现掉帧
使用 StrictMode → 检测主线程IO等违规操作
测试阶段(线下):
字节码插桩(Matrix TraceCanary) → 精确定位慢方法
Choreographer帧监控 → 统计掉帧率
Android Profiler → 实时监控CPU/内存/网络
线上监控:
Looper Printer → 轻量级,监控主线程消息耗时
Choreographer帧回调 → 实时帧率采集
ANR Watchdog → 检测卡死
结合采样 → 只采集部分用户数据,降低性能影响
推荐的线上监控组合:
帧率监控(发现问题)+ Looper Printer(定位消息)+ 堆栈采样(定位方法)
→ 三者结合可以完整定位卡顿原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 五、Looper Printer监控
# 5.1 BlockCanary核心原理
// Looper.loop()中的关键代码:
public static void loop() {
for (;;) {
Message msg = queue.next();
// 消息处理前打印
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target);
}
msg.target.dispatchMessage(msg); // 处理消息
// 消息处理后打印
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 5.2 监控实现
Looper.getMainLooper().setMessageLogging(new Printer() {
private long startTime;
private StackSampler stackSampler;
@Override
public void println(String x) {
if (x.startsWith(">>>>>")) {
startTime = System.currentTimeMillis();
stackSampler.startSampling(); // 开始子线程采样堆栈
} else if (x.startsWith("<<<<<")) {
long duration = System.currentTimeMillis() - startTime;
stackSampler.stopSampling();
if (duration > THRESHOLD) { // 如500ms
String stack = stackSampler.getStackTraces();
reportBlock(duration, stack);
}
}
}
});
// 堆栈采样的关键:在子线程中定期获取主线程堆栈
class StackSampler {
void sample() {
StackTraceElement[] stackTrace = mainThread.getStackTrace();
buffer.put(System.currentTimeMillis(), stackTrace);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 5.3 方案的局限与改进
局限性:
1. 只能检测消息级别的卡顿,无法发现消息之间的空档期卡顿
2. 堆栈采样是概率性的,可能采不到真正耗时的代码
3. Printer本身有一定开销(字符串拼接)
改进方案:
1. 高频采样(如每50ms采样一次)→ 提高命中率
2. 配合Choreographer监控帧率 → 更准确判断掉帧
3. 使用ASM字节码插桩 → 每个方法前后插入耗时统计代码
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 六、Choreographer帧监控
# 6.1 帧回调监控
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
private long lastFrameTimeNanos = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTimeNanos != 0) {
long intervalMs = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
if (intervalMs > 16.6) {
int droppedFrames = (int) (intervalMs / 16.6) - 1;
Log.w("FPS", "丢弃 " + droppedFrames + " 帧, 耗时 " + intervalMs + "ms");
}
float fps = 1000f / intervalMs;
fpsMonitor.report(fps);
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 6.2 FrameMetrics API(Android 7+)
window.addOnFrameMetricsAvailableListener(
(window1, frameMetrics, dropCount) -> {
long inputDuration = frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION);
long animDuration = frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION);
long measureDuration = frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION);
long drawDuration = frameMetrics.getMetric(FrameMetrics.DRAW_DURATION);
long syncDuration = frameMetrics.getMetric(FrameMetrics.SYNC_DURATION);
long totalDuration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION);
// 精确定位是哪个阶段导致掉帧
if (totalDuration > 16_666_666) { // 纳秒
analyzeSlowFrame(inputDuration, animDuration, measureDuration,
drawDuration, syncDuration);
}
}, new Handler());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 七、ANR的触发与监控
# 7.1 ANR触发条件
ANR类型及超时时间:
1. InputDispatching ANR(5秒)
→ 输入事件5秒内未处理完毕
2. BroadcastReceiver ANR
→ 前台广播10秒 / 后台广播60秒
3. Service ANR
→ 前台Service 20秒 / 后台Service 200秒
4. ContentProvider ANR(10秒)
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 7.2 ANR监控方案
// 方案1:ANR-WatchDog
// 在子线程中定期检查主线程是否响应
class ANRWatchDog extends Thread {
private volatile long lastTick = 0;
private volatile boolean reported = false;
private final Runnable tickRunnable = () -> {
lastTick = 0; // 主线程能执行说明没卡
reported = false;
};
@Override
public void run() {
while (!isInterrupted()) {
lastTick = SystemClock.uptimeMillis();
mainHandler.post(tickRunnable);
try {
Thread.sleep(5000); // 等待5秒
} catch (InterruptedException e) {
return;
}
if (lastTick != 0 && !reported) {
// 5秒后主线程还没执行tickRunnable → ANR
reported = true;
StackTraceElement[] stack = Looper.getMainLooper().getThread().getStackTrace();
reportANR(stack);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 7.3 traces.txt分析
关键信息:
"main" prio=5 tid=1 Blocked
at com.example.app.MainActivity.onResume(MainActivity.java:42)
- waiting to lock <0x0123456> held by thread 15
"Thread-15" prio=5 tid=15 Waiting
at java.lang.Thread.sleep(Native Method)
- locked <0x0123456>
分析:主线程等待Thread-15释放锁,Thread-15在sleep → 死锁导致ANR
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 八、启动速度优化
# 8.1 启动阶段划分
冷启动时间 = T(进程创建) + T(Application) + T(Activity) + T(首帧渲染)
T1: 进程创建 → Zygote fork + 加载APK
T2: Application → attachBaseContext + ContentProvider.onCreate + onCreate
T3: Activity → onCreate + setContentView(inflate)
T4: 首帧 → onResume + measure + layout + draw
优化目标:冷启动 < 1秒
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 8.2 具体优化策略
1. Application.onCreate优化
├── 延迟初始化:非必要SDK放在IdleHandler
├── 异步初始化:线程池并行初始化
└── 按需初始化:用到时才加载
2. Activity优化
├── 减少setContentView层级
├── ViewStub延迟加载
└── AsyncLayoutInflater异步Inflate
3. 启动窗口优化
├── windowBackground设置启动背景图
└── SplashScreen API(Android 12+)
4. 类加载优化
├── 减少Application中的类引用数量
└── 关键类预加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 8.3 启动耗时测量
# 方法1:adb命令测量
adb shell am start -W com.example.app/.MainActivity
# 输出:
# ThisTime: 385ms → 最后一个Activity启动耗时
# TotalTime: 520ms → 从startActivity到Activity可见的总耗时
# WaitTime: 545ms → 包含前一个Activity的pause时间
# 方法2:代码埋点
class App : Application() {
companion object {
val launchStartTime = SystemClock.uptimeMillis()
}
}
class MainActivity : Activity() {
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) {
val launchTime = SystemClock.uptimeMillis() - App.launchStartTime
Log.d("Launch", "冷启动耗时: ${launchTime}ms")
}
}
}
# 方法3:Reported Fully Drawn(推荐)
// 在数据加载完成、首屏渲染完成后调用
reportFullyDrawn()
// 会在logcat中输出:Fully drawn xxx/.MainActivity: +1s200ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 九、布局渲染优化
# 9.1 布局性能问题定位
工具1:Layout Inspector
→ 查看View层级和属性
工具2:GPU呈现模式
→ 柱状图查看每帧渲染耗时
→ 绿线=16.6ms
工具3:Show GPU Overdraw
→ 无色(0x) → 蓝(1x) → 绿(2x) → 粉(3x) → 红(4x+)
工具4:Systrace/Perfetto
→ 精确到方法级别
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 9.2 具体优化策略
1. 减少层级
├── ConstraintLayout替代多层嵌套
├── merge标签减少include额外层级
└── ViewStub延迟加载
2. 减少过度绘制
├── 移除不必要的背景
├── clipRect裁剪不可见区域
└── 减少alpha使用
3. RecyclerView优化
├── DiffUtil增量更新
├── setHasFixedSize(true)
├── RecycledViewPool复用
├── 预加载setInitialPrefetchItemCount
└── 避免onBindViewHolder创建对象
4. Compose优化(如使用Jetpack Compose)
├── 稳定类型标记@Stable
├── 避免不必要的重组
└── derivedStateOf减少计算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 十、IO与线程优化
# 10.1 SharedPreferences的坑
SP的性能问题:
1. apply()可能导致ANR
→ apply()的写操作加入QueuedWork
→ Activity.onPause/onStop时等待QueuedWork完成
→ 写入量大会阻塞主线程
2. 全量写入
→ 每次保存都是全量序列化XML
3. 加载阻塞
→ getString()等会等待文件加载完
替代方案:
├── MMKV → mmap高性能
├── DataStore → 协程异步
└── SQLite → 结构化数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 10.2 线程优化策略
线程数过多的问题:
→ 线程创建开销(栈默认1MB)
→ 上下文切换开销
→ 线程数超限导致OOM
优化方案:
1. 统一线程池
→ 全局共享一个线程池,避免各SDK各自创建
→ 区分IO密集型和CPU密集型任务
2. 合理的线程池配置
// IO密集型
val ioPool = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2)
// CPU密集型
val cpuPool = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors())
3. 协程替代线程
→ Kotlin协程开销远小于线程
→ 挂起而非阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 10.3 数据库优化
SQLite性能优化:
1. 使用事务批量操作
db.beginTransaction();
try {
for (item : items) {
db.insert(...);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
// 100条insert:无事务约500ms → 有事务约10ms
2. 使用索引
CREATE INDEX idx_user_name ON users(name);
// 10万条数据查询:无索引约200ms → 有索引约1ms
3. WAL模式
db.enableWriteAheadLogging();
// 允许读写并发,提升多线程访问性能
4. 预编译语句
SQLiteStatement stmt = db.compileStatement("INSERT INTO users VALUES(?,?)");
// 复用stmt多次执行,避免重复编译SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 十一、Systrace与Perfetto分析
# 11.1 Systrace使用
# 抓取Systrace
python systrace.py -a com.example.app -t 5 \
gfx input view wm am sm sched freq idle \
-o trace.html
# 关键标记查看:
# Choreographer#doFrame → 每帧的起点
# measure/layout/draw → 各阶段耗时
# RenderThread → GPU渲染耗时
# SurfaceFlinger → 合成耗时
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 11.2 Perfetto(Android 10+推荐)
# 使用Perfetto抓取
adb shell perfetto \
-c - --txt \
-o /data/misc/perfetto-traces/trace.perfetto-trace \
<<EOF
buffers: { size_kb: 65536 fill_policy: RING_BUFFER }
data_sources: { config { name: "linux.ftrace" ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "power/suspend_resume"
atrace_categories: "gfx"
atrace_categories: "view"
atrace_categories: "wm"
atrace_apps: "com.example.app"
}}}
duration_ms: 5000
EOF
# 在 https://ui.perfetto.dev 打开分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 11.3 自定义Trace标记
// 在代码中添加自定义跟踪标记
import android.os.Trace;
void loadData() {
Trace.beginSection("loadData"); // 开始
try {
Trace.beginSection("parseJSON");
parseJSON(data);
Trace.endSection(); // 结束parseJSON
Trace.beginSection("saveDB");
saveToDB(parsed);
Trace.endSection(); // 结束saveDB
} finally {
Trace.endSection(); // 结束loadData
}
}
// 在Systrace/Perfetto中可以看到自定义区间的耗时
// 精确定位哪个方法是瓶颈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 11.4 Perfetto Trace分析技巧
Perfetto Trace的高级分析方法:
1. 识别帧时间线
在Perfetto UI中查找:
├── SurfaceFlinger行 → 每帧的合成时间
├── App主线程行 → Choreographer#doFrame标记
├── RenderThread行 → DrawFrame标记
└── GPU行 → GPU完成时间
正常帧:
|doFrame(8ms)|sync(0.5ms)|DrawFrame(4ms)|GPU(3ms)| < 16.6ms ✓
掉帧:
|doFrame(25ms) |sync|DrawFrame|GPU| > 16.6ms ✗
└── 主线程耗时过长
2. CPU调度分析
查看主线程的调度状态:
├── Running (绿色) → 正在CPU上执行
├── Runnable (蓝色) → 等待CPU调度(CPU繁忙)
├── Sleeping (灰色) → 等待唤醒(lock/wait/sleep)
└── Uninterruptible Sleep (橙色) → 等待IO(磁盘/网络)
如果主线程大量处于:
Runnable → CPU竞争激烈,其他线程占用CPU
Sleeping → 等待锁(检查是否有死锁/锁竞争)
Uninterruptible Sleep → 主线程在做IO(必须移到后台线程)
3. Binder调用追踪
在App进程→system_server进程之间的Binder调用:
App主线程 → binder transaction → system_server处理 → binder reply → App继续
如果system_server忙碌 → App主线程被阻塞等待 → 掉帧
常见的Binder耗时调用:
├── PackageManager.getApplicationInfo()
├── WindowManager.addView()
├── ActivityManager.getRunningAppProcesses()
└── ContentProvider.query()
4. 内存与GC分析
在Perfetto中查看GC事件:
├── young_gc → Minor GC(通常 < 2ms)
├── full_gc → Major GC(可能 > 10ms)
└── concurrent_gc → 并发GC(对主线程影响小)
如果频繁看到GC事件 → 内存抖动 → 优化对象创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 十一点五、字节码插桩监控原理
# 11.5.1 ASM插桩的工作原理
字节码插桩(Matrix TraceCanary的方案):
编译时:ASM框架修改字节码
┌───────────────────────────────────┐
│ 原始代码: │
│ void doSomething() { │
│ // 业务逻辑 │
│ } │
│ │
│ 插桩后: │
│ void doSomething() { │
│ TraceMethod.i(methodId); │ ← 方法开始
│ // 业务逻辑 │
│ TraceMethod.o(methodId); │ ← 方法结束
│ } │
└───────────────────────────────────┘
运行时:记录每个方法的耗时
TraceMethod.i(id) → 记录方法开始时间戳
TraceMethod.o(id) → 记录方法结束时间戳 → 计算耗时
优化策略(避免全量插桩的性能影响):
├── 只插桩项目代码,排除系统API
├── 排除简单的getter/setter方法
├── 使用long数组存储时间戳(避免对象创建)
├── 方法ID使用int而非String(减少内存)
└── 环形缓冲区存储最近的调用记录
检测到卡顿时:
回溯环形缓冲区 → 还原出方法调用树 → 精确定位耗时方法
输出示例:
├── MainActivity.onCreate() [520ms]
│ ├── initSDK() [200ms]
│ │ └── ThirdPartySDK.init() [195ms] ← 这是瓶颈!
│ ├── setContentView() [150ms]
│ │ └── LayoutInflater.inflate() [145ms]
│ └── loadData() [170ms]
│ ├── DatabaseHelper.query() [120ms]
│ └── parseResult() [50ms]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 12.2 Hook方案(运行时修改)
运行时Hook方案对比:
1. Epic/Pine(Inline Hook)
→ 修改方法入口的机器码
→ 跳转到自定义的Hook函数
→ 优点:精确到单个方法
→ 缺点:兼容性问题(不同CPU架构/ART版本)
2. Frida(注入Hook)
→ 注入进程后修改ART内部结构
→ 适合调试分析
→ 缺点:不适合线上使用
3. ASM编译时插桩(推荐)
→ 编译期修改字节码
→ 无运行时兼容性问题
→ 优点:稳定可靠
→ 缺点:需要重新编译
线上监控推荐方案:
ASM编译时插桩 + Looper Printer + Choreographer帧监控
→ 三种方案结合使用
→ 先通过帧监控发现掉帧
→ 再通过Looper Printer定位消息级别卡顿
→ 最后通过方法耗时还原调用栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 十四、面试高频问题与深度分析
# 14.1 如何检测卡顿?
方案1:Looper Printer
→ 监控每个Message处理耗时
→ 优点:简单,线上可用
→ 代表:BlockCanary
方案2:Choreographer FrameCallback
→ 监控每帧间隔
→ 优点:精确FPS数据
方案3:ANR-WatchDog
→ 子线程定时检查主线程响应
→ 优点:可以提前于系统ANR获取堆栈
方案4:字节码插桩
→ ASM在每个方法前后插入计时代码
→ 优点:精确到方法级别
→ 代表:Matrix TraceCanary
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 12.2 如何优化RecyclerView的卡顿?
层面1:数据层
├── DiffUtil → 增量更新替代notifyDataSetChanged
├── Paging → 分页加载大数据集
└── 后台线程计算Diff
层面2:布局层
├── setHasFixedSize(true) → 避免多余requestLayout
├── 减少Item布局层级
└── 使用ConstraintLayout简化布局
层面3:绑定层
├── 避免onBindViewHolder中创建对象
├── 图片异步加载(Glide/Coil)
└── 预取setInitialPrefetchItemCount
层面4:缓存层
├── setItemViewCacheSize → 调整缓存大小
├── RecycledViewPool → 多个RecyclerView共享
└── setRecycleChildrenOnDetach → 控制回收时机
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 14.3 线上ANR如何排查?
线上ANR排查流程:
1. 收集信息
├── ANR发生时的主线程堆栈
├── 所有线程堆栈
├── CPU使用率
├── 内存状况
└── 磁盘IO状况
2. 分类分析
├── 主线程死锁 → 查找lock关键字
├── 主线程IO → 查找read/write/query
├── 主线程Binder调用 → 查找transact
├── CPU满载 → 查看CPU usage
└── 内存不足 → 查看GC日志
3. 常见根因
├── SharedPreferences.apply()在onPause等待
├── ContentProvider查询耗时
├── 第三方SDK主线程初始化
├── WebView初始化
└── 多进程共享文件锁竞争
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 十三、性能优化的工程化实践
# 13.1 性能监控体系搭建
一套完整的性能监控体系需要覆盖以下维度:
性能监控体系:
线上监控:
├── 启动耗时
│ ├── 冷启动时间(进程创建到首帧显示)
│ ├── 温启动时间(Activity重建到首帧显示)
│ └── 热启动时间(Activity从后台恢复)
│
├── 页面渲染
│ ├── 帧率FPS(目标60fps/90fps/120fps)
│ ├── 掉帧次数和严重程度
│ └── 页面加载完成时间
│
├── 卡顿检测
│ ├── 主线程耗时方法统计
│ ├── ANR发生率
│ └── 用户感知卡顿率
│
├── 内存监控
│ ├── Java堆内存使用量
│ ├── Native内存使用量
│ ├── OOM发生率
│ └── 大图检测
│
└── IO监控
├── 主线程IO操作检测
├── 数据库查询耗时
└── 网络请求耗时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 13.2 性能优化的CI/CD集成
将性能监控融入开发流程:
1. 编译阶段
├── Lint规则检查:检测主线程IO、大图资源等
├── 方法数/包体积监控:每次构建对比增量
└── 自动化字节码插桩:ASM插入性能监控代码
2. 测试阶段
├── 自动化性能测试(Macrobenchmark)
│ ├── 冷启动时间
│ ├── 滑动帧率
│ └── 页面加载时间
├── Monkey测试 + 性能数据采集
└── 内存泄漏自动检测(LeakCanary集成CI)
3. 发布阶段
├── 灰度发布 + 性能指标门禁
│ └── 启动时间 > 基线1.2倍 → 阻断发布
├── A/B实验对比性能指标
└── 自动回滚机制
4. 线上阶段
├── 实时性能大盘(启动、帧率、ANR率)
├── 异常报警(ANR率突增、OOM突增)
└── 用户分群分析(低端机/高端机)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 13.3 线上监控的数据分析
线上性能数据的分析方法:
1. 指标定义
├── P50/P90/P99分位值
│ └── P90启动时间 < 2秒为优秀
├── 绝对值 vs 相对值
│ └── 版本间对比使用相对值
└── 采样率设置
└── 高端机10% / 低端机50%
2. 归因分析
├── 版本维度:哪个版本引入了劣化
├── 机型维度:哪些机型问题严重
├── 场景维度:哪个页面/操作最慢
└── 堆栈维度:哪个方法最耗时
3. 常见优化收益评估
├── 启动优化1秒 → 用户留存提升约5%
├── 页面加载优化500ms → 转化率提升约2%
└── ANR率降低到0.1%以下 → 用户投诉大幅下降
4. 长期治理策略
├── 建立性能基线,每个版本对比
├── 性能劣化自动归因到责任人
├── 定期的性能专项治理
└── 低端机专项适配(功能降级策略)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 14.4 性能优化常见面试题
问题:如何监控应用的帧率?
// 方法1:使用Choreographer回调
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long lastFrameTimeNanos = 0;
int frameCount = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
}
frameCount++;
long diff = frameTimeNanos - lastFrameTimeNanos;
if (diff >= 1_000_000_000) { // 每秒计算一次
float fps = frameCount * 1_000_000_000f / diff;
Log.d("FPS", "当前帧率: " + fps);
frameCount = 0;
lastFrameTimeNanos = frameTimeNanos;
}
// 注册下一帧回调
Choreographer.getInstance().postFrameCallback(this);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
问题:如何定位ANR?
ANR定位方法:
1. 查看traces.txt
/data/anr/traces.txt
包含ANR时所有线程的堆栈信息
找到main线程的堆栈 → 定位卡在哪里
2. 查看EventLog
adb logcat -b events | grep anr
包含ANR的时间、进程、原因
3. Systrace/Perfetto分析
查看ANR前后主线程的执行情况
分析是什么操作阻塞了主线程
4. 线上ANR监控
├── FileObserver监听/data/anr/目录
├── ANR-WatchDog定时检查主线程响应
└── 收集ANR时的堆栈和系统信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 十五、总结
性能优化知识图谱:
卡顿原理
├── 渲染管线 → VSync + Choreographer + RenderThread
├── 掉帧原因 → 主线程耗时/GC/IO/锁等待
└── ANR → Input 5s / Broadcast 10s / Service 20s
监控方案
├── Looper Printer → BlockCanary
├── Choreographer → 帧率监控
├── ANR-WatchDog → ANR预警
├── 字节码插桩 → 方法级耗时
└── Systrace/Perfetto → 系统级分析
优化实战
├── 启动优化 → 延迟/异步/按需初始化
├── 布局优化 → 层级/过度绘制/RecyclerView
├── IO优化 → SP替代/异步IO/数据库事务
├── 线程优化 → 统一线程池/协程
└── 内存优化 → 减少GC/避免抖动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
性能优化是一个系统性工程,需要从监控、定位、修复、验证四个环节形成闭环。理解底层原理是做好性能优化的基础。
上次更新: 2026/06/10, 11:13:41