12、内存抖动与卡顿:内存抖动的原因,Allocation Tracker使用,如何避免频繁GC

内存抖动,这个词你肯定听过。说白了,就是应用在短时间内频繁地创建和销毁对象,导致垃圾回收器(GC)被反复触发。嗯,这就像你不停地往垃圾桶里扔东西,又不停地清空,系统自然就卡住了。

我在项目中遇到过好几次这样的问题。有一次,一个列表滑动卡顿得让人抓狂,用工具一查,好家伙,每秒钟触发了十几次GC。这就是典型的内存抖动。

12.1 内存抖动是怎么发生的?

先看一个最简单的例子:

// 这段代码看着没问题,对吧?
for (int i = 0; i < 1000; i++) {
    String temp = "item_" + i;
    // 处理temp...
}

你想想看,每次循环都创建一个新的String对象。如果这个循环在UI线程里频繁执行,比如在onDraw()或者getView()里,那GC就会被反复唤醒。

为什么会这样?因为Dalvik和ART的GC机制有个特点:当分配的对象达到一定阈值时,就会触发GC。如果对象分配速度太快,GC还没来得及回收完,新的分配又来了,那就形成了恶性循环。

核心问题:内存抖动的本质是「分配速度 > 回收速度」。

12.2 Allocation Tracker:揪出元凶

我个人习惯用Android Studio自带的Allocation Tracker来定位问题。它就像个监控摄像头,能记录下每个对象是在哪行代码创建的。

使用步骤很简单:

  1. 打开Android Studio,连接设备或模拟器
  2. 点击Profiler,选择Memory标签
  3. 点击「Record allocation」开始记录
  4. 操作你的App,复现卡顿场景
  5. 点击「Stop recording」停止

记录完成后,你会看到类似这样的数据:

Allocations: 12,345
Total Size: 8.2 MB
Top Allocated Classes:
    byte[] - 3,200 allocations
    String - 2,800 allocations
    int[] - 1,500 allocations
    ...

这里要注意,byte[]和String往往是内存抖动的头号嫌疑犯。我曾经排查过一个图片加载库的问题,发现它每次解码图片都创建了新的byte[],导致GC频繁触发。

小技巧:在Allocation Tracker里,可以按「Allocations」排序,看看哪些类被创建得最多。然后双击进去,就能看到具体的调用栈。

12.3 如何避免频繁GC?

避免内存抖动,说白了就是减少对象的创建。我总结了几个实战经验:

12.3.1 复用对象,而不是新建

比如在ListView或RecyclerView的getView()里,一定要复用convertView:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.item, parent, false);
        holder = new ViewHolder(convertView);
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder) convertView.getTag();
    }
    // 更新数据...
    return convertView;
}

嗯,这个大家都懂。但有时候我们会忽略一些细节,比如在onBindViewHolder里创建了新的对象:

// 错误示范
void onBindViewHolder(ViewHolder holder, int position) {
    String text = "item_" + position;  // 每次绑定都创建新String
    holder.textView.setText(text);
}

// 正确做法
void onBindViewHolder(ViewHolder holder, int position) {
    holder.textView.setText("item_" + position);  // setText内部会处理
}

12.3.2 使用对象池

对于频繁创建和销毁的对象,比如Message、Handler里的Runnable,可以用对象池来复用:

public class MessagePool {
    private static final int MAX_POOL_SIZE = 50;
    private final Queue<Message> pool = new LinkedList<>();

    public Message obtain() {
        Message msg = pool.poll();
        if (msg == null) {
            msg = new Message();
        }
        return msg;
    }

    public void recycle(Message msg) {
        if (pool.size() < MAX_POOL_SIZE) {
            pool.offer(msg);
        }
    }
}

Android系统本身就有Message.obtain(),我建议你直接用系统的,别自己再造轮子。

12.3.3 避免在循环中创建对象

这个坑我踩过好几次。比如在onDraw()里创建Paint对象:

// 错误示范
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint paint = new Paint();  // 每次绘制都创建新对象
    paint.setColor(Color.RED);
    canvas.drawCircle(100, 100, 50, paint);
}

// 正确做法
private Paint paint = new Paint();  // 成员变量,只创建一次

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    paint.setColor(Color.RED);
    canvas.drawCircle(100, 100, 50, paint);
}

警告:onDraw()方法每秒可能被调用60次,在这里创建对象简直就是灾难。我曾经见过一个自定义View,在onDraw里创建了Bitmap,结果GC频率直接飙到每秒20次。

12.3.4 使用基本类型代替包装类型

int比Integer好,long比Long好。包装类型会创建对象,基本类型不会:

// 错误示范
Integer sum = 0;
for (int i = 0; i < 1000; i++) {
    sum += i;  // 每次都会创建新的Integer对象
}

// 正确做法
int sum = 0;
for (int i = 0; i < 1000; i++) {
    sum += i;  // 没有对象创建
}

12.4 内存抖动的检测指标

怎么判断你的App有没有内存抖动?我一般看这几个指标:

指标 正常范围 抖动预警
GC频率 < 5次/分钟 > 10次/分钟
单次GC耗时 < 10ms > 20ms
内存分配速率 < 1MB/s > 5MB/s
卡顿率 < 0.5% > 2%

这些数据在Android Studio的Profiler里都能看到。我个人习惯在测试时打开「Allocation Tracking」,跑一遍核心流程,看看有没有异常。

12.5 核心知识体系

下面这张图总结了内存抖动的核心逻辑:

内存抖动核心逻辑 频繁创建对象 对象未复用 循环中new对象 内存分配速度 > 回收速度 频繁GC → UI卡顿 → ANR风险 复用对象 对象池 避免循环创建

从这张图可以看出,内存抖动的根源就是对象创建太频繁。解决思路也很清晰:复用、池化、避免循环创建。

我的建议:在开发阶段就养成好习惯。每次写循环、写onDraw、写getView时,多问自己一句:「这里会不会创建新对象?」久而久之,内存抖动的问题自然就少了。

好了,关于内存抖动就聊这么多。记住,GC不是洪水猛兽,但频繁的GC绝对是性能杀手。用好Allocation Tracker,养成好的编码习惯,你的App就能跑得更流畅。

公众号:蓝海资料掘金营,微信deep3321