目录

在这里插入图片描述

一、为什么要进行内存优化呢?

我们开发一个App程序,如果不了解内存的使用情况,就是将稳定性弃之不管。因为你不知道他在什么时候会发生OOM问题,不知道为什么程序会卡顿,不知道为什么会发生问题。你也没有自信跟别人说,你可以也出一个稳定可靠的App程序,所以这一篇文章,我们来研究一下内存优化。

Android进行内存优化是为了提高应用的稳定性、流畅性和存活时间,同时降低应用占用的ROM空间。


二、有哪些问题需要我们进行内存优化

  1. 防止应用发生OOM(Out of Memory)错误:Android设备对每个应用进程分配的内存是有限的,当应用内存使用超过这个限制时,就会发生OOM错误,导致应用异常退出。还有一种是,当申请的控件在内存中无连续的空间可分配的时候,也会出现问题。

  2. 避免不合理使用内存导致GC(Garbage Collection)次数增多,导致应用卡顿:在Android系统中,GC会暂停所有线程(包括主线程)来回收内存,如果内存使用不合理,频繁触发GC,就会导致应用卡顿。这个问题用两个名词来说,就是内存抖动和内存泄漏。

只要解决了内存抖动和内存泄漏,那么OOM问题就迎刃而解了


2.1 内存抖动是什么?

  1. 内存频繁的申请和回收。
  2. 申请次数太多引发GC,同时还会引起卡顿,因为当GC发生时,应用的线程会被暂停,等待GC完成后才能继续执行。
  3. 申请速度过快。

(1)代码举例:内存频繁的申请和回收

public class MemoryChurnView extends View {  
  
    private Paint paint;  
  
    public MemoryChurnView(Context context) {  
        super(context);  
        paint = new Paint();  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
  
        // 这里每次onDraw都会创建一个新的String和Rect对象  
        // 这些对象在onDraw结束后都会变得不可达  
        String text = "Some text that changes frequently"; // 实际上这里的text并没有改变,但只是为了示例  
        Rect bounds = new Rect();  
        paint.getTextBounds(text, 0, text.length(), bounds);  
  
        // 绘制文本(这个操作本身不会导致内存抖动,但上面的对象创建会)  
        canvas.drawText(text, bounds.left, bounds.bottom, paint);  
  
        // 注意:这里的text和bounds在onDraw结束后应该被GC回收  
        // 但如果onDraw被频繁调用,就会导致频繁的GC,即内存抖动  
    }  
}

如果onDraw中包含了创建大对象或大量对象的代码,那么就会导致内存抖动。出于性能和内存使用的考虑,我们通常不会在onDraw中做这样的事情。而是将创建对象放到外面。

public class MemoryChurnView extends View {  
  
    private Paint paint;  
  
    public MemoryChurnView(Context context) {  
        super(context);  
        paint = new Paint();  
    }  
  		 // 这里每次onDraw都会创建一个新的String和Rect对象  
        // 这些对象在onDraw结束后都会变得不可达  
        String text = "Some text that changes frequently"; // 实际上这里的text并没有改变,但只是为了示例  
        Rect bounds = new Rect();  
        
    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
  
       
        paint.getTextBounds(text, 0, text.length(), bounds);  
  
        // 绘制文本(这个操作本身不会导致内存抖动,但上面的对象创建会)  
        canvas.drawText(text, bounds.left, bounds.bottom, paint);  
  
        // 注意:这里的text和bounds在onDraw结束后应该被GC回收  
        // 但如果onDraw被频繁调用,就会导致频繁的GC,即内存抖动  
    }  
}

(2)代码示例:申请速度过快

public void allocateMemoryTooFast() {  
    List<byte[]> largeDataList = new ArrayList<>();  
  
    // 假设我们有一个很大的数字N  
    int N = 1000000;  
  
    // 在一个循环中快速分配内存  
    for (int i = 0; i < N; i++) {  
        // 分配一个相对较大的byte数组(例如1MB)  
        byte[] largeData = new byte[1024 * 1024]; // 1MB  
        largeDataList.add(largeData); // 假设我们需要保留这些数据的引用  
  
        // 注意:这里的代码在真实应用中可能会导致内存峰值过高和OOM异常  
        // 因为我们在短时间内分配了大量的内存  
    }  
  
    // 在实际应用中,我们可能需要对largeDataList进行处理  
    // 或者在分配过程中添加一些逻辑来限制内存的使用  
}

申请内存过快的问题通常发生在短时间内分配了大量内存的情况下。这可能会导致内存峰值过高,甚至触发OOM(Out Of Memory)异常。


2.2 内存泄漏是什么?

  1. 一个长生命周期的对象持有一个短生命周期对象的强引用
public class MainActivity extends AppCompatActivity {  
  
    private Handler handler = new Handler() {  
        @Override  
        public void handleMessage(Message msg) {  
            // 处理消息  
        }  
    };  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
  
        // 假设在某个地方启动了一个线程,该线程使用handler发送消息  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                Message message = handler.obtainMessage();  
                // 设置消息内容等  
                handler.sendMessage(message);  
            }  
        }).start();  
    }  
  
    @Override  
    protected void onDestroy() {  
        super.onDestroy();  
        // 这里没有取消handler的消息队列或处理handler的引用,可能导致内存泄漏  
    }  
}

Handler被定义为一个匿名内部类的实例,并持有MainActivity的隐式引用(因为Handler中的handleMessage方法需要访问MainActivity的成员)。当MainActivity被销毁时,如果Handler仍然有未处理的消息或Runnable任务在消息队列中,那么这些任务会持有MainActivity的引用,从而阻止垃圾回收器回收MainActivity的内存,导致内存泄漏。

解决方法:将Handler定义为一个静态内部类,并通过弱引用持有外部类的引用。这样,当外部类(如MainActivity)被销毁时,垃圾回收器可以回收它,而不会因为Handler的引用而阻止。

public class MainActivity extends AppCompatActivity {  

    private static class MyHandler extends Handler {  
        private final WeakReference<MainActivity> activityWeakReference;  

        MyHandler(MainActivity activity) {  
            activityWeakReference = new WeakReference<>(activity);  
        }  

        @Override  
        public void handleMessage(Message msg) {  
            MainActivity activity = activityWeakReference.get();  
            if (activity != null) {  
                // 处理消息,注意这里要判断activity是否为null  
            }  
        }  
    }  

    private final MyHandler handler = new MyHandler(this);  

    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  

        // 启动线程并使用handler发送消息  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                Message message = handler.obtainMessage();  
                // 设置消息内容等  
                handler.sendMessage(message);  
            }  
        }).start();  
    }  

    @Override  
    protected void onDestroy() {  
        super.onDestroy();  
        // 不需要显式取消handler,因为handler是静态内部类,不会持有外部类的强引用  
    }  
}

如果Handler不是静态内部类,且你确实需要在非静态内部类中使用它,那么你应该在onDestroy方法中显式地移除所有回调和取消所有未完成的任务。然而,这种方法通常不如使用静态内部类+弱引用来得安全和优雅。

好的,到这里,大概问题我们都知道了,那么内存抖动和内存泄漏,很多时候,你可能自己都不知道,因为:

  1. 有可能你写的代码有问题,但是你没有看出来。
  2. 有可能是第三方框架,或者使用系统的代码有问题,你不知道。

那么怎么办呢?这个时候我们就需要借助工具来将其检测出来。


三、内存抖动工具使用

3.1 内存抖动工具介绍:Profiler

Android Profiler是Android Studio中的一个集成工具,它允许开发者在运行的应用上执行实时性能分析,是评估代码性能的重要工具。
这个章节,我们主要使用Memory Profiler。

Memory Profiler:帮助开发人员理解应用程序的内存使用情况,包括内存分配和回收,通过可视化图表


3.2 我们先来看看如何使用呢

(1)在Android Studio的主界面上,找到并点击“View”菜单,选择“Tool Windows”下的“Profiler”

在这里插入图片描述

(2)我们运行一个程序看看,直接点击运行。

在这里插入图片描述

我们就可以在Profiler里面看到信息。

在这里插入图片描述我们主要使用Memory Profiler,点击它

在这里插入图片描述

在这里插入图片描述


3.3 正常情况的内存

(1)正常的情况

刚启动的时候,内存有波动是正常的,启动后,我们可以看到也是一条直线,这就是正常状态。

在这里插入图片描述
我们可以点击一下Record,开始记录,看看申请的内存变量怎么样。

在这里插入图片描述
在这里插入图片描述
点击排序,可以看看哪个对象最多。


3.4 异常情况的内存

我们看看一个异常代码,比如在自定义view里面不断创建Paint。

public class MemoryJitterView extends View {
    private static final String TAG = "MemoryJitterView";
    private int width;  
    private int height;  
  
    public MemoryJitterView(Context context) {  
        super(context);  
    }  
  
    public MemoryJitterView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
    }  
  
    public MemoryJitterView(Context context, AttributeSet attrs, int defStyleAttr) {  
        super(context, attrs, defStyleAttr);  
    }  
  
    @Override  
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
        super.onSizeChanged(w, h, oldw, oldh);  
        this.width = w;  
        this.height = h;  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
  
        // 这里故意创建新的Paint对象来模拟内存抖动  
        // 在实际应用中,应该避免这样做,而是应该重用Paint对象  
        Paint paint = new Paint();  
        paint.setColor(Color.RED);  
        paint.setStrokeWidth(10);  
  
        // 画一条对角线来演示  
        canvas.drawLine(0, 0, width, height, paint);  
  
        // 为了更频繁地触发onDraw,可以调用invalidate()来请求重绘  
        // 注意:这样做会导致CPU和GPU过度使用,并且在实际应用中是不推荐的  
        // 这里仅用于演示内存抖动  
        postInvalidateOnAnimation(); // 在下一帧动画时间请求重绘
        Log.d(TAG, "onDraw: ");
    }  
}

然后在MainActivity里面使用它

 Handler().postDelayed(object :Runnable{
            override fun run() {
                // 设置自定义View为Activity的内容视图
               setContentView(MemoryJitterView(this@MainActivity))
               setContentView(MemoryJitterView(this@MainActivity))
               setContentView(MemoryJitterView(this@MainActivity))
               setContentView(MemoryJitterView(this@MainActivity))
               setContentView(MemoryJitterView(this@MainActivity))
               setContentView(MemoryJitterView(this@MainActivity))
            }
        },10000)

接下来我们运行程序。

(1)一开始的时候,91.9MB。

在这里插入图片描述随着运行,占用不断的增大,线条也呈斜线递增。

在这里插入图片描述这个时候,我们就点击进行记录分析。

在这里插入图片描述

记录到一定量以后点击停止,就可以开始分析。

在这里插入图片描述

点击一下排序,看看哪些对象产生的最多。

在这里插入图片描述
比如我们点击Paint。

在这里插入图片描述
具体问题位置,代码也刚好是41行,就全部出来。我们就要可以开始优化了。

在这里插入图片描述


四、内存泄漏的工具使用

LeakCanary是一个用于检测Android和Java应用中的内存泄漏的开源工具。但LeakCanary只能检测Activity、Fragment、ViewModel等,比较局限。
想要使用LeakCanary也可以看看这篇文章:https://blog.csdn.net/qq_40853919/article/details/140700972

下面我们来介绍一种MAT的内存分析工具,可以到官网进行下载。


4.1 如何使用

假设一个这样的场景:

  1. A:在MainActivity1界面。
  2. B:跳转到MainActivity2界面,然后再返回。

我想看看MainActivity2关掉以后,有没有出现内存泄漏的情况。所以呢我需要记录MainActivity1的堆栈信息,然后执行完B后跳回来的时候,再记录一次MainActivity1的堆栈信息,然后进行对比,如果有多出来的地方,那么就是内存泄漏了。

(1)记录堆栈

在这里插入图片描述
点击保存。

在这里插入图片描述

就会得到一个这样的文件。

在这里插入图片描述

如果直接放过去MemoryAnalyzer软件里面是会报错的。我们需要转一下格式,需要使用的Android SDK转换工具,hprof-conv命令

在这里插入图片描述
进行类型转换。
在这里插入图片描述

在这里插入图片描述
这样,我们就得到了两个转换后的文件,直接放到MemoryAnalyzer里面。

在这里插入图片描述

接下来使用工具,找到两个文件,直接打开他们。

在这里插入图片描述

到这里,我们已经可以看到内存情况。

在这里插入图片描述

除了柱状图,我们还有其他的数据显示方式。

在这里插入图片描述
如下是直方图的方式。
在这里插入图片描述

如果要进行对比,那么我们只需要点击这个就可以实现堆栈对比。对比两个文件,看看有没有内存泄漏。

在这里插入图片描述

如果是+,那么就是增加了多少,也就是有多少没有释放。
如果是+0,那么就是没有增加,也就是都释放了。

在这里插入图片描述
好了,MAT就介绍到这里。


五、总结

可以看到,其实大部分的原因,都是因为写代码的水平问题,写代码不规范,导致的一些内存抖动,或者内存泄漏。所以我们需要去提升自己的编码能力,去注意这些细节。

在这里插入图片描述在这里插入图片描述

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐