Android内存优化内存抖动的概念和危害

内存抖动是一种内存管理的不良现象,它会影响应用的性能和稳定性。本文将从以下几个方面介绍内存抖动的定义、原因、后果和检测方法。

一、内存抖动的定义

内存抖动示例图

内存抖动是指内存频繁分配和回收导致的不稳定现象。在Java中,内存分配和回收是由垃圾回收器(GC)来管理的。GC会定期扫描内存中的对象,判断哪些对象是无用的,然后释放它们占用的空间。这个过程称为垃圾回收(GC)。

GC是一种有益的机制,它可以避免内存泄漏,提高内存利用率。但是,如果GC过于频繁或者耗时过长,就会影响应用的运行效率。当GC发生时,应用的线程会被暂停,等待GC完成后才能继续执行。这个过程称为GC停顿(GC Pause)。

如果应用中存在大量短期存在的对象,或者对象的生命周期不一致,就会导致内存分配和回收的次数增加,从而增加GC的频率和时间。这就是内存抖动的本质。

二、内存抖动的原因

导致内存抖动的原因有很多,这里列举一些常见的场景:

  • 字符串拼接:字符串是不可变的对象,每次拼接字符串都会创建一个新的字符串对象,并且丢弃旧的字符串对象。这样就会产生大量短期存在的字符串对象,增加GC的压力。例如:
java 复制代码
// 以下代码会创建5个字符串对象:"Hello"、"World"、"Hello World"、"!"、"Hello World!"
String s = "Hello" + "World" + "!";
  • 资源复用:如果没有正确地复用资源,比如Bitmap、Drawable、File等,就会导致资源被重复创建和销毁,占用更多的内存空间,并且触发更多的GC。例如:
java 复制代码
// 以下代码每次都会创建一个新的Bitmap对象,并且在使用完毕后立即回收
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);
imageView.setImageBitmap(bitmap);
bitmap.recycle();
  • 不合理的对象创建:如果在循环或者频繁调用的方法中创建了不必要的对象,或者使用了不合适的数据结构,就会导致内存分配和回收的次数增加,造成内存抖动。例如:
java 复制代码
// 以下代码每次都会创建一个新的ArrayList对象,并且在方法返回后立即被回收
public List<String> getNames() {
    List<String> names = new ArrayList<>();
    names.add("Alice");
    names.add("Bob");
    names.add("Charlie");
    return names;
}

四、内存抖动的后果

内存抖动会给应用带来以下几种负面影响:

  • 频繁GC:当内存分配和回收过于频繁时,GC就会更加频繁地执行,消耗更多的CPU资源,并且影响应用线程的执行。
  • 内存曲线呈锯齿状:当内存分配和回收不平衡时,内存使用量就会呈现出波动性,上下起伏,形成锯齿状。这样就会增加OOM(Out Of Memory)错误发生的风险。
  • 页面卡顿:当GC停顿时间过长时,应用线程就会被暂停,导致页面的渲染和交互出现延迟,用户感受到卡顿现象。

五、内存抖动的检测

要检测应用是否存在内存抖动,可以使用一些工具来监控和识别内存抖动。例如:

  • Memory Profiler:Memory Profiler是Android Studio中的一个工具,它可以实时显示应用的内存使用情况,包括内存分配、回收、泄漏等。通过Memory Profiler,可以观察到内存抖动的现象,比如内存曲线的锯齿状,以及GC的频率和时间。
  • Allocation Tracker:Allocation Tracker是Memory Profiler中的一个功能,它可以记录应用在一段时间内创建的所有对象,以及它们的类型、大小、数量等。通过Allocation Tracker,可以找出应用中产生内存抖动的代码,比如字符串拼接、资源复用、不合理的对象创建等。

以下是一个使用Memory Profiler和Allocation Tracker检测内存抖动的示例图:

检测路径

六、常见的内存优化方式

①、避免字符串拼接:

字符串拼接是一种非常低效的操作,它会产生大量无用的字符串对象,增加GC的压力。为了避免字符串拼接,可以使用以下几种方法:

1.StringBuilder:

StringBuilder是一个可变的字符串类,它可以在不创建新对象的情况下,对字符串进行修改和拼接。使用StringBuilder可以大大减少字符串对象的创建和回收。例如:

java 复制代码
    // 以下代码只会创建一个StringBuilder对象和一个字符串对象:"Hello World!"
    StringBuilder sb = new StringBuilder();
    sb.append("Hello");
    sb.append("World");
    sb.append("!");
    String s = sb.toString();
2.String.format:

String.format是一个静态方法,它可以根据指定的格式化字符串和参数,生成一个新的字符串对象。使用String.format可以避免在循环中拼接字符串,提高代码的可读性和性能。例如:

java 复制代码
// 以下代码只会创建一个字符串对象:"Hello World!"
String s = String.format("%s %s!", "Hello", "World");
3.资源文件:

资源文件是一种存储在应用中的文本文件,它可以用来保存一些常量或者多语言的字符串。使用资源文件可以避免在代码中硬编码字符串,减少字符串对象的创建和回收。例如:

java 复制代码
    <!-- 以下代码是一个资源文件(res/values/strings.xml)中的一段内容 -->
    <resources>
        <string name="hello_world">Hello World!</string>
    </resources>
java 复制代码
    // 以下代码只会创建一个字符串对象:"Hello World!"
    String s = getResources().getString(R.string.hello_world);

②、资源复用

资源复用是一种有效的优化内存抖动的方法,它可以减少资源的创建和销毁,提高内存利用率。为了实现资源复用,可以使用以下几种方法:

1.对象池

对象池是一种设计模式,它可以用来管理一组可重用的对象,而不是每次都创建和销毁对象。当需要一个对象时,可以从对象池中获取一个空闲的对象,使用完毕后,可以将对象归还到对象池中,等待下次使用。这样就可以避免频繁的内存分配和回收,减少GC的压力。例如:

java 复制代码
    // 以下代码是一个简单的Bitmap对象池的实现
    public class BitmapPool {
        // 一个存储Bitmap对象的队列
        private Queue<Bitmap> queue;
        // 对象池的最大容量
        private int capacity;

        // 构造方法,初始化队列和容量
        public BitmapPool(int capacity) {
            this.queue = new LinkedList<>();
            this.capacity = capacity;
        }

        // 从对象池中获取一个Bitmap对象,如果没有空闲的对象,就返回null
        public Bitmap getBitmap() {
            return queue.poll();
        }

        // 将一个Bitmap对象归还到对象池中,如果对象池已满,就回收该对象
        public void returnBitmap(Bitmap bitmap) {
            if (queue.size() < capacity) {
                queue.offer(bitmap);
            } else {
                bitmap.recycle();
            }
        }
    }

2.复用参数

复用参数是一种避免在方法中创建不必要的对象的方法,它可以将一些可变的参数作为方法的输入和输出,而不是在方法内部创建新的对象。这样就可以减少对象的创建和回收,提高代码的效率。例如:

java 复制代码
    // 以下代码是一个计算两个向量之间夹角的方法,它使用了一个复用参数result来存储计算结果,而不是在方法内部创建一个新的float数组
    public void calculateAngle(float[] vector1, float[] vector2, float[] result) {
        // 计算两个向量的点积
        float dotProduct = vector1[0] * vector2[0] + vector1[1] * vector2[1];
        // 计算两个向量的模长
        float length1 = (float) Math.sqrt(vector1[0] * vector1[0] + vector1[1] * vector1[1]);
        float length2 = (float) Math.sqrt(vector2[0] * vector2[0] + vector2[1] * vector2[1]);
        // 计算两个向量之间的夹角(弧度)
        float angle = (float) Math.acos(dotProduct / (length1 * length2));
        // 将计算结果存储在复用参数result中
        result[0] = angle;
    }

③、合理的对象创建

合理的对象创建是一种避免内存抖动的基本原则,它要求我们在编写代码时,尽量减少不必要的对象创建,或者使用更合适的数据结构。为了实现合理的对象创建,可以遵循以下几个建议:

1.避免在循环或者频繁调用的方法中创建对象

如果在循环或者频繁调用的方法中创建了不必要的对象,就会导致内存分配和回收过于频繁,造成内存抖动。因此,在编写代码时,应该尽量将对象的创建放在循环或者方法之外,或者使用静态变量或者成员变量来保存对象。例如:

java 复制代码
    // 以下代码是一个计算斐波那契数列第n项的方法,它使用了一个BigInteger数组来存储中间结果,但是每次调用该方法都会创建一个新的数组对象
    public BigInteger fibonacci(int n) {
        // 创建一个BigInteger数组,用来存储中间结果
        BigInteger[] array = new BigInteger[n + 1];
        // 初始化数组的第0项和第1项
        array[0] = BigInteger.ZERO;
        array[1] = BigInteger.ONE;
        // 从第2项开始,计算斐波那契数列
        for (int i = 2; i <= n; i++) {
            // 使用数组的前两项相加,得到当前项
            array[i] = array[i - 1].add(array[i - 2]);
        }
        // 返回数组的最后一项,即斐波那契数列的第n项
        return array[n];
    }
java 复制代码
    // 以下代码是一个优化后的计算斐波那契数列第n项的方法,它使用了一个静态变量来保存BigInteger数组,避免了每次调用该方法都创建一个新的数组对象
    public class FibonacciCalculator {
        // 创建一个静态变量,用来存储BigInteger数组
        private static BigInteger[] array;

        // 计算斐波那契数列第n项的方法
        public static BigInteger fibonacci(int n) {
            // 如果静态变量为空,或者长度不足,就重新创建一个新的数组对象,并初始化第0项和第1项
            if (array == null || array.length < n + 1) {
                array = new BigInteger[n + 1];
                array[0] = BigInteger.ZERO;
                array[1] = BigInteger.ONE;
            }
            // 从第2项开始,计算斐波那契数列
            for (int i = 2; i <= n; i++) {
                // 如果当前项为null,就使用数组的前两项相加,得到当前项
                if (array[i] == null) {
                    array[i] = array[i - 1].add(array[i - 2]);
                }
            }
            // 返回数组的最后一项,即斐波那契数列的第n项
            return array[n];
        }
    }

④、使用合适的数据结构

如果使用了不合适的数据结构,就会导致内存分配和回收不平衡,或者浪费内存空间,造成内存抖动。因此,在编写代码时,应该根据实际需求,选择合适的数据结构。例如:

1.使用基本类型而不是包装类型

基本类型(如int、float、boolean等)是直接存储在栈上的,它们不需要创建对象,也不会触发GC。而包装类型(如Integer、Float、Boolean等)是存储在堆上的对象,它们需要创建对象,并且会触发GC。因此,在可能的情况下,应该优先使用基本类型而不是包装类型。例如:

java 复制代码
        // 以下代码使用了包装类型Integer来存储一个整数值,这会导致内存分配和回收
        Integer value = new Integer(100);
java 复制代码
        // 以下代码使用了基本类型int来存储一个整数值,这会避免内存分配和回收
        int value = 100;

2.使用SparseArray而不是HashMap

SparseArray是Android中提供的一种数据结构,它可以用来存储键值对,其中键是int类型,值是任意类型。SparseArray比HashMap更节省内存空间,因为它不需要创建额外的对象来保存键值对。因此,在可能的情况下,应该优先使用SparseArray而不是HashMap。例如:

java 复制代码
        // 以下代码使用了HashMap来存储一些键值对,其中键是int类型,值是String类型,这会导致内存分配和回收
        HashMap<Integer, String> map = new HashMap<>();
        map.put(1, "Alice");
        map.put(2, "Bob");
        map.put(3, "Charlie");
java 复制代码
        // 以下代码使用了SparseArray来存储一些键值对,其中键是int类型,值是String类型,这会避免内存分配和回收
        SparseArray<String> array = new SparseArray<>();
        array.put(1, "Alice");
        array.put(2, "Bob");
        array.put(3, "Charlie");

⑤、使用数组而不是集合

数组是一种固定长度的数据结构,它可以用来存储一组相同类型的元素。数组比集合(如ArrayList、LinkedList等)更节省内存空间,因为它不需要创建额外的对象来保存元素。因此,在可能的情况下,应该优先使用数组而不是集合。例如:

java 复制代码
    // 以下代码使用了ArrayList来存储一组整数值,这会导致内存分配和回收
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
java 复制代码
    // 以下代码使用了数组来存储一组整数值,这会避免内存分配和回收
    int[] array = new int[3];
    array[0] = 1;
    array[1] = 2;
    array[2] = 3;

👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀

相关推荐
sunnyday042643 分钟前
Spring Boot 项目中使用 Dynamic Datasource 实现多数据源管理
android·spring boot·后端
幽络源小助理2 小时前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
inBuilder低代码平台2 小时前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐2 小时前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
白熊小北极2 小时前
Android Jetpack Compose折叠屏感知与适配
android
HelloBan2 小时前
setHintTextColor不生效
android
洞窝技术5 小时前
从0到30+:智能家居配网协议融合的实战与思考
android
QING6185 小时前
SupervisorJob子协程异常处理机制 —— 新手指南
android·kotlin·android jetpack
毕设源码-朱学姐6 小时前
【开题答辩全过程】以 基于安卓的停车位管理系统与设计为例,包含答辩的问题和答案
android
PWRJOY6 小时前
解决Flutter构建安卓项目卡在Flutter: Running Gradle task ‘assembleDebug‘...:替换国内 Maven 镜像
android·flutter·maven