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

Android性能优化对于开发者来讲是一个不可小觑的问题,若是软件的性能极差,形成界面卡顿,甚至直接挂掉,对于用户来讲是一个极其致命的,可能会致使用户直接把应用给卸载了。相反的,若是把性能优化得极致,运行得很流畅,从而增长用户的好感,获得好评,因此性能优化对于开发者来讲是很是重要的。java

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

内存泄露

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

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

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

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

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

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

常量池:存放常量。函数

关于内存分配,读者能够参考《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来分析内存泄露的状况: 一、根据data object的Total Size,找到内存泄露的操做; 二、找到内存泄露的对象(怀疑对象),也就是经过MAT对比操做先后的hprof文件来定位内存泄露,是那个数据对象内存泄露了; 三、找到内存泄露的缘由,也就是那个对象持有了第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就会被丢弃。