Android内存抖动分析与常见案例

本文详细讨论了内存抖动的定义,常见案例如字符串拼接、对象频繁创建、内存池利用等,以及如何通过优化代码避免内存抖动和OOM。同时介绍了ART虚拟机对内存管理的改进。
摘要由CSDN通过智能技术生成

一、内存抖动定义

内存波动图形呈锯齿状、GC 导致卡顿。内存抖动在 Dalvik 虚拟机上更明显,因为 ART 虚拟机内存管理、回收策略做了优化,所以内存分配、GC 效率提升了 5~10 倍,内存抖动发生概率小。

当内存频繁分配和回收导致内存不稳定,出现内存抖动,内存抖动通常表现为频繁 GC、内存曲线呈锯齿状。

并且,内存抖动的危害严重,会导致页面卡顿,甚至 OOM。

OOM 原因

主要原因有如下两点:

1. 频繁创建对象,导致内存不足及不连续碎片:

public class MainActivity extends AppCompatActivity {

    private Button mButton;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        mButton = (Button) findViewById(R.id.button);

        mButton.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View view) {

                for (int i = 0; i < 100000; i++) {

    // 频繁创建大量的对象

                    byte[] data = new byte[1024 * 1024];

                }
            }
        });
    }
}

在这段代码中,每次点击按钮时都会创建 100,000 个大约为 1MB 的数组,如果内存不够用,则可能导致 OOM 错误。请注意,实际应用中应避免这种不负责任的内存使用行为。

2. 不连续的内存片无法被分配,导致 OOM:

public class MainActivity extends AppCompatActivity {

   private Button mButton;

   private ArrayList<byte[]> mDataList;

   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.activity_main);

       mButton = (Button) findViewById(R.id.button);

       mButton.setOnClickListener(new View.OnClickListener() {

           @Override

           public void onClick(View view) {

               mDataList = new ArrayList<>();

               for (int i = 0; i < 100000; i++) {

                   // 频繁创建大量的对象
                   byte[] data = new byte[1024 * 1024];
                   mDataList.add(data);
               }
           }
       });
   }
}

在这段代码中,每次点击按钮时都会创建大量的 1MB 大小的数组,并将它们添加到 mDataList中。由于内存是不连续的,因此在较大的数组中分配这些不连续的内存片可能导致 OOM 错误。请注意,实际应用中应避免这种不负责任的内存使用行为。

内存抖动解决

这里假设有这样一个场景:点击按钮使用 Handler 发送空消息,Handler 的 handleMessage 方法接收到消息后会导致内存抖动

for 循环创建 100 个容量为 10w+的 string[]数组在 30ms 后继续发送空消息。使用 MemoryProfiler 结合代码可找到内存抖动出现的地方。查看循环或频繁调用的地方即可。

public class MainActivity extends AppCompatActivity {

    private Button mButton;

    private Handler mHandler;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        mButton = (Button) findViewById(R.id.button);

        mButton.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View view) {

                mHandler.sendEmptyMessage(0);

            }

        });

        mHandler = new Handler() {

            @Override

            public void handleMessage(Message msg) {

                for (int i = 0; i < 100; i++) {

                    String[] arr = new String[100000];

                }

                mHandler.sendEmptyMessageDelayed(0, 30);

            }
        };
    }
}

请注意,这个代码中的消息循环可能会导致内存泄漏,因此您需要在适当的时候删除消息。

二、内存抖动常见案例

下面列举一些导致内存抖动的常见案例,如下所示:

字符串使用加号拼接
  1. 实际开发中我们不应该使用字符串使用加号进行拼接,而应该使用 StringBuilder 来替代。

  2. 初始化时设置容量,减少 StringBuilder 的扩容。

public class Main {

    public static void main(String[] args) {

        // 使用加号拼接字符串

        String str = "";

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 100000; i++) {

            str = str + "hello";

        }

        System.out.println("使用加号拼接字符串的内存使用量:" + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024) + " MB");

        System.out.println("使用加号拼接字符串的时间:" + (System.currentTimeMillis() - startTime) + " ms");

        // 使用StringBuilder

        StringBuilder sb = new StringBuilder(5);

        startTime = System.currentTimeMillis();

        for (int i = 0; i < 100000; i++) {

            sb.append("hello");

        }

        System.out.println("使用StringBuilder的内存使用量:" + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024) + " MB");

        System.out.println("使用StringBuilder的时间:" + (System.currentTimeMillis() - startTime) + " ms");

    }

}

输出结果:

使用加号拼接字符串的内存使用量:75 MB

使用加号拼接字符串的时间:4561 ms

使用 StringBuilder 的内存使用量:77 MB

使用 StringBuilder 的时间:4 ms

资源复用

使用全局缓存池,避免频繁申请和释放的对象。

public class ObjectPool {
    private static ObjectPool instance = null;
    private HashMap<String, Object> pool = new HashMap<>();
    private ObjectPool() {}
    public static ObjectPool getInstance() {
    if (instance == null) {
        instance = new ObjectPool();
    }
        return instance;
    }

    public void addObject(String key, Object object) {
        pool.put(key, object);
    }

    public Object getObject(String key) {
        return pool.get(key);
    }

    public void removeObject(String key) {
        pool.remove(key);
    }
}

该代码使用单例模式创建了一个 ObjectPool 类,并实现了添加、获取和删除对象的方法。

当应用程序需要使用某个对象时,可以通过调用 ObjectPool.getInstance().getObject(key) 方法从缓存池中获取该对象。

当不再需要该对象时,可以调用 removeObject(key) 方法将其从缓存池中删除。

但使用后,手动释放对象池中的对象(removeObject 这个 key)。

不合理的对象创建

onDraw 中创建的对象尽量进行复用

public class CustomView extends View {
    private Paint paint;
    private Rect rect;
    public CustomView(Context context) {
        super(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 重复创建对象,导致内存抖动
        paint = new Paint();
        rect = new Rect();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL);
        rect.set(0, 0, getWidth(), getHeight());
        canvas.drawRect(rect, paint);
    }

}

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 重复创建对象,导致内存抖动
        setContentView(new CustomView(this));
    }
}

上面的代码中,在CustomViewonDraw方法和MainActivityonCreate方法中,每次都重新创建了PaintRect对象,这会导致内存波动,因为系统并不能回收之前创建的对象。

为了避免这种情况,我们可以将PaintRect对象声明为类变量,并在构造方法中初始化,以保证只创建一次:

public class CustomView extends View {
    private Paint paint;
    private Rect rect;

    public CustomView(Context context) {
        super(context);
        // 初始化对象
        paint = new Paint();
        rect = new Rect();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL);
        rect.set(0, 0, getWidth(), getHeight());
        canvas.drawRect(rect, paint);
    }
}

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new CustomView(this));
    }
}

每次创建局部变量时,内存都会分配给它,但在循环结束后,它们不会被立即回收。这将导致内存的不断增加,最终导致内存抖动。

在循环中不断创建局部变量
//----------------------------错误示例---------------------------

for(int i=0;i< 100000;i++){

        Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.large_image);

        }

//----------------------------正确示例---------------------------

        Bitmap bitmap;

        for(int i=0;i< 100000;i++){

        bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.large_image);

        bitmap.recycle();

        }

在这个例子中,每次循环都会创建一个 Bitmap 对象,并将其赋值给局部变量 bitmap。但是,循环结束后, Bitmap 对象不会被立即回收,因此内存不断增加。

使用不合理的数据结构

建议使用 SparseArray 类族、ArrayMap 来替代 HashMap。

SparseArray用int[]数组存放key,避免了HashMap中基本数据类型需要装箱的步骤,其次不使用额外的结构体(Entry),单个元素的存储成本下降。

public class Main {

    public static void main(String[] args) {
        int N = 100000;
// Create a SparseArray
        SparseArray<Integer> sparseArray = new SparseArray<>();
        for (int i = 0; i < N; i++) {
            sparseArray.put(i, i);
        }

        System.out.println("SparseArray size: " + sparseArray.size());
        System.gc();
        long memorySparseArray = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
// Create an ArrayMap
        ArrayMap<Integer, Integer> arrayMap = new ArrayMap<>();
        for (int i = 0; i < N; i++) {
            arrayMap.put(i, i);
        }

        System.out.println("ArrayMap size: " + arrayMap.size());
        System.gc();
        long memoryArrayMap = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
// Create a HashMap
        HashMap<Integer, Integer> hashMap = new HashMap<>();
        for (int i = 0; i < N; i++) {
            hashMap.put(i, i);
        }

        System.out.println("HashMap size: " + hashMap.size());
        System.gc();
        long memoryHashMap = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        System.out.println("Memory usage:");
        System.out.println("SparseArray: " + memorySparseArray / 1024.0 + " KB");
        System.out.println("ArrayMap: " + memoryArrayMap / 1024.0 + " KB");
        System.out.println("HashMap: " + memoryHashMap / 1024.0 + " KB");
    }
}

  • 18
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值