andorid 内存优化-内存抖动

在 Android 开发中,内存抖动(Memory Churn) 是指应用频繁创建和销毁小对象,导致内存频繁分配和回收,进而引发性能问题。以下是常见的问题场景及解决方案:

1. 循环中创建对象

问题表现:在for/while循环或频繁调用的方法中持续创建新对象,导致短时间内产生大量临时对象。

 

典型场景:

 

  • onDraw()中创建PaintRect等对象。
  • AdaptergetView()/onBindViewHolder()中频繁创建数据对象。

 

示例代码:

 

java
 
 
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // 错误:每次绘制都创建新对象
    Paint paint = new Paint();
    Rect rect = new Rect(0, 0, 100, 100);
    canvas.drawRect(rect, paint);
}
 

 

解决办法:

 

  • 对象复用:在循环外创建对象并复用(如成员变量或静态变量)。
  • 使用对象池:通过SparseArray或自定义池管理常用对象。

 

java
 
 
private Paint paint = new Paint(); // 成员变量复用
private Rect rect = new Rect();

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    rect.set(0, 0, 100, 100);
    canvas.drawRect(rect, paint);
}
 

2. 字符串拼接(String.concat 或 + 操作符)

问题表现:在循环或频繁调用的方法中使用+拼接字符串,会生成大量临时String对象。

 

示例代码:

 

java
 
 
// 错误:每次循环创建新String对象
String result = "";
for (int i = 0; i < 1000; i++) {
    result += "item" + i; // 等价于 result = result.concat("item" + i);
}
 

 

解决办法:

 

  • 使用StringBuilder:在循环中替代String拼接。

 

java
 
 
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append("item").append(i); // 减少临时对象
}
String result = sb.toString();
 

3. 自动装箱(Autoboxing)

问题表现:基本数据类型(如intlong)与包装类(如IntegerLong)之间的自动转换,会频繁创建包装对象。

 

示例代码:

 

java
 
 
// 错误:每次运算创建新Integer对象
Integer sum = 0; // 初始化为包装类
for (int i = 0; i < 1000; i++) {
    sum += i; // 等价于 sum = Integer.valueOf(sum.intValue() + i);
}
 

 

解决办法:

 

  • 优先使用基本数据类型:避免不必要的装箱操作。

 

java
 
 
int sum = 0; // 使用基本类型
for (int i = 0; i < 1000; i++) {
    sum += i; // 无装箱操作
}
 

4. 动画与帧刷新

问题表现:在ValueAnimatorObjectAnimator或自定义动画中,每帧都可能创建新对象。

 

典型场景:

 

  • 动画更新时频繁创建PointFPath等对象。
  • AnimatorUpdateListener中进行复杂计算并创建临时对象。

 

示例代码:

 

java
 
 
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        // 错误:每帧创建新对象
        PointF point = new PointF(100 * animation.getAnimatedFraction(), 200);
        // 使用point更新UI
    }
});
 

 

解决办法:

 

  • 复用对象:在监听器外部创建对象并更新其属性。

 

java
 
 
private PointF point = new PointF(); // 成员变量复用

animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        point.set(100 * animation.getAnimatedFraction(), 200); // 更新现有对象
        // 使用point更新UI
    }
});
 

5. 频繁调用创建新对象的方法

问题表现:某些方法内部会创建新对象,若被频繁调用(如onTouchEvent()onScroll()),会导致内存抖动。

 

典型场景:

 

  • BitmapFactory.decodeResource()每次调用都创建新Bitmap
  • Arrays.asList()返回新的ArrayList对象。

 

解决办法:

 

  • 缓存结果:避免重复调用相同方法创建对象。

 

java
 
 
// 错误:每次点击都解析Bitmap
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.image);
        imageView.setImageBitmap(bmp);
    }
});

// 正确:提前解析并缓存Bitmap
private Bitmap cachedBitmap;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    cachedBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);
    button.setOnClickListener(v -> imageView.setImageBitmap(cachedBitmap));
}
 

6. 内部类隐式持有外部类引用

问题表现:非静态内部类(如匿名类)隐式持有外部类的引用,若频繁创建会导致外部类无法被回收。

 

典型场景:

 

  • HandlerRunnableAsyncTask中使用匿名内部类。

 

示例代码:

 

java
 
 
// 错误:匿名Runnable持有Activity引用
new Thread(new Runnable() {
    @Override
    public void run() {
        // 长时间运行的任务
    }
}).start();
 

 

解决办法:

 

  • 使用静态内部类 + 弱引用:避免外部类被意外持有。

 

java
 
 
private static class MyRunnable implements Runnable {
    private final WeakReference<MainActivity> activityRef;

    public MyRunnable(MainActivity activity) {
        this.activityRef = new WeakReference<>(activity);
    }

    @Override
    public void run() {
        MainActivity activity = activityRef.get();
        if (activity != null) {
            // 使用activity
        }
    }
}
 

7. 集合频繁扩容

问题表现:ArrayListHashMap等集合在容量不足时会自动扩容并复制数据,导致内存抖动。

 

示例代码:

 

java
 
 
// 错误:未指定初始容量,频繁扩容
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    list.add("item" + i); // 多次触发扩容
}
 

 

解决办法:

 

  • 预分配容量:根据数据量预估初始大小。

 

java
 
 
ArrayList<String> list = new ArrayList<>(1000); // 初始容量足够大
for (int i = 0; i < 1000; i++) {
    list.add("item" + i); // 避免扩容
}
 

8. 资源未及时释放

问题表现:BitmapCursorInputStream等资源使用后未及时释放,导致内存占用过高,触发频繁 GC。

 

解决办法:

 

  • 使用try-with-resources:自动关闭资源。
  • onDestroy()中释放资源:如回收Bitmap

 

java
 
 
// 正确:使用try-with-resources关闭流
try (InputStream is = getResources().openRawResource(R.raw.file)) {
    // 读取数据
} catch (IOException e) {
    e.printStackTrace();
}
 

9. 自定义 View 绘制优化不足

问题表现:在onDraw()中进行复杂计算或创建大量对象,导致每帧绘制时内存波动。

 

解决办法:

 

  • 减少onDraw()中的对象创建:复用PaintPath等对象。
  • 避免复杂计算:将不变的计算移至onSizeChanged()等方法。

 

java
 
 
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // 一次性计算,避免在onDraw()中重复计算
    calculatePath(); 
}
 

10. 第三方库使用不当

问题表现:某些库的 API 设计可能导致频繁创建对象(如网络请求库、图片加载库)。

 

典型场景:

 

  • 每次图片加载都创建新的RequestOptions
  • 网络请求频繁解析 JSON 生成新对象。

 

解决办法:

 

  • 复用配置对象:如 Glide 的RequestOptions可静态化。

 

java
 
 
private static final RequestOptions OPTIONS = new RequestOptions()
        .centerCrop()
        .placeholder(R.drawable.placeholder);

// 使用静态OPTIONS,避免重复创建
Glide.with(context)
     .load(url)
     .apply(OPTIONS)
     .into(imageView);
 

检测与分析工具

  1. Android Profiler:实时监控内存分配,定位内存抖动热点。
  2. Memory Profiler:查看短时间内的内存分配峰值和 GC 频率。
  3. Allocation Tracking:记录特定时间段内的所有对象分配,找出频繁创建的对象。
  4. Lint:静态代码分析工具,可检测潜在的内存抖动问题(如字符串拼接)。

总结

内存抖动的核心问题是频繁创建和销毁小对象,导致 GC 压力增大和 UI 卡顿。解决思路包括:

 

  • 对象复用:避免在循环、高频调用方法中创建新对象。
  • 资源管理:及时释放不再使用的资源,减少内存占用。
  • 预分配与缓存:合理设置集合初始容量,缓存常用对象。
  • 优化算法:减少不必要的计算和对象创建。

 

通过结合代码审查和工具分析,可以有效识别并解决内存抖动问题,提升应用性能。
posted @ 2025-06-19 09:45  一点点征服  阅读(56)  评论(0)    收藏  举报