性能工具专题
性能优化不是 “凭感觉改几行代码”, 而是先观测, 再定位, 再验证. 工具章节的面试重点是: 每个工具看什么, 适合什么问题, 输出如何转化为工程动作. 方法论, 指标口径与跨域取舍见 性能优化总览.
一, 工具选型总览
| 问题类型 | 首选工具 | 看到什么 | 常见动作 |
|---|---|---|---|
| 启动慢 | Android Studio Profiler / Perfetto / Macrobenchmark | CPU, 主线程, 启动阶段耗时 | 延迟初始化, 异步化, Baseline Profile |
| 卡顿掉帧 | Perfetto / Systrace / gfxinfo | Choreographer, RenderThread, 帧耗时 | 减少主线程阻塞, 优化布局/绘制 |
| 内存泄漏 | LeakCanary / Memory Profiler / meminfo | 引用链, 堆大小, PSS | 解除生命周期引用, 缓存上限 |
| I/O/网络违规 | StrictMode / Trace sections | 主线程磁盘/网络, 慢调用 | 切线程, 缓存, 预加载 |
| Native 热点 | simpleperf / Perfetto | CPU sample, 调用栈, 符号 | 优化算法, 减少 JNI 往返 |
| 线上复现困难 | dumpsys / bugreport / 自定义 trace | 系统状态快照 | 关联日志, 设备维度排查 |
工具回答要避免 “列名词”, 而要说清楚 “我怀疑什么 → 用什么采样 → 看哪个指标 → 怎么验证改动”.
二, Android Studio Profiler
Android Studio Profiler 适合本地开发阶段快速观察 CPU, Memory, Network, Energy.
- CPU Profiler: 看主线程是否有长任务, 方法调用耗时, 线程调度. 可用 Java/Kotlin Method Trace (分 Sampled 与 Instrumented 两种模式), 采样开销更低, 插桩更细但扰动更大.
- Memory Profiler: 看 Java/Kotlin 堆, 对象分配, GC 频率, Heap Dump. 适合发现短时间内对象抖动和明显泄漏.
- Network Profiler: 看请求时序, 流量大小, 频率, 但复杂线上网络问题仍要配合 OkHttp event listener, 服务端日志和抓包合规流程.
- Energy Profiler: 关注 wakelock, 定位, 网络等耗电行为.
局限: Profiler 连接调试进程会带来观测扰动, 结论要用 release-like 包, Macrobenchmark 或 Perfetto 再验证.
三, Perfetto, Systrace 与 Trace sections
Perfetto 是现代 Android 系统级 tracing 工具, 可观察 CPU 调度, Binder, 线程状态, Choreographer, SurfaceFlinger, RenderThread, I/O 等. Systrace 是旧版工具, 很多概念仍沿用, 但新项目优先用 Perfetto.
import androidx.tracing.Trace
fun bindFeed(items: List<Item>) {
Trace.beginSection("Feed.bind")
try {
adapter.submitList(items)
} finally {
Trace.endSection()
}
}
Trace sections 的价值是把业务阶段写进系统 trace, 让你在 Perfetto 里看到 “哪段业务代码” 对应主线程长任务. 命名要稳定, 短, 能定位模块, 避免把用户敏感信息写入 trace 名称.
看 Perfetto 的常见路径:
- 先看主线程是否 Running 太久或频繁 Blocked.
- 看耗时段是否跨过 VSync 导致 missed frame.
- 看 RenderThread/GPU 是否被复杂绘制或纹理上传拖慢.
- 看 Binder, I/O, 锁等待是否阻塞主线程.
- 结合自定义 Trace sections 还原业务阶段.
四, Macrobenchmark 与 Baseline Profile
Macrobenchmark 用独立测试 APK 驱动目标 App, 在接近真实环境下测启动, 滚动, 页面跳转等宏观性能. 它适合做性能回归门禁, 比普通单元测试更贴近用户体验.
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
iterations = 10,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}
Baseline Profile 是把关键路径的类和方法提前提供给 ART, 安装后可更早编译热点代码, 改善冷启动和首帧性能. 典型流程是用 profileinstaller + androidx.baselineprofile Gradle 插件生成 / 合并 profile, 再用 Macrobenchmark 验证收益.
面试要点:
- Macrobenchmark 关注稳定设备, 固定版本, 足够迭代次数和噪声控制.
- Baseline Profile 不是万能优化, 主要改善解释执行 / JIT 预热带来的启动和关键路径抖动.
- 性能数据要保存历史趋势, 否则无法判断回归.
五, LeakCanary 与内存定位
LeakCanary 自动观察 Activity, Fragment, ViewModel 等对象生命周期, 对象应被回收却仍被引用时触发 heap dump 并给出泄漏引用链.
常见泄漏原因:
- 静态单例持有 Activity/Context.
- Handler/Runnable/Coroutine 未随生命周期取消.
- Adapter, Listener, Callback 未解绑.
- Dialog/PopupWindow/Animator 持有 View.
- 全局缓存无上限或 key/value 持有页面对象.
Memory Profiler 更适合看对象分配和堆变化, LeakCanary 更适合自动发现泄漏. 线上内存问题则要结合 OOM 日志, dumpsys meminfo, 业务埋点和设备分布分析.
六, StrictMode 与开发期红线
StrictMode 用来在开发/测试阶段发现主线程磁盘 I/O, 网络, 资源未关闭, Activity 泄漏等问题.
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
它不是线上性能监控方案, 而是把 “本不该发生的开发期违规” 尽早暴露. 不要为了消除告警简单 permitAll, 应该定位调用链并把 I/O 移到合适线程或启动阶段之外.
七, dumpsys, gfxinfo, meminfo
dumpsys 是 Android 系统服务状态入口, 适合连接真机后快速拿到系统级快照.
| 命令 | 关注点 | 用法场景 |
|---|---|---|
adb shell dumpsys gfxinfo <pkg> | 帧耗时, Janky frames, 渲染统计 | 页面滑动 / 动画掉帧 |
adb shell dumpsys meminfo <pkg> | PSS, Java/Native/Graphics 内存 | OOM, 内存增长 |
adb shell dumpsys activity <pkg> | Activity/Service/进程状态 | 生命周期, 进程保活排查 |
adb shell dumpsys batterystats | 耗电归因 | 后台任务, 网络, 唤醒 |
gfxinfo framestats 可导出每帧各阶段时间, 适合脚本化对比优化前后. meminfo 的 PSS 更接近进程实际占用视角, 但仍要结合 Android 版本, 厂商和图形内存差异理解.
gfxinfo 示意输出, 非真实测量:
Stats since: 123456789ns
Total frames rendered: 240
Janky frames: 18 (7.50%)
50th percentile: 10ms
90th percentile: 21ms
95th percentile: 28ms
99th percentile: 44ms
Number Missed Vsync: 12
Total frames rendered 是统计窗口内的渲染帧数, Janky frames 和百分比是工具口径下的提示, 分位数展示长尾, Number Missed Vsync 表示错过 VSync 的计数. 先确认采样窗口, 刷新率, 设备和场景, 再与同条件基线比较; 这些字段不能单独证明根因, 仍需用 Frame Timeline/Perfetto 区分 UI, RenderThread, GPU 或调度问题.
八, simpleperf 与 Native CPU 热点
simpleperf 是 Android 官方 native profiling 工具, 可采样 CPU 指令, 调用栈和符号, 适合 NDK/JNI, 音视频, 加解密, 图像处理等热点定位.
典型关注:
- native 函数是否占用异常 CPU.
- JNI 往返是否过多.
- 锁竞争, 内存拷贝, 算法复杂度是否是瓶颈.
- so 是否保留符号或能通过符号表 / 映射文件还原调用栈.
面试表达可以说: Java/Kotlin 层先用 Perfetto/Profiler 定位到 native 调用段, 再用 simpleperf 下钻 native 热点, 最后用基准测试和真实场景回归验证.
九, 电量优化的证据链
电量问题要先说明耗电来源, 再在相同条件下测量. 不应根据一次使用感受或工具截图宣称节电百分比.
| 耗电来源 | 常见触发条件 | 优先核查 | 可能的工程动作 |
|---|---|---|---|
| Wakelock | 后台任务未结束, 播放或下载未正确收尾 | batterystats 中的持锁时间, 任务生命周期 | 缩短持锁范围, 用系统调度约束任务, 确保取消与完成路径释放资源 |
| 网络无线电 | 高频小请求, 失败重试, 前后台切换 | 请求批次, 网络类型, 传输字节, 重试次数 | 合并可延迟请求, 使用缓存和退避, 避免非必要轮询 |
| 定位 | 高精度持续订阅, 未随页面/任务停止 | 精度, 间隔, 前后台状态, 定位回调次数 | 按业务精度分级, 设置时长/距离条件, 任务结束后移除更新 |
| 后台任务 | 无约束的定时任务, 重复调度 | WorkManager/Job 约束, 执行次数, 失败链路 | 使用网络, 充电和空闲约束, 合并唯一工作, 设置合理退避 |
| 渲染负载 | 长时间动画, 高频重组 / 绘制, 屏幕常亮 | Frame Timeline, RenderThread, GPU 和刷新率 | 减少无意义重绘, 在不可见时暂停动画, 降低刷新频率或质量 |
测量步骤与记录表
- 写明唯一假设, 例如 “后台同步的网络批次会减少无线电激活次数”; 不把多个改动混入同一次对比.
- 固定或记录设备型号, 电池健康状态, Android 版本, App 构建版本, 屏幕亮度, 网络类型, 账号数据量和测试时长.
- 准备优化前后两个构建, 使用相同起始电量与业务脚本, 至少保留空闲或未执行业务的对照组.
- 用 Battery Historian 解析
batterystats, 用 Perfetto 观察调度, wakelock 和网络相关轨道, 或使用系统电量报告交叉核对. 工具字段和可见数据会随系统版本变化. - 记录原始报告路径或可复现命令, 比较任务次数, 持锁时长, 网络字节, 定位回调和帧时间等中间证据; 再判断是否需要扩大样本或回滚改动.
| 字段 | 本次记录 |
|---|---|
| 假设与改动版本 | [填写可复核的假设和 commit/build 标识] |
| 设备与系统 | [型号, Android 版本, 电池状态] |
| 网络, 屏幕与数据条件 | [Wi-Fi/蜂窝, 亮度, 账号/数据集] |
| 测试时长与重复次数 | [填写] |
| 对照组与工具报告 | [填写报告位置或命令] |
| 观察到的中间证据 | [wakelock/网络/定位/帧时间等] |
| 结论与限制 | [只填写可由上述条件复核的结论] |
该表是读者练习和实验记录模板, 其中字段不是本项目实测数据. 电量受设备温度, 基带状态, 电池老化和系统调度影响; 单次测量只能形成假设证据, 不应外推为普遍指标.
命令, 输出与 Baseline Profile 操作
以下命令要求已启用 adb 调试, 目标设备允许 profile/trace, 且应在 release-like, 可复现场景中执行; 工具和权限限制随 Android 版本, user/userdebug 构建变化. 命令输出字段仅作解读示例, 不是本项目实测结果.
adb shell perfetto -o /data/misc/perfetto-traces/app.perfetto-trace -t 10s sched freq binder_driver gfx view
adb pull /data/misc/perfetto-traces/app.perfetto-trace .
adb shell dumpsys gfxinfo com.example.app framestats
adb shell simpleperf record -p <pid> -g --duration 10 -o /data/local/tmp/perf.data
adb pull /data/local/tmp/perf.data ./perf.data
simpleperf report -i ./perf.data
Perfetto 要先选择与假设对应的 data source; 打开 trace 后看 main/RenderThread 的 slice, sched 状态和 Binder/I/O 是否跨过 frame deadline. gfxinfo framestats 的每行代表一帧时间戳, Janky frames 是统计提示, 不能脱离刷新率和采样窗口判断根因. simpleperf 的顺序是: 设备端 record 写入 /data/local/tmp/perf.data, adb pull 生成其主机端副本 ./perf.data, 主机端 simpleperf report -i ./perf.data 读取该副本. 它们是同一采样数据文件的设备端与主机端副本, 并非 report 直接读取设备路径. report 按 sample 占比列函数; 先确认符号可用和调用栈, 再检查热点是否在目标场景稳定出现. 设备必须允许目标进程被 profile, user 构建, SELinux, profileable/debuggable 状态可能限制采样; 应按权限错误调整到获准的构建, 不能伪造结果.
Baseline Profile 配置片段 (需 androidx.baselineprofile 插件和独立 benchmark/profile module; 版本按项目 Gradle 统一管理):
baselineProfile {
filter { include("com.example.app") }
}
Profile 生成场景应覆盖冷启动和关键交互, 生成后随 APK/AAB 打包; 用 Macrobenchmark 在固定设备, 构建和迭代数下对比. 若关键路径变动, 重生成 profile; 不把 Profile 当作替代主线程, 算法或 I/O 优化的手段.
问题, 工具, 证据与限制
选工具先从问题出发: 启动/帧用 Macrobenchmark 与 Perfetto, Java heap 用 heap dump, native allocation 用 heapprofd, 线上退出原因用平台 API/vitals. 每次记录设备, OS, 构建类型, profileable/debuggable, trace 配置和噪声. Profiler UI 与命令可能随 Android Studio/Perfetto 版本变化; 操作步骤应带版本, 单次 trace 只支持一个假设, 不直接证明因果.
高频面试题
Q1: Profiler, Perfetto, Systrace 怎么选? Profiler 适合开发期快速看单进程 CPU/内存/网络; Perfetto 适合系统级时序分析, 能看调度, 渲染, Binder, I/O; Systrace 是旧链路但概念相通. 复杂卡顿优先 Perfetto, 内存泄漏优先 LeakCanary/Memory Profiler.
Q2: Trace.beginSection 有什么价值? 它把业务阶段标记进系统 trace, 让 Perfetto 里能把主线程长任务和具体模块对应起来. 注意 section 名称要稳定, 简洁, 无敏感信息, 并保证 begin/end 成对.
Q3: Macrobenchmark 和普通 benchmark 区别? Macrobenchmark 从 App 外部驱动真实启动, 滚动, 跳转等宏观场景, 更贴近用户体验; 普通 microbenchmark 更适合测小函数 / 算法. 性能门禁通常用 Macrobenchmark 看趋势和回归.
Q4: Baseline Profile 解决什么问题? 它把启动和关键路径热点提前提供给 ART 编译, 减少冷启动 / 首帧阶段解释执行和 JIT 预热成本. 它不是替代业务优化, 仍要用 Macrobenchmark 验证收益.
Q5: LeakCanary 报出泄漏后怎么处理? 先看泄漏对象和引用链, 判断是否生命周期结束后仍被持有; 再定位静态引用, 回调, 协程, Handler, Adapter 等持有者; 修复后重新进入 / 退出页面并确认泄漏不再出现.
易错点 / 追问
- 不要只贴工具截图, 要能说明指标含义和下一步工程动作.
- 不要在 debug, 插桩, 连接 Profiler 的环境下直接宣称线上性能收益.
- Perfetto 看到主线程长任务后, 要继续区分 CPU 忙, 锁等待, I/O, Binder 等原因.
- Baseline Profile 需要随关键路径变化更新, 否则 profile 会逐渐失效.
- StrictMode 告警不能靠关闭规则解决, 应修正线程和资源使用.