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()

这时候你该怎么做?

  1. 双击调用栈中的某一行,直接跳转到对应源码。
  2. 看看是不是在循环里或者频繁回调里加载了图片。
  3. 检查是否有图片缓存机制(比如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%的内存抖动问题。