Android 内存抖动(Memory Churn)详解
一、什么是内存抖动?
内存抖动指在短时间内频繁地创建和销毁对象,导致 GC(垃圾回收)频繁触发,进而引起应用卡顿、掉帧的现象。
核心特征:
-
高频分配:短时间内大量对象被创建
-
快速回收:这些对象很快变成垃圾
-
GC 风暴:系统频繁执行 GC,甚至连续多次 GC
-
卡顿表现:UI 线程被 GC 暂停,出现掉帧(jank)
形象比喻:就像你不断往垃圾桶里扔纸团,保洁阿姨不得不反复来清理,导致你手头的工作不断被打断。
二、内存抖动的危害
| 危害 | 说明 |
|---|---|
| UI 卡顿 | GC 会暂停所有线程(尤其是 Dalvik 时代的 Stop-The-World),导致掉帧 |
| 功耗增加 | 频繁 GC 消耗 CPU 资源,增加电量消耗 |
| OOM 风险 | 极端情况下,对象分配速度超过 GC 回收速度,导致内存溢出 |
| 性能劣化 | 碎片化内存,影响后续大对象分配效率 |
三、常见场景(重点)
1. onDraw() / onMeasure() / onLayout() 中创建对象
这是最常见、最典型的场景。自定义 View 的绘制方法会被频繁调用(每帧 16ms 可能调用多次),如果在里面创建对象,极易引发抖动。
// ❌ 错误示例:onDraw 中创建 Paint 和 Rect
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint(); // 每帧都创建!
Rect rect = new Rect(0, 0, 100, 100); // 每帧都创建!
canvas.drawRect(rect, paint);
}
2. 字符串拼接(尤其是循环中)
// ❌ 错误:循环中大量创建 String 对象
String result = "";
for (int i = 0; i < 1000; i++) {
result += "item" + i; // 每次 += 都创建新的 StringBuilder 和 String
}
// ✅ 正确:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("item").append(i);
}
String result = sb.toString();
3. Adapter 的 getView() / onBindViewHolder() 中创建对象
// ❌ 错误:每次滑动都创建新的 OnClickListener
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.itemView.setOnClickListener(new View.OnClickListener() { // 每次绑定都创建!
@Override
public void onClick(View v) { }
});
}
// ✅ 正确:使用静态内部类或 ViewHolder 缓存
holder.itemView.setOnClickListener(mClickListener);
4. 属性动画 / 值动画的回调中创建对象
// ❌ 错误:动画每帧都创建对象
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.addUpdateListener(animation -> {
float value = (float) animation.getAnimatedValue();
Paint paint = new Paint(); // 每帧创建!
invalidate();
});
5. 传感器 / 定位 / 触摸事件回调中创建对象
// ❌ 错误:高频回调中创建对象
@Override
public void onSensorChanged(SensorEvent event) {
float[] values = new float[3]; // 每次传感器更新都创建
System.arraycopy(event.values, 0, values, 0, 3);
}
6. Bitmap 处理不当
// ❌ 错误:每次显示都 decode 新的 Bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg);
// ✅ 正确:使用 LruCache 缓存,或使用 inBitmap 复用
7. 正则表达式在循环中使用
// ❌ 错误:每次循环都编译正则
for (String text : list) {
boolean matches = text.matches("\\d+"); // 内部每次创建 Pattern 对象
}
// ✅ 正确:预编译 Pattern
Pattern pattern = Pattern.compile("\\d+");
for (String text : list) {
boolean matches = pattern.matcher(text).matches();
}
8. AutoBoxing(自动装箱)
// ❌ 错误:大量 Integer 对象被创建
for (int i = 0; i < 10000; i++) {
Integer value = i; // 自动装箱,创建 Integer 对象
list.add(value);
}
// ✅ 正确:使用基本类型集合(如 SparseArray)避免装箱
SparseArray<String> sparseArray = new SparseArray<>();
四、如何检测内存抖动?
1. Android Studio Memory Profiler
-
打开 Profiler → Memory
-
观察内存曲线是否呈锯齿状(快速上升又快速下降)
-
锯齿越密集、越尖锐,抖动越严重
2. Allocation Tracker(分配追踪器)
Profiler → Memory → 点击 Record(红色圆点)
→ 操作 App → 点击 Stop → 查看分配详情
可以精确看到:
-
哪些类被分配最多
-
分配发生在哪个线程
-
分配的调用栈
3. Systrace / Perfetto
观察是否有大量 GC 事件:
atrace -t 10 -o trace.html gfx input view wm am ss
在 trace 中搜索 GC 标签,如果 GC 频繁出现且持续时间长,说明存在抖动。
4. Logcat 观察 GC 日志
D/dalvikvm: GC_CONCURRENT freed 2048K, 50% free 4096K/8192K, paused 2ms+3ms
如果看到大量 GC_CONCURRENT、GC_FOR_ALLOC、GC_EXPLICIT 频繁打印,且 paused 时间较长,就是抖动信号。
五、解决方案
1. 对象池 / 缓存复用(Object Pool)
对于高频创建的对象,使用对象池复用:
public class PaintPool {
private static final int MAX_SIZE = 5;
private final Queue<Paint> pool = new ArrayDeque<>();
public Paint acquire() {
Paint paint = pool.poll();
return paint != null ? paint : new Paint();
}
public void release(Paint paint) {
if (pool.size() < MAX_SIZE) {
paint.reset();
pool.offer(paint);
}
}
}
Android 系统本身也提供了一些池化机制:
-
Message.obtain()替代new Message() -
TypedArray回收:typedArray.recycle()
2. 避免在频繁调用的方法中创建对象
public class CustomView extends View {
private Paint mPaint; // 提前初始化
private Rect mRect; // 提前初始化
public CustomView(Context context) {
super(context);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mRect = new Rect();
}
@Override
protected void onDraw(Canvas canvas) {
// 只修改属性,不创建新对象
mPaint.setColor(Color.RED);
mRect.set(0, 0, getWidth(), getHeight());
canvas.drawRect(mRect, mPaint);
}
}
3. 使用合适的数据结构
| 场景 | 推荐方案 |
|---|---|
| 键为 int 的 Map | SparseArray / SparseIntArray / LongSparseArray |
| 键为 int 的 Set | SparseBooleanArray |
| 大量基本类型 | ArrayList + 手动管理,或使用 Trove 等第三方库 |
| 避免 HashMap 自动装箱 | 使用 ArrayMap(Android 特有,更省内存) |
4. Bitmap 复用(inBitmap)
BitmapFactory.Options options = new BitmapFactory.Options();
options.inBitmap = reusedBitmap; // 复用已存在的 Bitmap 内存
options.inMutable = true;
Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.img, options);
5. 字符串优化
-
循环拼接用
StringBuilder -
预编译正则
Pattern -
使用
String.format()时避免在循环中调用
6. 避免 AutoBoxing
// ❌
ArrayList<Integer> list = new ArrayList<>();
// ✅
IntArrayList list = new IntArrayList(); // 使用 fastutil 等库
// 或
int[] array = new int[size];
7. 使用 WeakReference 谨慎处理回调
避免匿名内部类持有外部引用导致无法回收,但也不要滥用导致对象被过早回收。
8. 延迟初始化 + 懒加载
不是所有对象都需要立即创建,按需初始化可以减少启动时的内存压力。
六、最佳实践总结
| 原则 | 说明 |
|---|---|
| 不要在 onDraw/onMeasure/onLayout 中 new 对象 | 这是铁律 |
| 高频回调中保持零分配 | 传感器、动画、触摸事件等 |
| 优先使用基本类型 | 避免 AutoBoxing |
| 对象能复用就复用 | Paint、Rect、Path、StringBuilder |
| 使用合适的数据结构 | SparseArray 替代 HashMap<Integer, ?> |
| 定期 Profile 内存 | 养成用 Memory Profiler 检查的习惯 |
| 关注 GC 日志 | Logcat 中 GC 频繁就是警报 |
七、一句话总结
内存抖动的本质 = 高频创建 + 快速废弃 → GC 频繁 → 卡顿。解决之道 = 对象复用 + 避免高频分配 + 选择合适数据结构。