• Android内存优化内存抖动的解决实战


    问题背景

    假设我们有一个应用,它的功能是在一个TextView上显示一个计数器,每隔一秒钟就更新一次计数器的值。为了实现这个功能,我们使用了一个Handler来发送空消息,并在接收到消息时更新计数器的值,并再次发送空消息,形成一个循环。同时,为了模拟一些复杂的业务逻辑,我们在循环中创建了大量的数组对象。以下是我们的代码:

    public class MainActivity extends AppCompatActivity {
        // 定义一个TextView来显示计数器的值
        private TextView textView;
        // 定义一个计数器变量
        private int counter = 0;
        // 定义一个Handler对象
        private Handler handler = new Handler() {
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                // 更新计数器的值
                counter++;
                // 在TextView上显示计数器的值
                textView.setText(String.valueOf(counter));
                // 模拟一些复杂的业务逻辑,创建大量的数组对象
                for (int i = 0; i < 1000; i++) {
                    int[] array = new int[1000];
                    array[0] = i;
                }
                // 再次发送空消息,形成一个循环
                handler.sendEmptyMessage(0);
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            // 初始化TextView
            textView = findViewById(R.id.textView);
            // 发送空消息,启动循环
            handler.sendEmptyMessage(0);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    这段代码看起来没有什么问题,但是当我们运行这个应用时,我们会发现应用的内存使用情况非常不稳定,内存曲线呈现出明显的锯齿状,GC事件也非常频繁。这就是典型的内存抖动的现象。

    问题定位

    要定位这个问题,我们可以使用Memory Profiler来观察应用的内存使用情况。Memory Profiler是Android Studio中的一个工具,它可以实时显示应用的内存使用情况,包括内存分配、回收、泄漏等。通过Memory Profiler,我们可以发现应用存在明显的内存波动和GC频率。

    以下是一个使用Memory Profiler观察应用内存使用情况的示例图:

    从示例图中可以看出:

    • Memory Usage:这个图表显示了应用在不同时间点的内存使用量,以及不同类型的内存(如Java Heap、Native Heap、Graphics等)。从图表中可以看出,应用的Java Heap内存使用量呈现出明显的波动性,上下起伏,形成锯齿状。这说明应用存在大量短期存在的对象,导致内存分配和回收不平衡。
    • GC Events:这个图表显示了应用在不同时间点发生的GC事件,以及GC事件的类型(如GC Alloc、GC Concurrent等)。从图表中可以看出,应用发生了非常频繁的GC事件,几乎每隔一秒钟就会发生一次GC事件。这说明应用的GC压力非常大,GC需要消耗更多的CPU资源,并且影响应用线程的执行。

    从这里重新输出,我会继续为你介绍内存抖动的解决实战。接下来,我将从以下几个方面给你展示如何使用Memory Profiler和代码排查来分析和解决一个具体的内存抖动问题:

    问题分析

    要分析这个问题,我们可以使用Memory Profiler来捕获堆转储,并根据对象分配跟踪来找出导致内存抖动的代码位置和原因。堆转储是一种保存应用在某个时间点的内存快照的文件,它可以用来查看应用中存在的所有对象,以及它们的类型、大小、数量等。对象分配跟踪是一种记录应用在一段时间内创建的所有对象,以及它们的类型、大小、数量、调用栈等的功能。通过堆转储和对象分配跟踪,我们可以发现应用中产生内存抖动的代码位置和原因。

    以下是使用Memory Profiler捕获堆转储和对象分配跟踪的示例图:

    从示例图中可以看出:

    • Heap Dump:这个功能可以让我们捕获应用在某个时间点的堆转储文件,并查看其中的内容。从堆转储文件中,我们可以看到应用中存在的所有类和实例,以及它们的类型、大小、数量等。通过对比不同时间点的堆转储文件,我们可以发现哪些类和实例是短期存在的,导致内存波动。
    • Allocation Tracking:这个功能可以让我们记录应用在一段时间内创建的所有对象,并查看其中的内容。从对象分配跟踪中,我们可以看到应用中创建的所有对象,以及它们的类型、大小、数量、调用栈等。通过分析对象分配跟踪,我们可以找出哪些代码位置是创建了大量对象,导致GC频繁。

    使用这两个功能,我们可以定位到导致内存抖动的代码位置和原因。经过分析,我们发现:

    • 在堆转储文件中,我们发现有大量的int[]数组对象存在于Java Heap中,它们占用了大部分的内存空间,并且在不同时间点的堆转储文件中,它们的数量和大小都有明显的变化。这说明这些数组对象是短期存在的,导致内存波动。
    • 在对象分配跟踪中,我们发现有大量的int[]数组对象被创建,它们的类型、大小、数量都是一致的。通过查看它们的调用栈,我们发现它们都是在MainActivity的handleMessage方法中创建的。这说明这个方法是创建了大量数组对象,导致GC频繁。 接下来,我将从以下几个方面给你展示如何使用Memory Profiler和代码排查来分析和解决一个具体的内存抖动问题:

    问题解决

    要解决这个问题,我们可以修改代码逻辑,避免在循环中创建大量数组,或者使用对象池来复用数组对象,从而减少内存分配和回收。以下是我们的优化方案:

    • 避免在循环中创建大量数组:如果我们不需要在循环中创建大量数组,我们可以将数组的创建放在循环之外,或者使用静态变量或者成员变量来保存数组。这样就可以避免每次循环都创建一个新的数组对象,减少内存分配和回收。例如:
    public class MainActivity extends AppCompatActivity {
        // 定义一个TextView来显示计数器的值
        private TextView textView;
        // 定义一个计数器变量
        private int counter = 0;
        // 定义一个Handler对象
        private Handler handler = new Handler() {
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                // 更新计数器的值
                counter++;
                // 在TextView上显示计数器的值
                textView.setText(String.valueOf(counter));
                // 模拟一些复杂的业务逻辑,使用一个静态变量来保存数组对象,避免在循环中创建大量数组对象
                for (int i = 0; i < 1000; i++) {
                    array[0] = i;
                }
                // 再次发送空消息,形成一个循环
                handler.sendEmptyMessage(0);
            }
        };
        // 定义一个静态变量,用来保存数组对象
        private static int[] array = new int[1000];
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            // 初始化TextView
            textView = findViewById(R.id.textView);
            // 发送空消息,启动循环
            handler.sendEmptyMessage(0);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 使用对象池来复用数组对象:如果我们需要在循环中创建大量数组,我们可以使用对象池来管理和复用数组对象,而不是每次都创建和销毁数组对象。当需要一个数组时,可以从对象池中获取一个空闲的数组,使用完毕后,可以将数组归还到对象池中,等待下次使用。这样就可以避免频繁的内存分配和回收,减少GC的压力。例如:
    public class MainActivity extends AppCompatActivity {
        // 定义一个TextView来显示计数器的值
        private TextView textView;
        // 定义一个计数器变量
        private int counter = 0;
        // 定义一个Handler对象
        private Handler handler = new Handler() {
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
                // 更新计数器的值
                counter++;
                // 在TextView上显示计数器的值
                textView.setText(String.valueOf(counter));
                // 模拟一些复杂的业务逻辑,使用一个对象池来管理和复用数组对象,避免在循环中创建大量数组对象
                for (int i = 0; i < 1000; i++) {
                    // 从对象池中获取一个空闲的数组对象
                    int[] array = arrayPool.getArray();
                    array[0] = i;
                    // 将数组对象归还到对象池中
                    arrayPool.returnArray(array);
                }
                // 再次发送空消息,形成一个循环
                handler.sendEmptyMessage(0);
            }
        };
        // 定义一个对象池,用来管理和复用数组对象
        private ArrayPool arrayPool;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            // 初始化TextView
            textView = findViewById(R.id.textView);
            // 初始化对象池
            arrayPool = new ArrayPool(1000, 1000);
            // 发送空消息,启动循环
            handler.sendEmptyMessage(0);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的核心笔记(还含底层逻辑):https://qr18.cn/FVlo89

    性能优化核心笔记:https://qr18.cn/FVlo89

    启动优化

    内存优化

    UI优化

    网络优化

    Bitmap优化与图片压缩优化https://qr18.cn/FVlo89

    多线程并发优化与数据传输效率优化

    体积包优化

    《Android 性能监控框架》:https://qr18.cn/FVlo89

    《Android Framework学习手册》:https://qr18.cn/AQpN4J

    1. 开机Init 进程
    2. 开机启动 Zygote 进程
    3. 开机启动 SystemServer 进程
    4. Binder 驱动
    5. AMS 的启动过程
    6. PMS 的启动过程
    7. Launcher 的启动过程
    8. Android 四大组件
    9. Android 系统服务 - Input 事件的分发过程
    10. Android 底层渲染 - 屏幕刷新机制源码分析
    11. Android 源码分析实战

  • 相关阅读:
    计算机网络-计算机网路体系结构(概念,组成,功能,分类,标准化工作及相关组织,性能指标(速率,带宽,吞吐量,时延,时延带宽积,往返时间RTT,利用率))
    【算法】一文带你从浅至深入门dp动态规划
    java计算机毕业设计实验中心网站(附源码、数据库)
    流媒体技术基础-流媒体服务与框架
    Java多线程、常用类、枚举类、注解、集合、泛型、IO、反射
    cv2 可视化操作整理(直线,圆,矩形,多边形,文本)
    网址,URL,域名,IP地址,DNS,域名解析(转载)
    坚持了 10 年的 9 个编程好习惯
    gerapy下载和安装以及部署全流程
    网络代理的多面应用:保障隐私、增强安全和数据获取
  • 原文地址:https://blog.csdn.net/weixin_61845324/article/details/133245881