目录
[1. 指标口径要统一](#1. 指标口径要统一)
[2. 启动时长](#2. 启动时长)
[3. 帧率 / 掉帧](#3. 帧率 / 掉帧)
[4. 内存(PSS / Java / Native)](#4. 内存(PSS / Java / Native))
[5. CPU](#5. CPU)
[6. Crash 率](#6. Crash 率)
[7. 包体积](#7. 包体积)
[8. 耗电量](#8. 耗电量)
[9. 流量](#9. 流量)
[1. 简介](#1. 简介)
[2. adb 命令:dumpsys 三件套](#2. adb 命令:dumpsys 三件套)
[2.1 dumpsys meminfo ------ 内存快照](#2.1 dumpsys meminfo —— 内存快照)
[2.2 dumpsys gfxinfo ------ 掉帧统计](#2.2 dumpsys gfxinfo —— 掉帧统计)
[2.3 dumpsys batterystats ------ 耗电审计](#2.3 dumpsys batterystats —— 耗电审计)
[2.4 其它常用(同属 dumpsys 家族)](#2.4 其它常用(同属 dumpsys 家族))
[3. Systrace:理解原理即可,不必深用](#3. Systrace:理解原理即可,不必深用)
[3.2核心读图法(这套方法直接平移到 Perfetto)](#3.2核心读图法(这套方法直接平移到 Perfetto))
[4. Perfetto:2026 年的主力,必须吃透](#4. Perfetto:2026 年的主力,必须吃透)
[4.1 关键概念(相对 Systrace 的三大升级)](#4.1 关键概念(相对 Systrace 的三大升级))
[4.2 监控:三种抓取方式](#4.2 监控:三种抓取方式)
[4.3 读图:三个必看轨道](#4.3 读图:三个必看轨道)
[4.4 SQL 分析:拉开差距的地方](#4.4 SQL 分析:拉开差距的地方)
[heapprofd:顺手解决 native 内存](#heapprofd:顺手解决 native 内存)
[4.5 优化闭环(标准动作)](#4.5 优化闭环(标准动作))
[1. Android Studio Profiler:四大件](#1. Android Studio Profiler:四大件)
[1.1. CPU Profiler ------ 找"谁在吃时间"](#1.1. CPU Profiler —— 找"谁在吃时间")
[1.2. Memory Profiler ------ 找"谁赖着不走"](#1.2. Memory Profiler —— 找"谁赖着不走")
[1.3. Network Profiler ------ 找"谁在乱发请求"](#1.3. Network Profiler —— 找"谁在乱发请求")
[1.4. Energy Profiler ------ 找"谁在偷偷唤醒"](#1.4. Energy Profiler —— 找"谁在偷偷唤醒")
[二、Layout Inspector ------ 布局的内窥镜](#二、Layout Inspector —— 布局的内窥镜)
[2.2 监控能看什么:](#2.2 监控能看什么:)
[3. APK Analyzer ------ 出厂前的终检](#3. APK Analyzer —— 出厂前的终检)
[3.1 关键概念](#3.1 关键概念)
[3.2 三大功能](#3.2 三大功能)
[1. LeakCanary:内存泄漏的自动哨兵](#1. LeakCanary:内存泄漏的自动哨兵)
[1.1 关键概念:判定逻辑比"对象还在"更精妙](#1.1 关键概念:判定逻辑比"对象还在"更精妙)
[1.2 监控:读懂泄漏报告](#1.2 监控:读懂泄漏报告)
[2. BlockCanary:卡顿监控的"思想母体"](#2. BlockCanary:卡顿监控的"思想母体")
[2.1 关键概念:一个精妙到值得背诵的原理](#2.1 关键概念:一个精妙到值得背诵的原理)
[2.2 必须讲清的三个局限(这才是"初识"的精髓)](#2.2 必须讲清的三个局限(这才是"初识"的精髓))
[2.3 它留下的范式(后面所有工具都在用)](#2.3 它留下的范式(后面所有工具都在用))
[2.4 使用与优化闭环](#2.4 使用与优化闭环)
[3. Matrix:生产级的线上 APM 全家桶](#3. Matrix:生产级的线上 APM 全家桶)
[3.1 关键概念:从"单个探针"到"监控体系"](#3.1 关键概念:从"单个探针"到"监控体系")
[3.2 为什么它是"工业级":以 TraceCanary 为例看代差](#3.2 为什么它是"工业级":以 TraceCanary 为例看代差)
[3.3 线上使用的三大纪律(方法论重点)](#3.3 线上使用的三大纪律(方法论重点))
[3.4 优化闭环(线上版标准动作)](#3.4 优化闭环(线上版标准动作))
前言
先学怎么度量,才能有优化。没有度量就没有优化。下面是这篇文章的主要内容。

一、性能指标体系
1. 指标口径要统一
只有指标判断的口径指标要统一,才能做横向对比和回归监控。三个通用原则:
-
口径要固定:比如启动时长,是"进程创建→第一帧绘制"还是"点击图标→首屏数据可交互",必须写死,团队统一。
-
看分位数,不看平均值:平均值会被少量高端机拉漂亮。线上看 P50 / P90 / P95,P90 代表"九成用户的体验",优化优先级最高。
-
设备要分档:同一指标在旗舰机和千元机上的区别是很大的。监控时按高/中/低端分桶,低端机往往才是瓶颈所在。
2. 启动时长
2.1关键概念
-
冷启动:进程不存在,系统 fork 进程 → 加载类 → Application → 首帧,耗时最长,是优化主战场。
-
温启动:进程在但 Activity 被回收,走 onCreate 重建。
-
热启动:进程和 Activity 都在,只走 onResume,基本不用优化。
-
TTID(Time to Initial Display):第一帧绘制完成的时间。
-
TTFD(Time to Full Display):首屏内容完全可交互的时间,更贴近用户体感。
-
冷启动的系统链路 :
ActivityThread.main()→handleBindApplication()→ ContentProvider 初始化(容易被忽视的隐形耗时)→Application.onCreate()→Activity.onCreate()→ 首帧。每个环节都可拆解度量。
2.2监控
bash
adb shell am start -W <pkg>/<MainActivity>
# 输出 TotalTime(含进程创建)和 WaitTime
-
Logcat 里系统会自动打印
Displayed com.xxx/.MainActivity: +1s234ms。 -
代码里调用
reportFullyDrawn()标记 TTFD,Logcat 会多出Fully drawn日志。 -
Macrobenchmark(Jetpack 库):CI 上自动跑启动测试,输出分位数,做回归防线。
-
线上:进程启动时打时间戳,首帧回调时上报差值,按设备分档聚合。
2.3优化
-
Application 减负:第三方 SDK 懒加载/按需初始化;异步初始化的放子线程(注意依赖顺序)。
-
消灭 ContentProvider 偷跑 :很多 SDK 用 ContentProvider 自动初始化(包括老的 androidx.startup),
dumpsys或 trace 能看到,手动改为懒加载。 -
Baseline Profile:把启动路径的热点代码预编译进 AOT,官方数据可提速 20~30%,接入成本低,性价比高。
-
启动页障眼法 :SplashScreen API +
windowBackground主题背景,让用户"感觉"快。 -
首屏布局减重:减少层级、避免首帧前解析大布局(可用 AsyncLayoutInflater)。
-
Perfetto 抓冷启动 trace:看主线程哪段在跑什么,是 IO、锁还是布局耗时,对症下药。
3. 帧率 / 掉帧
3.1关键概念
-
Vsync:垂直同步信号,60Hz 屏幕每 16.6ms 一次,120Hz 每 8.3ms 一次。每一帧的 UI 生产必须在一个 Vsync 周期内完成。
-
渲染管线:主线程 measure/layout/draw → 同步给 RenderThread → GPU 渲染 → SurfaceFlinger 合成上屏。任何一环超时都会掉帧。
-
Jank(掉帧):一帧耗时超过 Vsync 周期即算一次 jank。
-
平均 FPS 是陷阱 :平均 59 FPS 可能是"59 帧 1ms + 1 帧 1000ms",体感照样卡。正确口径是 jank 率 和 慢帧占比(如 >16.6ms / >50ms 的帧占比)。
3.2监控
bash
adb shell dumpsys gfxinfo <pkg> framestats # 最近 120 帧的各阶段耗时
-
Profile GPU Rendering(开发者选项):屏幕上叠加彩色条形图,每根柱子是一帧,超过 16.6ms 横线即掉帧,颜色分段对应各渲染阶段。
-
FrameMetrics API (Android 7+)/ JankStats 库:代码级获取每帧耗时,适合线上上报。
-
Perfetto :看
android.surfaceflinger和 app 的 frame timeline,精确到哪一帧、哪个阶段超时。
3.3优化
-
布局减重:减少嵌套层级;ConstraintLayout 扁平化;merge/ViewStub。
-
RecyclerView 专项 :ViewHolder 复用、DiffUtil 局部刷新、
onBindViewHolder里禁止耗时操作和对象分配、图片异步加载 + 预加载下一屏。 -
图片治理 :加载尺寸不超过控件尺寸,列表滑动时暂停加载(Glide 的
pauseRequests)。 -
主线程让路:IO、JSON 解析、SP 读写挪出主线程;减少锁竞争。
-
onDraw 纪律:不在 onDraw 里 new 对象(会触发 GC 抖动);Path/Paint 复用。
-
过度绘制:开发者选项打开"显示 GPU 过度绘制",红色区域要消掉(多余背景、层叠不透明 View)。
4. 内存(PSS / Java / Native)
4.1关键概念
-
PSS(Proportional Set Size) :进程独占内存 + 共享内存按比例分摊,是衡量应用内存的标准口径。对比理解:RSS 把共享库全算给你(偏高),USS 只算独占(偏低)。
-
Java 堆 :受
maxMemory()限制,超了抛 OOM。ART 的 GC 会自动回收,但回收有代价(卡顿)。 -
Native 堆 :C/C++ 分配,Bitmap 像素数据在 Android 8.0 后也放这里。Java 堆没爆也可能 OOM------总内存超了系统照样杀。
-
内存抖动(Churn):短时间内大量创建回收对象,GC 频繁触发,主线程被 GC 抢占导致卡顿。监控上看是内存曲线锯齿状。
-
内存泄漏:长生命周期对象持有了短生命周期对象的引用(经典:静态变量持有 Activity),导致回收不掉,越用越高。
-
LMK(LowMemoryKiller):内存紧张时系统按 oom_adj 分数杀进程,前台应用内存占用越低越安全。
4.2监控
bash
adb shell dumpsys meminfo <pkg> # 看 PSS 总表:Java Heap / Native Heap / Graphics / Code / Stack
-
Profiler Memory :实时曲线 + Dump HPROF 堆快照,按 Retained Size 排序找大头。
-
LeakCanary:开发期自动检测 Activity/Fragment 泄漏并给出引用链,接入零成本。
-
线上:定期采样 PSS 上报;监控 OOM 崩溃、FD 数量、线程数。
4.3优化
-
修泄漏(收益最大):LeakCanary 报什么修什么。高频来源:静态引用、未注销的监听/广播、Handler 内部类、WebView、未取消的动画和 RxJava 订阅。
-
图片内存:Glide 的 BitmapPool 复用;按控件尺寸加载;大图用子采样(inSampleSize)。
-
消抖动:onDraw/onBind 里不分配对象;用对象池;避免自动装箱(int→Integer);SparseArray 替代 HashMap<Integer, E>。
-
Native 侧:so 里 malloc/new 配对释放;Bitmap 用完 recycle(8.0 前尤其重要)。
-
进程治理:非必要不多进程;大内存操作(WebView、播放器)独立进程,用完杀掉。
5. CPU
5.1关键概念
-
CPU 使用率:分用户态(us)和内核态(sy)。sy 高通常是 IO、锁、系统调用过多。
-
big.LITTLE 调度:大小核架构,主线程任务太重可能被调度到小核上"越跑越慢"。
-
线程数:不是越多越好,线程多了上下文切换开销大,且每个线程默认栈占 1MB 虚拟内存。
5.2监控
bash
adb shell top -H # 看线程级 CPU 占用
adb shell dumpsys cpuinfo # 各进程负载
-
Profiler CPU :三种采样------Java/Kotlin 方法采样、方法 trace、System Trace(能看到调度和锁等待,最接近 Systrace)。
-
Perfetto :
sched事件看线程在哪颗核上跑、被谁抢占、锁等待多久。
5.3优化
-
主线程只做 UI:计算、解析、IO 全走线程池。
-
线程池治理 :统一池化,拒绝到处
new Thread();按任务类型分 IO 池/CPU 池。 -
算法和频次:减少循环内重复计算;降低定时器/轮询频率;懒计算。
-
减少 GC:内存抖动和 CPU 是连动的,消抖动同时降 CPU。
6. Crash 率
6.1关键概念
-
Java Crash :未捕获异常沿调用栈上抛到
UncaughtExceptionHandler,进程死亡。 -
Native Crash :so 层段错误等,系统生成 tombstone 文件,需要符号表(symbol)还原堆栈。
-
口径 :UV 崩溃率(崩溃用户/活跃用户)比 PV 更能反映影响面。Vitals 红线:1.09%。
-
注意 Crash 和 ANR 是两条独立的线,用户体感都差,但治理方法不同。
6.2监控
-
自建:
Thread.setDefaultUncaughtExceptionHandler()捕获 → 写本地 → 下次启动上报。 -
成熟 SDK:Firebase Crashlytics、Bugly(国内常用);Native 崩溃用 Breakpad/Crashpad。
-
Play Vitals。
6.3优化
-
空指针治理:Kotlin 空安全、判空规范,NullPointerException 常年霸榜。
-
集合类越界、并发修改:CopyOnWrite、并发容器。
-
灰度发布:小流量先验证,崩溃飙升立刻放量刹车/回滚。
-
兜底:高频闪退点用 try-catch 局部兜底(注意只兜已知可恢复的,别把异常全吞了掩盖问题)。
-
Native:发版时务必保留符号表并上传到崩溃平台,否则堆栈全是地址没法看。
7. 包体积
7.1关键概念
-
APK 构成 (APK Analyzer 一眼看穿):
classes.dex(代码)、res/+resources.arsc(资源)、lib/(so 库,往往是大头)、assets/、META-INF。 -
下载大小 vs 安装大小:商店显示的是下载大小,安装解压后更大。
-
App Bundle:Google Play 按设备分发,只下发所需 so 和分辨率资源,天然瘦身。
7.2监控
-
APK Analyzer:看各组成占比,找增量。
-
CI 上定期构建对比基线,超出阈值报警(体积回归是最容易"温水煮青蛙"的)。
-
Matrix 的 ApkChecker 可做依赖和资源冗余分析。
7.3优化(按投入产出比)
-
so 裁剪 :
abiFilters只保留arm64-v8a(国内生态已可接受),去掉 x86;不必要的 so 动态下发。 -
资源压缩 :
shrinkResources true(配合 R8);PNG 转 WebP/AVIF;能用矢量图(VectorDrawable)不用位图。 -
资源混淆:AndResGuard 把资源路径压短,再省一截。
-
代码裁剪:R8/ProGuard 全量混淆;定期清理无用依赖和死代码。
-
资源上云:大图、音频、皮肤类资源改启动后按需下载。
-
resConfigs "zh"去掉无用语言资源。
8. 耗电量
8.1关键概念
-
耗电大户:CPU 持续唤醒、屏幕、网络射频(蜂窝最耗电)、GPS、传感器。
-
WakeLock:阻止 CPU 休眠,忘了释放就是"应用退了电还在掉"。
-
Doze 模式 / App Standby:系统会对后台行为分批、延迟执行,你的"准时轮询"在 Doze 下根本不准时------所以要顺应系统调度,而不是对抗它。
-
电量无法直接测量单应用数值,是系统按各组件使用时间估算分摊的。
8.2监控
bash
adb shell dumpsys batterystats
-
Battery Historian:把 batterystats 导出渲染成时间轴,看 WakeLock、网络、GPS 何时活跃。
-
Profiler Energy:开发期直观查看。
-
Vitals:excessive wakeups / stuck partial wake locks 指标。
8.3优化
-
网络合并:零散请求合并批量发送,射频唤醒次数比总流量更影响耗电。
-
后台任务交给 WorkManager/JobScheduler:系统统一调度,比自建 Alarm 省电。
-
WakeLock 审计:确保 acquire/release 配对,加超时参数兜底。
-
定位降频:不需要精确定位时用被动定位/降低采样率。
-
消灭轮询和无限动画:页面不可见时停掉动画和定时刷新。
9. 流量
9.1关键概念
-
区分前台流量 / 后台流量:后台偷跑流量是用户投诉重灾区。
-
长连接心跳、埋点上报、预加载策略是流量的隐形大头。
-
弱网下的大请求既是流量问题也是体验问题(和启动、ANR 联动)。
9.2监控
-
TrafficStats API:代码里按 UID 统计收发字节数,可区分前后台。
-
Profiler Network:开发期看每次请求的大小和耗时。
-
C**harles/抓包:**看是否有重复请求、冗余字段。
-
**线上:**接口级埋点统计请求/响应大小。
9.3优化
-
缓存:HTTP 缓存头(ETag/Last-Modified)、本地数据缓存,能不发就不发。
-
压缩:开 gzip/br;协议从 JSON 换 Protobuf 可省一半以上。
-
图片按需:列表加载缩略图,详情才加载大图;走 CDN 按屏幕分辨率出图。
-
增量更新:数据同步用增量;应用自身更新用差分包。
-
预取克制:WiFi 才预取大资源;流量模式下降级。
-
合并与心跳:埋点批量上报,心跳间隔动态调整。
二、系统工具
1. 简介
-
dumpsys 系列:回答"现在是多少"。结果型快照,数值直出,可脚本化。
-
Systrace:回答"什么时候发生了什么"。过程型时间轴,但已是上一代产品。
-
Perfett:同样是时间轴,但数据全、带 SQL 查询引擎,2026 年的绝对主力。
2. adb 命令:dumpsys 三件套
-
dumpsys的本质:系统服务的自述报告 。Android 每个系统服务(ActivityManager、WindowManager、BatteryStats......)都实现了dump()方法,dumpsys 就是把它们的内部状态打印出来。 -
三个特性要记住:无需集成 SDK、大部分无需 root、可随时执行------这决定了它是线上灰度机、CI 自动化、用户反馈现场的首选采集手段。
-
局限:它是快照,告诉你"累计是多少",不告诉你"哪个时刻、为什么"。要追过程,交给 Perfetto。
2.1 dumpsys meminfo ------ 内存快照
bash
adb shell dumpsys meminfo <pkg>
输出怎么看(按行从上往下读):
| 行 | 含义 | 关注点 |
|---|---|---|
| Java Heap | Java/Kotlin 对象占用 | 接近 maxMemory() 上限 → 要 OOM |
| Native Heap | C/C++ 分配(8.0 后含 Bitmap 像素) | 持续增长不回落 → native 泄漏嫌疑 |
| Graphics | 图形缓冲区(Surface/纹理) | 列表页飙高 → 图片问题 |
| Code | dex/so 加载占用 | 基本不用管 |
| TOTAL PSS | 总内存口径 | 所有对比的基准数 |
底部还有两个隐藏金矿:
-
Views / Activities 数量:退出一个页面后再 dump,如果 Activities 数没归零 → Activity 泄漏实锤,接着上 LeakCanary 找引用链。
-
App Summary 段(高版本系统):一张 PSS 分类汇总表,适合贴进博客和报告。
监控玩法(脚本化是灵魂):
bash
# 每 5 秒采样一次 TOTAL PSS,写进文件
while true; do
adb shell dumpsys meminfo <pkg> | grep "TOTAL PSS" >> mem_log.txt
sleep 5
done
典型用例:反复进出同一页面 20 次,PSS 阶梯式上涨不回落 = 泄漏;Monkey 压测 30 分钟后内存没回到基线 = 有持有没释放。
优化闭环:PSS 高 → 看哪一行高 → Java 高用 Profiler dump HPROF,Native 高用 Perfetto heapprofd,Graphics 高查图片加载尺寸和缓存。
2.2 dumpsys gfxinfo ------ 掉帧统计
bash
adb shell dumpsys gfxinfo <pkg> # 汇总统计
adb shell dumpsys gfxinfo <pkg> framestats # 逐帧原始数据
adb shell dumpsys gfxinfo <pkg> reset # 清零!测试前必做
汇总段看什么:
bash
Janky frames: 38 (12.54%) ← 掉帧占比,核心指标
50th percentile: 8ms
90th percentile: 20ms ← P90 超 16.6ms 说明约一成用户体感卡顿
95th percentile: 32ms
99th percentile: 88ms ← P99 反映偶发严重卡顿(GC/IO 尖刺)
framestats 逐帧数据:最近 120 帧,每帧一行纳秒级时间戳。关键计算:
bash
掉帧量 = FrameCompleted - IntendedVsync
结果 > 16.6ms(60Hz)即掉帧。把这 120 行数据拷出来贴进 Excel/Python 画分布直方图,是写博客的绝佳素材。
监控玩法:reset → 手指匀速滑动列表 10 秒 → dump → 记录 janky 百分比。每次优化后重复此流程,数据可复现、可对比。
优化闭环:janky 占比高 → framestats 找到超时帧集中的场景 → 上 Perfetto 看那些帧里主线程在干什么(inflate?锁?IO?)→ 对症优化 → reset 重测对比。
2.3 dumpsys batterystats ------ 耗电审计
bash
adb shell dumpsys batterystats reset # 清零,测试开始
adb shell dumpsys batterystats # 查看
adb shell dumpsys batterystats --charged # 上次充电以来的完整记录
输出重点三段:
-
WakeLock 记录:谁在什么时间持有多久的 partial wakelock------后台耗电第一嫌疑人。
-
网络活动:各进程收发次数和字节数,唤醒次数比总量更影响耗电。
-
Job/Alarm 调度历史:后台任务的实际执行时间点。
监控玩法(灭屏静置测试):手机充满 → reset → 灭屏放置一晚 → dump。正常机器一晚安睡,如果看到你的应用持有了几百秒 wakelock 或唤醒了上千次,就是它把 CPU 拽醒的。
进阶 :adb bugreport 导出后扔进 Battery Historian 网页工具,渲染成时间轴图表,WakeLock、网络、GPS 的活跃时段一目了然。
优化闭环:wakelock 滥用 → 改 WorkManager/JobScheduler 让系统统一调度;唤醒频繁 → 合并任务、拉长间隔、去掉自建 Alarm 轮询。
2.4 其它常用(同属 dumpsys 家族)
-
dumpsys cpuinfo:各进程 CPU 负载排行 -
dumpsys activity lru:进程优先级/LRU,研究保活时用 -
dumpsys package <pkg>:权限、版本、安装信息 -
dumpsys SurfaceFlinger:合成层信息,掉帧深度分析时辅助
3. Systrace:理解原理即可,不必深用
3.1关键概念
-
定位:Android 第一代系统级时间轴工具,回答"这段时间里系统每一刻在干什么"。
-
原理 :基于内核 ftrace 采集 CPU 调度事件 + 各系统服务预置的埋点 + 应用自己打的标记,渲染成一个 HTML 文件。
-
2026 年的现实 :已被 Perfetto 全面取代,Android Studio 里入口已移除,命令行工具还停在 Python2 时代。但几乎所有经典性能博客的配图都是 Systrace,概念与 Perfetto 100% 互通------学它是为了看懂存量资料,顺带掌握 trace 分析的思维模型。
3.2核心读图法(这套方法直接平移到 Perfetto)
抓一条 trace:
bash
python systrace.py -a <pkg> -t 10 -o trace.html sched gfx view wm am binder_driver
打开后从上到下四个区域:
-
CPU 区:每核一条轨道,看任务被调度到哪颗核、是否被抢占。
-
SurfaceFlinger 区:系统合成器,看 Vsync 节奏和上屏。
-
应用进程行 :每帧一个圆点------绿色正常、黄色超 16.6ms、红色超 32ms。找卡不用猜,点黄点红点。
-
线程状态色块(最重要的分析语言):
| 颜色 | 状态 | 含义 |
|---|---|---|
| 绿 | Running | 正在 CPU 上跑 |
| 蓝 | Runnable | 能跑但没抢到 CPU(被抢占/排队) |
| 白 | Sleeping | 主动睡眠(正常的等待) |
| 橙/红 | Uninterruptible Sleep | 被 IO 或锁卡死------主线程出现这个颜色就是病灶 |
应用侧插桩(分析自己代码的必备手段):
bash
Trace.beginSection("inflateFeedItem")
// ...你的代码...
Trace.endSection()
抓出来的图上,你的函数会以具名区间嵌套显示在主线程时间轴上,谁慢谁快一目了然。
3.3优化闭环(经典三段式)
-
复现卡顿场景同时抓 trace;
-
找黄/红帧 → 放大 → 看主线程那段时间在跑什么区间、什么状态;
-
橙红色(IO/锁)→ 挪出主线程;inflate 区间长 → 减布局;Runnable 时间过长 → 查线程优先级和抢占。优化后重抓对比。
4. Perfetto:2026 年的主力,必须吃透
4.1 关键概念(相对 Systrace 的三大升级)
-
数据格式升级 :trace 存为 protobuf 二进制(
.pftrace),不再是臃肿 HTML;Android 10+ 系统内置 perfetto 守护进程,不用装任何东西。 -
Trace Processor------SQL 查询引擎(杀手锏) :整个 trace 可被 SQL 查询。这意味着分析可以批量化、脚本化、进 CI------这是 Systrace 永远做不到的。
-
数据源全家桶 :ftrace(调度/频率/binder)、atrace(应用插桩)、帧时间线、heapprofd(native 内存分配采样)、电量轨------一个工具覆盖内存、CPU、帧率、耗电四条线。
4.2 监控:三种抓取方式
① 命令行快速抓:
bash
adb shell perfetto -o /data/misc/perfetto-traces/trace.pftrace \
-t 20s sched freq idle am wm gfx view binder_driver dalvik
② 配置文件抓(推荐,可控性强) :写一份 config.pbtxt 指定数据源、时长、ring buffer 大小,长时间后台监控就靠它。
③ 开发者选项 → System Tracing:下拉快捷开关,图形化勾选数据源,按一下就开始录------复现用户现场问题时最好使。
分析界面 :打开 ui.peretto.dev,把 .pftrace 拖进去,纯网页无需安装。
4.3 读图:三个必看轨道
-
Expected Timeline / Actual Timeline:帧分析的精华。Expected 是"应该何时上屏",Actual 是"实际上屏",错位处直接标出 janky 帧,比 Systrace 的圆点更精确。
-
CPU 调度轨道:线程状态色块逻辑与 Systrace 完全一致(绿跑蓝等白睡橙卡)。
-
主线程 slice 轨道 :
Trace.beginSection插桩的区间在这里呈树状嵌套,M 键标记区间可测任意段耗时。
4.4 SQL 分析:拉开差距的地方
UI 左下角进 Query 页签,直接对 trace 跑 SQL:
sql
-- 找出耗时超过 10ms 的所有函数区间,降序排列
SELECT name, dur / 1000000.0 AS ms
FROM slice
WHERE dur > 10000000
ORDER BY dur DESC
LIMIT 50;
sql
-- 统计某页面滑动期间掉帧帧数
SELECT count(*) FROM actual_frame_timeline_slice WHERE jank_type != 'None';
这套能力的真正价值在于自动化:CI 上每次发版跑固定场景 → 抓 trace → 同一套 SQL 出指标报告 → 超阈值报警。性能回归从"人肉盯"变成"流水线卡口"。
heapprofd:顺手解决 native 内存
Perfetto 内置的 native 堆采样器,配置里打开后能在 UI 里看到每个 so、每个调用栈的内存分配火焰图------dumpsys meminfo 发现 Native Heap 异常后的下一站。
4.5 优化闭环(标准动作)
-
复现场景抓 trace(尽量让场景可重复);
-
UI 定位慢帧/慢区间,记下嫌疑函数;
-
SQL 批量统计该场景所有 slice 耗时,出 Top N 清单------优化按清单从大到小做,不凭感觉;
-
优化后重抓,用同一套 SQL 对比前后数据,把对比表贴进博客------读者最爱看这个。
三、IDE工具
1. Android Studio Profiler:四大件
先理解通用骨架
-
打开:View → Tool Windows → Profiler,run 到设备后自动 attach。
-
所有子工具共享同一条时间轴 ------这是它最大的隐藏价值:CPU 尖峰的那个时刻,能直接看到当时 Network 在发什么请求、Memory 涨了多少。多维联动,单工具做不到。
-
Session 可导出(.trace / .hprof),发给同事或贴进博客当证据。
1.1. CPU Profiler ------ 找"谁在吃时间"
关键概念:三种记录模式,选错就白抓
| 模式 | 原理 | 开销 | 适用 |
|---|---|---|---|
| Callstack Sample | 周期性采栈 | 低 | 日常首选,抓热点 |
| Trace Methods(插桩) | 记录每个方法进出 | 高,且会失真 | 小范围精测 |
| System Trace | 系统级调度/锁/帧数据 | 中 | 看锁竞争、调度、掉帧 |
经验法则:先 Sampled 找大方向,怀疑锁/调度问题再 System Trace;插桩模式慎用------它会让被测代码变慢几倍,数据只能看相对比例。
关键概念:四种视图,各管一件事
-
Call Chart :时间轴上的调用火焰,横轴是时间。找"哪段时间在干什么",复现卡顿时看这个。
-
Flame Chart :聚合火焰图,累计耗时越宽的方块越可疑。找"整体热点"。
-
Top Down:从顶层调用者往下展开,看主路径。
-
Bottom Up :从某个函数反查"都有谁调用了我 "------比如发现
JSONObject.<init>耗时高,用它一秒定位所有解析点。
监控/优化闭环:
-
复现卡顿 → Record 一段 Sampled trace;
-
Call Chart 找到主线程长区间 → 点开看是什么方法;
-
典型病灶及对策:
-
主线程 JSON/SP/DB → 挪子线程;
-
inflate占大头 → 减布局层级、ViewStub 延迟加载; -
Lock.wait/ monitor contention(System Trace 可见)→ 缩小锁粒度、换无锁结构;
-
-
优化后重录,同场景对比火焰宽度。
1.2. Memory Profiler ------ 找"谁赖着不走"
关键概念:先看懂彩色曲线 Java / Native / Graphics / Stack / Code / Others 分色堆叠。两种经典形状要一眼认出:
-
锯齿状高频抖动 → 内存抖动(GC 太勤,会卡顿);
-
阶梯式只涨不跌 → 泄漏嫌疑。
核心操作三连(顺序固定):
-
反复执行可疑场景(如进出某页面 10 次);
-
点 Force GC(垃圾桶图标)------先挤掉可回收的水分;
-
Capture heap dump 抓 HPROF 快照。
Heap dump 四列数据,重点是第三列:
| 列 | 含义 |
|---|---|
| Allocations | 实例个数 |
| Shallow Size | 对象自身占多少 |
| Retained Size | 这个对象死了,能连带释放多少------按它排序找大头 |
| Depth | 距 GC Root 的引用链深度 |
实操心法:
-
按 Retained Size 降序,Bitmap、字节数组、集合类通常霸榜;
-
点进可疑实例,右侧 References 面板看引用链,勾选 "Show nearest GC root"------泄漏分析就是回答"谁拽着它不放";
-
装了 LeakCanary 的话,heap dump 里泄漏对象直接带漏水标记,跳转即得引用链;
-
想抓抖动来源:用 Record allocations,回放一次场景,看哪类对象被疯狂 new。
优化闭环:Retained 排序 → 引用链找 GC Root → 修(解注册/清缓存/复用池)→ 重走场景重新 dump,实例数应归零。
1.3. Network Profiler ------ 找"谁在乱发请求"
关键概念:
-
时间轴上每个网络请求是一个区间块,长短即耗时;
-
三视图:总时间轴、Connection view(按连接聚合)、Thread view;
-
点开单请求看:请求/响应字节数、耗时、调用栈(跳到发请求的代码行)。
能直接看出的四类病:
-
重复请求:同一接口短时间连发 → 加防抖/缓存;
-
无缓存命中:每次全量 200,从未 304 → 检查缓存头/本地缓存策略;
-
大响应:一个 JSON 几百 KB → 分页、瘦身、Protobuf;
-
串行瀑布:请求 B 等 A 完成才发 → 改并行或合并接口。
局限要知道:只认系统网络栈和 OkHttp 系,自研 socket 看不全;HTTPS 明文内容看不了(要内容用 Charles/抓包)。
1.4. Energy Profiler ------ 找"谁在偷偷唤醒"
关键概念:
-
它是估算模型(CPU+网络+GPS+传感器的加权),不是真实功耗------精确账还是 batterystats 那一套;
-
真正的价值在时间轴上的事件标记:WakeLock acquire/release、Alarm、Job 何时发生、持续了多久。
典型用法: 页面退出 / App 退后台后,盯着时间轴看------如果 wakelock 事件条还亮着、alarm 还在规律打点,就是后台耗电病灶,点击事件直接跳对应代码。
优化闭环:发现退后台仍持锁 → 查 acquire/release 配对 → 改 WorkManager → 重测事件条应消失。
二、Layout Inspector ------ 布局的内窥镜
2.1关键概念:
-
运行时布局树快照:把当前屏幕真实的 View 层级扒出来,左边树、中间 3D 分层视图、右边属性面板;
-
3D 旋转视图是精髓------层级嵌套深度直接变成"楼层厚度",哪块布局臃肿一眼看穿;
-
2026 年要额外知道:对 Compose 同样适用,且能显示 recomposition counts(重组次数)------某个组件重组计数狂涨就是 Compose 卡顿的头号线索。
2.2 监控能看什么:
-
层级过深:LinearLayout 套 LinearLayout 套五六层 → 改 ConstraintLayout 扁平化;
-
该消失没消失:一个永远 GONE 的弹窗还在树里参与 measure → 换 ViewStub;
-
属性实况:padding/margin/宽高 的实际计算值,布局错乱时不用猜;
-
View 与代码对号:点节点跳布局 XML(带资源解析)。
优化闭环 :3D 视图发现某分支层级厚 → 逐层看是否可 merge/扁平化 → 改完重新 inspect,层级数应下降。配合开发者选项的"过度绘制"用:Inspector 管结构,Overdraw 管渲染,一个页面两个视角各查一遍。
3. APK Analyzer ------ 出厂前的终检
3.1 关键概念
前面所有工具都是"运行时"视角,APK Analyzer 是静态视角------不运行 App,直接解剖最终产物,看里面到底装了什么。路径:Build → Analyze APK,或直接把 APK/AAB 拖进 Android Studio。
3.2 三大功能
① 体积组成树(瘦身第一步)
-
dex / res / lib / assets / resources.arsc 各占多少 MB,降序排列;
-
第一眼看占比:lib(so 库)通常是最大的隐藏肥肉,其次是 res 里的大图。
② DEX 查看器
-
看方法引用总数------逼近 64K 要 MultiDex,心中有数;
-
Defined vs Referenced 方法数对比,能粗略看 R8 裁剪效果;
-
检查"不该进来的东西":某个只用于测试的库、别的业务线的 SDK 被打进来了------组件化项目常见问题,树里直接现形。
③ Manifest / resources.arsc 终态查看
-
看合并后的最终 Manifest:三方 SDK 悄悄声明了什么权限、什么组件,发版前必须过一眼(权限合规就靠这步兜底);
-
resources.arsc 里查资源冗余。
杀手级功能:Compare with previous APK
-
选两个 APK 做 diff,逐文件列出体积增减;
-
版本间体积回归分析的人工版------新版大了 3MB 到底大在哪,diff 完直接点名。瘦身优化后用它出"前后对比表",博客证据就有了。
优化闭环:组成树找大头(通常是 so 和大图)→ abiFilters 裁剪 / WebP 转换 / shrinkResources → 重新打包 → Compare 验证每个大头的变化量。
四、线上工具初识
把探针埋进 App,跟版本一起发出去,替你在用户手机里站岗。同时带来一个新矛盾------监控本身也消耗性能,所以"采样率、动态开关、聚合上报"是线上工具区别于开发工具的三大纪律。
这三个工具恰好是三个时代的代表:
-
LeakCanary:开发期自动报警的标杆(内存方向);
-
BlockCanary:卡顿监控的"思想母体"(已停更,但所有后来者都是它的演进);
-
Matrix:生产级线上 APM 全家桶(微信的工业化答卷)。
1. LeakCanary:内存泄漏的自动哨兵
1.1 关键概念:判定逻辑比"对象还在"更精妙
核心思想一句话:泄漏不是"对象活着",而是"该死的时候没死"。
工作流程四步(背下来,面试和排查都用得上):
-
埋点 :Activity/Fragment 走到
onDestroy时,LeakCanary 把它包装进KeyedWeakReference(弱引用)并关联一个ReferenceQueue; -
等待 GC:弱引用的特性------对象被回收后,弱引用会进入 ReferenceQueue。等几秒、触发一次 GC;
-
判定 :队列里没等到这个引用 → 该回收没回收 → 疑似泄漏,自动 dump 堆内存(HPROF);
-
分析 :用自家解析器 Shark 计算每个存活对象到 GC Root 的最短引用链,找出"拽着它不放"的那一环,弹通知给你。
配套要理解的点:
-
接入零成本 :
debugImplementation一行依赖,2.x 版本靠 ContentProvider 自动初始化,不用写代码------所以你也明白了为什么它绝不能上线上(dump 堆会冻结 App 几秒); -
默认监控 Activity、Fragment、View 等,任何自定义对象可手动监控:
Kotlin
AppWatcher.objectWatcher.watch(myObj, "播放器实例退出后未释放")
1.2 监控:读懂泄漏报告
通知点开后的引用链是从下往上读 (从泄漏对象向 GC Root 方向),带 ~~~~~ 下划线的是 Shark 推断的最可疑环节。一条典型链:
MainActivity has leaked:
static Singleton.sListener
↳ Listener.this$0 ← 内部类隐式持有外部类,元凶在这
↳ MainActivity instance ← 本该死了
优化闭环:高频泄漏模式清单
LeakCanary 报的问题基本都落在下面这张表里,按出场率排序:
| 泄漏模式 | 修法 |
|---|---|
| 单例/静态变量持有 Activity 的 Context | 改用 applicationContext |
| Handler 非静态内部类隐式持有外部类 | static + WeakReference,或 onDestroy 里 removeCallbacksAndMessages(null) |
| EventBus/RxBus/监听器注册了没注销 | 生命周期对称:onCreate 注册 → onDestroy 注销 |
| RxJava 订阅 / 协程任务未取消 | dispose / 结构化并发(lifecycleScope) |
| 无限动画持有 View | 页面销毁时 cancel() |
| WebView 泄漏(系统级,修不干净) | 放独立进程,用完杀进程 |
验证动作 :修完重走场景------LeakCanary 不再报警 + dumpsys meminfo 里 Activities 计数归零,双保险。
2. BlockCanary:卡顿监控的"思想母体"
2.1 关键概念:一个精妙到值得背诵的原理
它不 hook 系统、不改代码,利用的是 Looper 自带的日志出口:
主线程 Looper 处理每个 Message 的前后,都会调用 Printer 打印:
>>>>> Dispatching to ... ← Message 开始处理
<<<<< Finished to ... ← Message 处理结束
于是它的做法:
-
Looper.getMainLooper().setMessageLogging(myPrinter)换掉这个 Printer; -
自定义 Printer 记录两条日志的时间差 = 这个 Message 的处理耗时;
-
超过阈值(默认 1 秒,可配)→ 抓取主线程堆栈 + CPU 状态 + 内存,落盘报警。
2.2 必须讲清的三个局限(这才是"初识"的精髓)
-
盲区 :它测的是 Message 处理耗时 。如果卡发生在 Choreographer 帧回调之外的 native 渲染层(RenderThread),或卡在两 Message 之间的间隙,它感知不到------它抓的是"严重卡顿/准 ANR",不是掉帧(16.6ms 级的卡顿它管不着)。
-
采样误差:超时才去抓堆栈,这意味着抓到的堆栈信息可能已经滞后于实际导致卡顿的代码执行点。例如,一个耗时方法可能在 Message 处理的前半段就已经执行完毕,但 BlockCanary 在超时阈值(如 1 秒)到达后才触发采样,此时主线程可能已经在执行其他无关代码,导致抓到的堆栈无法精确定位到真正的"元凶"。这种采样误差在偶发性、间歇性卡顿的排查中尤为明显,可能误导开发者去优化一个"替罪羊"方法。因此,BlockCanary 的报告更适合作为问题存在的"信号",而非精确的"诊断书"。要准确定位,仍需结合 Perfetto 或 Systrace 的时间轴分析,观察卡顿发生时主线程上具体是哪个函数区间(slice)在长时间执行。
-
已停更多年 :不要真的发到线上去,学它是为了掌握范式。
2.3 它留下的范式(后面所有工具都在用)
探测点(Printer)+ 阈值判定 + 超时抓栈 + 聚合上报
微信 Matrix、手淘 APM、各家自研卡顿监控,全是这个范式的工业化版本。理解 BlockCanary = 拿到理解一切卡顿监控的钥匙。
2.4 使用与优化闭环
接入后故意在主线程 Thread.sleep(2000) 试一发,读它的报告:
-
堆栈:主线程当时在干什么------主线程 IO(SP/DB)、锁等待、inflate、JSON 解析是四大常客;
-
CPU 负载 字段别忽略:如果卡顿瞬间 CPU 满载,可能是别的线程/进程在抢资源,主线程只是受害者------这时优化方向就不是改主线程代码,而是查后台任务。
3. Matrix:生产级的线上 APM 全家桶
3.1 关键概念:从"单个探针"到"监控体系"
微信出品,模块化设计,按需取用:
| 模块 | 管什么 | 对应前面的知识点 |
|---|---|---|
| TraceCanary | 掉帧、卡顿、启动耗时、ANR | 帧率 + ANR 指标 |
| ResourceCanary | Activity 泄漏、重复 Bitmap、大内存对象 | 内存指标 |
| IOCanary | 主线程 IO、buffer 过小、重复读写 | 隐蔽卡顿源 |
| SQLiteCanary | 慢 SQL | 启动/卡顿 |
| BatteryCanary | WakeLock、唤醒、耗电 | 耗电指标 |
| ApkChecker | APK 组成、冗余资源(静态分析,进 CI 用) | 包体积 |
3.2 为什么它是"工业级":以 TraceCanary 为例看代差
对比 BlockCanary 的三处进化:
-
采集方式:从"看日志"到"编译期插桩" Matrix 用 ASM 字节码插桩:编译期给每个方法出入口自动注入 method beat 打点,运行时精确计算每个方法耗时并还原调用树。BlockCanary 那种"超时才抓栈、抓到未必准"的误差,被彻底消除。
-
探测点三管齐下:Looper Printer + Choreographer 帧回调 + 消息调度监控,盲区大幅缩小。
-
ANR 多路判定:watchdog 思路(子线程定时探主线程)+ 系统信号(SIGQUIT)监听互相印证,漏报误报都压低。
3.3 线上使用的三大纪律(方法论重点)
-
采样率:全量采集性能开销和数据量都扛不住,常规 1%~10% 用户采样;
-
动态开关 :平时关或低频,线上指标异常时通过配置中心拉全量采集------平时省着,出事猛看;
-
聚合才有意义 :按场景 (哪个页面 jank 率高)、版本 (新版是否劣化)、机型档位分桶。一条孤立数据没有价值,Top 慢函数排行榜才是优化工单。
注意:Matrix 本体只有客户端采集,看板/上报服务端要自建或对接公司 APM(国内商用替代:Bugly、听云;海外:Firebase Performance Monitoring,思路相同)。
3.4 优化闭环(线上版标准动作)
线上大盘发现 P90 劣化(Matrix 采样数据)
→ 拉慢场景全量采集,拿 Top 慢函数清单
→ 本地用 Perfetto / Profiler 复现深挖(前三节的工具接力)
→ 修复发版
→ 盯线上大盘数据收敛,闭环