Android内存泄露分析以及工具的使用

1,131 阅读11分钟

Android性能优化对于开发者来说是一个不可小觑的问题,如果软件的性能极差,造成界面卡顿,甚至直接挂掉,对于用户来说是一个极其致命的,可能会导致用户直接把应用给卸载了。相反的,如果把性能优化得极致,运行得很流畅,从而增加用户的好感,得到好评,所以性能优化对于开发者来说是非常重要的。

Android的性能优化通常涉及到内存泄露检测、渲染性能优化、电量优化、网络优化和Bitmap内存管理优化,以及多线程优化等等,当然性能优化的不止这些,除此之外还有安装包优化和数据传输效率等,所以Android的性能优化涉及的范围是比较广的。心急吃不了热豆腐,因此需要我们一点点来学习,慢慢研究。

内存泄露

内存泄露,关乎到开发者本身写代码的问题,所以平时开发者写代码要有严谨性和清晰的逻辑,申请的内存,没用之后,就要释放掉。那么什么是内存泄露呢?

了解内存泄露,首先需要了解java的内存分配,其中主要包括静态存储区、栈区、堆区、寄存器、常量池等。

静态存储区:内存在程序编译的时候就已经分配好,这块的内存在程序整个运行期间都一直存在,主要存放静态数据、全局的static数据和一些常量。

栈区:保存局部变量的值,其中包括:用来保存基本数据类型的值、保存类的实例(即堆区对象的引用(指针)),以及用来保存加载方法时的帧。也就是说函数一些内部变量的存储在栈区,函数执行结束的时,这些存储单元就会自动被释放掉。因为栈内存内置在处理器的里面,所以运算速度很快,但是栈区的容量有限。

堆区:也叫做动态内存分配,用来存放动态产生的数据,如new出来的对象。用malloc或者new来申请分配一个内存。在C/C++可能需要自己负责释放,但在java里面直接依赖GC机制。

寄存器:JVM内部虚拟寄存器,存取速度非常快,程序不可控制。

常量池:存放常量。

关于内存分配,读者可以参考《Java 内存分配全面浅析》

栈和堆的区别: 1)堆是不连续的内存区域,堆空间比较灵活也特别大。 2)栈式一块连续的内存区域,大小是由操作系统决定的。

由于堆是不连续的内存区域,管理起来特别的麻烦,如果频繁的new和remove,可能会造成大量的内存碎片,所造成的内存碎片就造成了内存泄露,这样就会导致运行效率低下。但是对于栈,栈的特点是先进后出,进出完全不会产生碎片,运行效率高且稳定。因此我们关于内存泄露,主要看堆内存。

内存泄露(memory leak):是指程序在申请内存后,无法释放已申请的内存空间。也就是说当一个对象已经不再使用了,本该被回收时,但有另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。

内存溢出

内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。在Android中如果出现内存溢出,也就是我们经常看到的OOM情况,那么应用就会闪退。由于内存泄露,而导致可用的内存空间越来越少,从而导致OOM。因此平时写代码特别需要主要内存泄露的情况,因为一旦出现内存泄露,随着泄露的内存越来越多,就会造成内存溢出。

既然是内存泄露会导致内存溢出,归根结底还是需要优化内存泄露。优化内存泄露,首先需要找到内存泄露的地方,然后才能去优化。

确定内存泄露

1)使用AndroidStudio自带的Memory Monitors进行内存分析。

monitor

通过可视化可以观察到该应用的Memery、CPU、NetWork和GPU变化等情况。这里我们主要观察Memery(内存)即可,上图是我打开应用的首页时Memery的情况。

然后点击几次InitiateGC:

monitor

这是点击GC之后,稳定下来的情况,基本上时一条水平线的状态的了。

monitor

Free:表示还可用的内存,在图中浅灰色表示。 Allocated:表示已经分配的内存大小,同样在图中蓝色表示

当我进入下个页面的时候,明显看到内存变化

monitor

当我返回上一个页面的时候,然后GC

monitor

上图就是返回之后,点击GC的情况,Free和Allocated并没有变化,说明刚刚进入的那个页面就没有出现内存泄露的情况,如果出现变化比较明显,那就可以判断刚刚所进入的页面出现了内存泄露的情况。同样也可以使用Heap Viewer观察内存泄露。

Heap Viewer

Heap Viewer:能够实时查看App分配的内存大小和空闲内存大小,并发现内存泄露。除此功能以外,Heap Viewer还可以检测内存抖动,因为内存抖动的时候,会频繁发生GC,这个时候我们只需要开启Heap Viewer,观察数据的变化,如果发生内存抖动,会观察到数据在短时间内频繁更新。

启动Heap Viewer:

monitor

选择设备下对应的包名,然后update Heap

monitor

选择Head,然后GC

monitor

monitor

点击Cause GC,发现所有的数据都更新了,更新后的表格显示,在Heap上哪些数据是可用的,选中其中任一行数据,就可以看到详细数据。

data object的total size就是当前进程中Java的对象所占用的内存总量。我们反复执行某一个操作并同时执行GC排除可以回收掉的内存,注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。反之如果代码中存在没有释放对象引用的情况,随着操作次数的增多Total Size的值会越来越大。

点击class object,屏幕上马上出现大量更新的数据,矩形图列出这一数据内存分配的数量,以及确切的容量,heap viewer可以有效地分析程序在堆中所分配的数据类型,以及数量和大小。

Allocation Tracker

除了Head Viewer和Memory Monitor,还可以使用Allocation Tracker(分配追踪器)。

monitor

关于Allocation Tracker可以查看 《Android性能专项测试之Allocation Tracker(Android Studio)》 这篇文章进行学习。

以上的工具都具有不同的特点,具体使用那一个工具可以按照以下来划分: 1)Memory Monitor:获得内存的动态视图 2)Heap Viewer:显示堆内存中存储了什么 3)Allocation Tracker:具体是哪些代码使用了内存

使用MAT内存分析工具

MAT:Memory Analyzer Tools,一款详细分析Java堆内存的工具,从而能够分析出内存泄露的详细情况。

使用AndroidStudio生成hprof文件:

monitor

生成的hprof文件不能直接交给MAT, MAT是不识别的, 我们需要右键点击这个文件,转换成MAT识别的。

monitor

在eclipse中安装MAT,然后打开hprof文件:

monitor

monitor

使用MAT来分析内存泄露的情况: 1、根据data object的Total Size,找到内存泄露的操作; 2、找到内存泄露的对象(怀疑对象),也就是通过MAT对比操作前后的hprof文件来定位内存泄露,是那个数据对象内存泄露了; 3、找到内存泄露的原因,也就是那个对象持有了第2个步骤找出来的发生内存泄露的对象。

具体步骤: 1)进入Histogram,过滤出某一个嫌疑对象类;

monitor

2)分析持有此类对象引用的外部对象;

monitor

3)过滤掉一些弱引用、软引用、虚引用,因为它们迟早可以被GC干掉不属于内存泄露。

monitor

过滤掉之后就需要进入代码分析此时的对象的引用持有是否合理,然后进行解决。

内存优化一般分为两方面,一方面在开发过程中避免写出有内存泄露的代码,另一方面就是我们前面介绍的,利用一些内存分析工具检测出潜在的内存泄露。我看看平时我们开发中,那些需要注意内存泄露的地方。

静态变量引起的内存泄露
    private static Context sContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        sContext = this;
    }

以上代码Activity无法正常销毁,因为sContext引用了它。

单例模式所造成的内存泄露
public class MyInstance {

    private static MyInstance instance;
    private Context context;

    private MyInstance(Context context) {
        this.context = context;
    }

    public static MyInstance getInstance(Context mcontext) {
        if (instance == null) {
            instance = new MyInstance(mcontext);
        }
        return instance;
    }

    public void setContext(Context context) {
        this.context = context;
    }
}

单例模式的生命周期和Application保持一致,如果在Activity中调用getInstance,把Activity的Context传入,那么Activity的对象就会被单例模式的MyInstance所持有,造成内存泄露,其实也是同属于静态变量引起的内存泄露,因为instance就是静态变量。而静态变量属于静态存储方式,其存储空间为内存中的静态数据区(在静态存储区内分配存储单元),该区域中的数据在整个程序的运行期间一直占用这些存储空间(在程序整个运行期间都不释放)。

非静态内部类引起内存泄露
    //隐式持有Activity实例,Activity.this.a
    public void loadData(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    try {
                        int b=a;
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

还有Handler使用非静态内部类的形式:

     private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

如何解决非静态内部类引起内存泄露的问题呢?这就需要将静态内部类修改为静态内部类,因为静态内部类不会隐式持有外部类。

//解决方案:
    private static class MyHandler extends Handler{
        //设置软引用保存,当内存一发生GC的时候就会回收。
        private WeakReference<MainActivity> mainActivity;

        public MyHandler(MainActivity mainActivity) {
            this.mainActivity = new WeakReference<MainActivity>(mainActivity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity main =  mainActivity.get();
            if(main==null||main.isFinishing()){
                return;
            }
            switch (msg.what){
                case 0:
                    //加载数据
                    // 引用MainActivity.this.a;
                    int b = main.a;
                    break;

            }
        }
    };

在上面的代码中使用静态内部类的形式创建了一个继承Handler的MyHandler,并且内部使用弱引用WeakReference,既WeakReference,如果不用弱引用的话,mainActivity就会直接持有了一个外部类的强引用,导致内存泄露。最好在onDestroy调用Handler的removeCallbacksAndMessages方法。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        MyHandler.removeCallbacksAndMessages(null);
    }

上面提到了弱引用,这里我们需要了解引用相关的知识:

1)StrongReference(强引用):从不回收,在JVM停止的时候才会终止。

2)SoftReference(软引用):当内存不足的时候就会回收。

3)WeakReference(弱引用):在垃圾回收的时候就会回收,它在GC后终止。

4)PhatomReference(虚引用):在垃圾回收的时候就会回收,同样在GC后终止。

资源未关闭引起的内存泄露情况

平时用到的资源,用完之后需要关闭,防止内存泄露,如BroadCastReceiver、Cursor、Bitmap、IO流和自定义属性AttributeSet等资源。在自定义的AttributeSet资源用完之后,需要调用attrs.recycle()进行回收。否则会造成内存泄露。

属性动画导致的内存泄露

属性动画有一类无限循环动画,如果没有在onDestroy方法中停止动画,Activity就会导致内存泄露。因为,如果没有停掉动画的话,Activity的View就会被动画持有,而View又持有了Activity,最终Activity无法释放。

用完后的监听未移除导致内存泄露
public class ListenerCollector {

    static private WeakHashMap<View, MyView.MyListener> sListener = new WeakHashMap<>();

    public void setsListener(View view, MyView.MyListener listener) {
        sListener.put(view, listener);
    }

    public static void clearListeners() {
        //移除所有监听。
        sListener.clear();
    }

}

如上面的代码,添加了监听,使用完之后,需要在onDestroy方法里需要调用clearListeners方法,移除监听。关于WeakHashMap的特点就是当除了自身有对key的引用外,如果此key没有其他引用那么此map会自动丢弃此值,如上面的view=null,那么sListener里面的view就会被丢弃。