4、定位工具(上):使用Android Studio Profiler(Memory Profiler)初步定位内存抖动
内存抖动这玩意儿,说白了就是短时间内频繁创建和销毁对象。你想想看,GC(垃圾回收)线程被反复唤醒,就像有人每隔几秒就按你家门铃——烦不烦?
我个人习惯,遇到卡顿问题第一件事不是翻代码,而是先打开Profiler看一眼。为什么?因为工具能告诉你「有没有问题」,而不是让你瞎猜。
4.1 打开Memory Profiler的正确姿势
在Android Studio里,点那个小手机图标,或者按 View → Tool Windows → Profiler。然后选择你的设备和进程。
嗯,这里要注意:一定要用Debug包或者可分析包。Release包有时候会关掉一些分析能力,白忙活一场。
进入Profiler后,你会看到CPU、Memory、Network、Energy四个标签。点Memory,进入内存分析视图。
核心视图解读:
- 堆内存曲线:实时显示Java堆使用量。锯齿状就是抖动信号。
- 已分配对象数:每秒新创建的对象数量。数值飙升,说明有问题。
- GC事件标记:小灰条代表一次GC。灰条越密集,抖动越严重。
我在项目中遇到过一位同事,盯着曲线看了半天说「没抖动啊」。我凑过去一看,他忘了勾选「Allocation Tracking」。没有分配追踪,你看到的只是堆大小变化,根本不知道谁在搞鬼。
4.2 如何一眼识别内存抖动
正常的内存曲线,应该是平缓上升然后阶梯式下降——这是正常的GC回收。但抖动呢?
- 锯齿状频繁波动:曲线像心电图,上下跳个不停。
- GC灰条密集出现:每隔一两秒就有一个灰条,甚至连续多个。
- 分配对象数居高不下:每秒几千甚至上万个对象被创建。
我的小技巧:把鼠标悬停在GC灰条上,会显示这次GC的耗时和回收了多少内存。如果每次GC都能回收几百KB甚至几MB,说明你的代码在疯狂造垃圾。
你可能会问:「为什么GC频繁就一定不好?」
因为GC会暂停应用线程(尤其是ART虚拟机下的并发GC,虽然暂停时间短,但频繁触发照样卡顿)。说白了,你的UI线程正在绘制一帧,突然被GC打断——掉帧就这么来的。
4.3 录制一段操作,抓取分配记录
光看曲线不够,你得抓到「谁在造垃圾」。点击「Record」按钮,开始录制内存分配。
操作步骤:
1. 点击 Record 按钮(红色圆点)
2. 在App上复现卡顿场景(比如快速滑动列表)
3. 点击 Stop 按钮
4. 等待分析结果生成
录制完成后,你会看到一个按「Allocations」排序的列表。默认是按对象数量降序排列的。排在最前面的,就是嫌疑最大的。
| 列名 | 含义 | 重点关注 |
|---|---|---|
| Allocations | 该类型对象被分配的总次数 | 数值越大越可疑 |
| Deallocations | 被回收的次数 | 如果接近Allocations,说明对象生命周期极短 |
| Shallow Size | 对象本身占用的内存 | 单个对象大小 |
| Stack Trace | 分配时的调用栈 | 定位到具体代码行 |
我曾经踩过的坑:有一次看到 String 类型分配了十几万次,心想「字符串拼接搞的鬼」。结果点开调用栈一看,是第三方SDK里的日志打印。所以别急着改代码,先确认调用栈是不是你自己的代码。
4.4 实战:从Profiler到代码定位
假设你发现 Bitmap 对象分配了5000次,点开Stack Trace,看到调用栈指向了 ImageView.setImageResource()。
这时候你该怎么做?
- 双击调用栈中的某一行,直接跳转到对应源码。
- 看看是不是在循环里或者频繁回调里加载了图片。
- 检查是否有图片缓存机制(比如Glide的缓存是否生效)。
我习惯的做法是:先看调用栈的顶层(最靠近当前线程的调用),再看底层(入口)。很多时候,抖动发生在 onBindViewHolder 或者 onDraw 这类高频回调里。
一个典型的抖动场景:
// 错误写法:每次绘制都创建新对象
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint(); // 每次绘制都new一个
paint.setColor(Color.RED);
canvas.drawCircle(x, y, radius, paint);
}
// 正确写法:复用对象
private Paint paint = new Paint();
@Override
protected void onDraw(Canvas canvas) {
paint.setColor(Color.RED);
canvas.drawCircle(x, y, radius, paint);
}
你看,就这一行 new Paint() 的移动,就能避免成千上万次对象分配。这就是Profiler告诉你的真相。
4.5 别忘了看「按包名分组」
在Allocation列表上方,有个下拉菜单,默认是「Arrange by class」。你改成「Arrange by package」试试。
这个视图会把所有分配按包名归类。如果你的代码包名下显示了几十万次分配,而系统包名下只有几千次——嗯,问题出在你自己身上。
反过来,如果系统包名(比如 android.graphics)占了大部分,那可能是系统API的调用方式有问题,或者你用了某些不当的写法触发了系统内部的对象创建。
避坑指南:我曾经发现 android.os.Message 分配了上万次。一开始以为是Handler使用不当,后来发现是某个第三方库在循环里 sendEmptyMessage。所以,别急着背锅,先看清楚调用栈归属。
4.6 小结:Profiler能告诉你的三件事
- 有没有抖动:看曲线和GC频率。
- 谁在造垃圾:看Allocation列表和调用栈。
- 问题出在哪:看包名分组和具体代码行。
不过话说回来,Profiler只是第一步。它能告诉你「哪里有问题」,但有时候给不出「为什么这么写会出问题」。下一章我们会聊更精细的定位工具——比如 Allocation Tracker 和 MAT 分析,到时候就能把根因挖个底朝天了。
嗯,先练好Profiler吧。这个工具用熟了,至少能解决80%的内存抖动问题。