
列表一滑就顿一下,开发机上却"偶尔才复现"。打开内存图,曲线不断上升又掉下去;CPU 也在跳;产品问是不是"FPS 低"。这三个现象可能来自同一段代码,也可能只是恰好同时出现。只截一张监控图,很难说明问题出在哪里。
一句话结论:先把卡顿锁定到一次可重复的操作和具体时间段,再对齐内存分配、线程运行状态与逐帧耗时;哪个信号先变化,就沿着它的调用栈继续查。
本文以 Android Studio 当前的 Profiler 任务、Android 系统追踪和 ADB 为入口,面向普通 View / Compose App。界面名称可能随 Android Studio 版本调整,文中的指标含义以 Android 官方文档为准;示例场景是教学假设,不是某个项目的实测报告。
1. 先建一条时间线,别先猜"内存泄漏"
做性能排查,第一步不是打开所有工具,而是固定复现条件:设备型号和 Android 版本、屏幕刷新率、构建类型、测试数据、网络状态、起始页面,以及操作步骤。比如"进入商品列表,连续向下滑动 15 秒,再返回首页"就比"列表有点卡"可复现得多。先用低开销的构建和 System Trace 记录这段操作;需要找具体对象时,再切到会增加开销的分配记录。Android 性能测量概览、Android Studio 性能分析
text
用户操作 → 出现卡顿的帧 → 当时主线程 / RenderThread 在做什么
↘ 同时是否大量分配、发生 GC
↘ 哪个线程占用 CPU、是在运行还是等待
三个指标的责任边界可以先记成这张表:
| 看到的信号 | 它回答什么 | 不能直接推出什么 | 先用哪个工具 |
|---|---|---|---|
| 内存曲线、分配、GC | 对象是否频繁产生或长期存活 | 曲线像锯齿不等于内存泄漏 | Memory Profiler 的 Java/Kotlin Allocations |
| CPU 占用与线程状态 | 哪些线程在运行,哪些调用栈最忙 | CPU 高不等于一定掉帧;CPU 低也不等于主线程没被阻塞 | System Trace,再做调用栈采样 |
| 逐帧耗时、卡顿帧 | 某一帧是否错过显示期限 | 平均 FPS 高不等于完全流畅 | System Trace 的 Janky frames / FrameTimeline |
我的顺序是先找卡顿帧,再找当时的工作。如果根本没有掉帧,只是电量消耗或后台发热,就不必强行从 FPS 入手。
2. 内存抖动怎么看:盯"分配速率",不是只盯占用总量
内存抖动通常指短时间创建很多临时对象,内存曲线反复上升,随后被垃圾回收(GC)拉低。它和泄漏不一样:抖动的对象会被回收;泄漏更像是页面退出后,回收后的基线仍持续抬高 。但一张锯齿图只能提出怀疑,不能证明每次卡顿都是 GC 导致。Java/Kotlin 分配记录、Heap Dump 指南
用可调试构建在 Android Studio 打开 Profiler → Track Memory Consumption (Java/Kotlin Allocations),让 App 完成一次固定操作后停止记录。先在时间轴选中异常的那几秒,再看:
Allocated对象数是否突然密集上升,旁边是否出现 GC 事件。- 哪些类的分配次数或总字节数最高,而不是只看单个最大对象。
- 分配调用栈落在哪个线程、哪段业务代码;同一行代码是否在每一帧或每个列表 item 中重复执行。
完整分配记录本身可能让 App 变慢。若分配量很大,先用 Sampled 模式找热点;找到嫌疑调用栈后,再缩短时间窗口精查。Java/Kotlin 分配视图不覆盖 C/C++ 的 malloc,图像或 native 相关内存还要结合 Native Allocations、进程 RSS 与 dumpsys meminfo 看。Java/Kotlin 分配记录、Native Allocations
想快速看一次内存快照,可用:
bash
adb shell dumpsys meminfo com.example.app
把 com.example.app 换成实际包名。这个命令适合比较进入页面前、操作后、退出页面后的内存分类;它不是连续分配记录。若退出页面并触发回收后,某类 Activity、View、Bitmap 仍被持有,再抓 Heap Dump 看引用链与 retained size。不要只因为 Java Heap 总量没立刻下降,就断定泄漏;内存管理器可能保留已申请的堆空间供后续复用。dumpsys 官方文档、Heap Dump 指南
3. CPU 占用怎么看:先分线程,再找热点
Android Studio 的 Profiler → System Trace 会把应用 CPU 使用、各线程状态、CPU 核心和显示帧放到同一时间轴。这里的 CPU 图按应用占设备总可用 CPU 容量的比例显示。先框选问题区间:是 main 线程长时间运行,还是某个图片解码、JSON 解析、数据库或自建线程池忙?如果主线程在等待锁或 Binder,进程 CPU 未必很高,页面仍可能卡住。System Trace 指南、实时检查应用
找到忙线程后,再用 Find CPU Hotspots (Callstack Sample) 抓调用栈,按 Bottom Up / Flame Chart 找真正花 CPU 的方法及其调用者。该采样任务要求 Android 8.0(API 26)及以上。采样适合定位长时间热点;如果需要精确看某段 Java/Kotlin 方法的调用次数与时长,可以短时间录制方法跟踪,但官方提醒插桩本身有明显开销,建议把记录限制在约 5 秒以内。不要拿插桩期间的 FPS 作为优化前后结果。调用栈采样、Java/Kotlin 方法记录
这里不要给 CPU 百分比设一个脱离场景的"合格线"。视频编码时短暂高占用可能正常;页面停住后仍有线程持续忙,才需要追循环、定时任务或未取消的工作。不同工具对多核 CPU 的百分比分母也可能不同,比较时要保持同一工具、同一设备和同一操作。
4. FPS 怎么看:看哪一帧迟到了
FPS 是单位时间显示的帧数,真正影响手感的往往是相邻帧间隔和迟到帧 。目标为 60Hz 时,每帧预算约 16.7ms;90Hz 约 11.1ms;120Hz 约 8.3ms。这是预算的简化写法,实际卡顿判断还受系统帧期限和显示管线影响。开发者选项里的"显示刷新率"显示的是屏幕当前刷新模式,不是你的 App 实际 FPS 。性能测量概览、卡顿帧检测
在 Android 12(API 31)及以上设备上,录一段 System Trace ,打开 Display 下的 Janky frames ,点中红色超时区,再看主线程、RenderThread 和 GPU completion 在那一帧附近做了什么。需要更细的系统视角时,可把 trace 导入 Perfetto 看 FrameTimeline;Android 11 及更早版本在 Profiler 中显示的帧轨道形式不同,按对应版本的 Display / Frame Lifecycle 查看。Android Studio 卡顿帧检测
ADB 也能快速取最近的帧统计:
bash
adb shell dumpsys gfxinfo com.example.app framestats
它给的是帧时间信息,不应把输出中的某一个数字直接叫作"实时 FPS"。要比较优化前后,固定同一段操作,优先看卡顿帧、P95/P99 帧耗时和超时程度;可重复的滚动场景可用 Macrobenchmark 的 FrameTimingMetric,其中 frameOverrunMs > 0 表示该帧错过期限,该指标要求 Android 12 及以上。dumpsys gfxinfo、FrameTimingMetric
5. 三个场景,沿着证据往下查
场景 A:滑列表时内存呈锯齿,偶尔掉帧
先把列表滚动录进 System Trace,找出掉帧时间点。如果那一刻附近有 GC,再录短时间 Java/Kotlin Allocations:假设分配调用栈反复落到 onBindViewHolder() 里的字符串拼接、日期格式化或 Bitmap 创建,就有了具体嫌疑。修改时优先减少每次绑定都会发生的分配,例如缓存已格式化文案、避免不必要的中间集合、让图片库按目标尺寸解码。最后重新测相同滚动动作,检查分配次数、GC 密度和超时帧是否一起下降。仅凭锯齿图去调大堆上限,不能消除分配源头。
场景 B:离开页面后 CPU 仍高,但 FPS 看起来正常
这时不要被"画面流畅"误导。框选页面离开后的 System Trace,找到持续运行的线程,再做调用栈采样。若热点落在持续轮询、未取消的 Flow 收集、定时器或解码任务,就检查它的生命周期归属和停止条件。修复后应验证:离开页面后该线程回到空闲,CPU 曲线下降;重新进入页面后功能仍正常。这是后台做了多余工作的问题,和当前页面是否掉帧是两个判断。
场景 C:帧不断迟到,但 App CPU 并不高
先点中一帧异常帧,看主线程是在运行、等待 I/O / 锁,还是 RenderThread、GPU 阶段拖长。若主线程等待锁,把大计算"再挪到后台线程"可能无效;若主要耗时在绘制和 GPU,检查大图尺寸、过度绘制、复杂阴影和重复图层。只有在 trace 指向该阶段时才改对应代码,改完再测同一段帧时间。CPU 低并不能证明渲染链没问题。Android Studio 卡顿帧检测
这三个场景的共同点是:先用时间重合找到嫌疑,再用调用栈或帧轨道确认因果。内存峰值、CPU 峰值和红色卡顿帧同时出现,仍可能是同一操作造成的三个结果;不要只凭曲线重合就把 GC 定为元凶。
6. 把"修好了"变成可复查的结论
一次完整的记录应至少留下:复现步骤、设备与刷新率、构建类型、优化前后的同场景 trace、异常帧位置,以及对应的分配或调用栈证据。快速查问题时可以用 debuggable 构建获得完整数据;评估真实改善时用低开销、接近发布配置的构建复测,因为 Profiler 的完整分配记录和方法插桩会扰动结果。Android Studio 性能分析、Java/Kotlin 分配记录
我通常用一句话写结论:"在某设备的某个操作中,哪一帧迟到了;对应时间段哪个线程或分配调用栈做了什么;改动后同场景的卡顿帧和分配/CPU 指标怎样变化。"能写出这句话,才算从"看到了内存、CPU、FPS 曲线"走到了真正的性能排查。