内存颤动是一种内存办理的不良现象,它会影响运用的功能和稳定性。本文将从以下几个方面介绍内存颤动的界说、原因、成果和检测办法。

一、内存颤动的界说

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

内存颤动示例图

内存颤动是指内存频频分配和收回导致的不稳定现象。在Java中,内存分配和收回是由废物收回器(GC)来办理的。GC会定期扫描内存中的目标,判别哪些目标是无用的,然后释放它们占用的空间。这个进程称为废物收回(GC)。

GC是一种有利的机制,它能够防止内存走漏,提高内存利用率。可是,假如GC过于频频或许耗时过长,就会影响运用的运转效率。当GC发生时,运用的线程会被暂停,等候GC完成后才干继续执行。这个进程称为GC中止(GC Pause)。

假如运用中存在很多短期存在的目标,或许目标的生命周期不一致,就会导致内存分配和收回的次数添加,然后添加GC的频率和时刻。这便是内存颤动的实质。

二、内存颤动的原因

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

  • 字符串拼接:字符串是不可变的目标,每次拼接字符串都会创立一个新的字符串目标,并且丢弃旧的字符串目标。这样就会发生很多短期存在的字符串目标,添加GC的压力。例如:
// 以下代码会创立5个字符串目标:"Hello"、"World"、"Hello World"、"!"、"Hello World!"
String s = "Hello" + "World" + "!";
  • 资源复用:假如没有正确地复用资源,比方Bitmap、Drawable、File等,就会导致资源被重复创立和毁掉,占用更多的内存空间,并且触发更多的GC。例如:
// 以下代码每次都会创立一个新的Bitmap目标,并且在运用完毕后立即收回
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);
imageView.setImageBitmap(bitmap);
bitmap.recycle();
  • 不合理的目标创立:假如在循环或许频频调用的办法中创立了不必要的目标,或许运用了不合适的数据结构,就会导致内存分配和收回的次数添加,形成内存颤动。例如:
// 以下代码每次都会创立一个新的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检测内存颤动的示例图:

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

检测路径

六、常见的内存优化办法

①、防止字符串拼接:

字符串拼接是一种十分低效的操作,它会发生很多无用的字符串目标,添加GC的压力。为了防止字符串拼接,能够运用以下几种办法:

1.StringBuilder:

StringBuilder是一个可变的字符串类,它能够在不创立新目标的情况下,对字符串进行修改和拼接。运用StringBuilder能够大大削减字符串目标的创立和收回。例如:

    // 以下代码只会创立一个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能够防止在循环中拼接字符串,提高代码的可读性和功能。例如:

// 以下代码只会创立一个字符串目标:"Hello World!"
String s = String.format("%s %s!", "Hello", "World");
3.资源文件:

资源文件是一种存储在运用中的文本文件,它能够用来保存一些常量或许多语言的字符串。运用资源文件能够防止在代码中硬编码字符串,削减字符串目标的创立和收回。例如:

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

②、资源复用
资源复用是一种有效的优化内存颤动的办法,它能够削减资源的创立和毁掉,提高内存利用率。为了完成资源复用,能够运用以下几种办法:
1.目标池
目标池是一种规划模式,它能够用来办理一组可重用的目标,而不是每次都创立和毁掉目标。当需求一个目标时,能够从目标池中获取一个闲暇的目标,运用完毕后,能够将目标归还到目标池中,等候下次运用。这样就能够防止频频的内存分配和收回,削减GC的压力。例如:

    // 以下代码是一个简略的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.复用参数
复用参数是一种防止在办法中创立不必要的目标的办法,它能够将一些可变的参数作为办法的输入和输出,而不是在办法内部创立新的目标。这样就能够削减目标的创立和收回,提高代码的效率。例如:

    // 以下代码是一个核算两个向量之间夹角的办法,它运用了一个复用参数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.防止在循环或许频频调用的办法中创立目标
假如在循环或许频频调用的办法中创立了不必要的目标,就会导致内存分配和收回过于频频,形成内存颤动。因而,在编写代码时,应该尽量将目标的创立放在循环或许办法之外,或许运用静态变量或许成员变量来保存目标。例如:

    // 以下代码是一个核算斐波那契数列第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];
    }
    // 以下代码是一个优化后的核算斐波那契数列第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。因而,在可能的情况下,应该优先运用根本类型而不是包装类型。例如:

        // 以下代码运用了包装类型Integer来存储一个整数值,这会导致内存分配和收回
        Integer value = new Integer(100);
        // 以下代码运用了根本类型int来存储一个整数值,这会防止内存分配和收回
        int value = 100;

2.运用SparseArray而不是HashMap
SparseArray是Android中供给的一种数据结构,它能够用来存储键值对,其中键是int类型,值是任意类型。SparseArray比HashMap更节约内存空间,因为它不需求创立额定的目标来保存键值对。因而,在可能的情况下,应该优先运用SparseArray而不是HashMap。例如:

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

⑤、运用数组而不是调集
数组是一种固定长度的数据结构,它能够用来存储一组相同类型的元素。数组比调集(如ArrayList、LinkedList等)更节约内存空间,因为它不需求创立额定的目标来保存元素。因而,在可能的情况下,应该优先运用数组而不是调集。例如:

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

重视大众号:Android老皮!!!欢迎大家来找我讨论沟通