Android性能调优篇之内存泄露

开篇废话

通过我之前的两篇文章

Android性能调优篇之探索JVM内存分配

Android性能调优篇之探索垃圾回收机制

我们大概了解了Java内存的一些基本知识,这个对于本篇文章的要讲的内存泄露,还是挺有帮助的。

本来最开始就想写关于内存泄露的文章的,由于它涉及了一些Java内存的基本知识,所以为了铺垫,写下了内存分配机制以及垃圾回收的两篇文章。

关于内存泄露,Memory Leak,我想基本上所有开发人员都多多少少接触过这个概念,因为它确实与我们的实际开发脱不了干系。这次我讲述内存泄露的角度主要是从Android实际开发的角度。


技术详情

1.什么是内存泄露

所谓内存泄露,就是指我们不再使用的对象持续占有内存,或者这些不再使用的对象没有办法得到及时释放(GC Roots依然可达),而导致内存空间的浪费。值得注意的是,我们App的内存泄露的不断积累,最终会导致OOM(Out Of Memory),更严重的导致程序崩溃,所以我们平时一定要处理内存泄露。

2.Android中的内存泄露

2.1 单例

我们通过代码,来看一下单例模式产生的内存泄露。

首先是单例类OyTestManager.java(这里就不写关于实际业务的代码了):

import android.content.Context;

/**
 * *****************************************************************
 * * 文件作者:ouyangshengduo
 * * 创建时间:2017/8/14
 * * 文件描述:单例模式演示内存泄露
 * * 修改历史:2017/8/14 21:41*************************************
 **/

public class OyTestManager {

    private static OyTestManager mInstance;
    private Context mContext;

    private OyTestManager(Context mContext){
        this.mContext = mContext;
    }

    public static OyTestManager getmInstance(Context mContext){
        if(null == mInstance){
            synchronized (OyTestManager.class){
                if(null == mInstance){
                    mInstance = new OyTestManager(mContext);
                }
            }
        }
        return mInstance;
    }
}


然后再MainActivity.java里面使用这个单例:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {


    private OyTestManager oyTestManager;

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

        initData();

    }

    /**
     * 数据初始化
     */
    private void initData(){

        oyTestManager = OyTestManager.getmInstance(this);

    }
}

代码非常简单,这里只是为了讲解我们实际开发中使用单例造成的内存泄露,通过上面的代码,我们使用Android Studio里面的Android Monitor进行内存泄露的分析(也可以使用其他的工具,如MAT),分析步骤为:

1 将以上代码在Android设备上跑起来,然后点击返回退出软件

2.点击Android Studio的Android Monitor中的Initiate GC,触发系统的一次GC,具体操作截图如下:

Android性能调优篇之内存泄露_第1张图片
触发GC

3.然后点击Initiate GC旁边的Dump Java Heap,将此时的系统的Java堆的情况导出来,稍等一会,会生成一个.hprof的文件
具体操作截图如下:

Android性能调优篇之内存泄露_第2张图片
导出堆状态

4.点击任务分析按钮,开始分析,分析结束会有一个分析结果,具体查看分析结果中的内容,看我们的软件退出之后,是否还有资源没有得到释放

Android性能调优篇之内存泄露_第3张图片
调出分析界面
Android性能调优篇之内存泄露_第4张图片
开始分析

从以上操作中,我们能够看出,我们的MainActivity已经退出了,但系统并没有回收掉这个MainActivity,因为在MainActivity中使用了单例模式,mInstance这个静态对象与MainAcitivty依然存在引用关系,从之前的内存相关的知识可以知道,mInstance在这里就可以作为一个GC Root,因为从GC Root开始进行搜索,对于MainActivity这个对象是可达的,所以,系统没有回收掉这个对象。

知道了原因,我们就可以对其进行优化了。

我们知道单例的静态特性与我们的App的生命周期是一样长的,所以,我们只需要把MainActivity的引用替换成我们的ApplictionContext,这样,系统就能回收掉MainActivity对象了,以下是单例模式进行优化后的写法:


import android.content.Context;

/**
 * *****************************************************************
 * * 文件作者:ouyangshengduo
 * * 创建时间:2017/8/14
 * * 文件描述:单例模式演示内存泄露
 * * 修改历史:2017/8/14 21:41*************************************
 **/

public class OyTestManager {

    private static OyTestManager mInstance;
    private Context mContext;

    private OyTestManager(Context mContext){
        this.mContext = mContext.getApplicationContext();
    }

    public static OyTestManager getmInstance(Context mContext){
        if(null == mInstance){
            synchronized (OyTestManager.class){
                if(null == mInstance){
                    mInstance = new OyTestManager(mContext);
                }
            }
        }
        return mInstance;
    }

}

在构造方法中this.mContext = mContext 改成了:this.mContext = mContext.getApplicationContext();

通过以上的写法,我们再进行上面那种分析方法,就不会出现Leaked Activity这一项了,也就意味着,我们已经对这个单例优化成功了。

2.2 匿名内被类

匿名内部类,在Java当中,非静态内部类默认将会有持有外部类的引用,当在内部类实例化一个静态的对象,那么,这个对象将会与App的生命周期一样长,又因为非静态内部类一直持有外部的MainActivity的引用,导致MainActivity无法被回收,内存泄露的代码如下:


import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {


    private OyTestManager oyTestManager;

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

        initData();
    }

    /**
     * 数据初始化
     */
    private void initData(){

        oyTestManager = OyTestManager.getmInstance(this);

    }

    //定义一个内部类
    class LeakTest{
        private static final String TAG = "Just a test";
    }
}

用上面的分析方法区分析,同样会出现Leaked Activities,也就是存在内存泄露

Android性能调优篇之内存泄露_第5张图片
内部类分析

这种情况,我们需要把匿名内部类修改为静态内部类,静态内部类,这样静态内部类就不会持有外部MainActivity的引用,从而不会有内存泄露的问题,优化后的代码如下:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {


    private OyTestManager oyTestManager;

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

        initData();
    }

    /**
     * 数据初始化
     */
    private void initData(){

        oyTestManager = OyTestManager.getmInstance(this);

    }

    //定义一个内部类
    static class LeakTest{
        private static final String TAG = "Just a test";
    }
}

2.3 Handler

Handler 我们应该比较熟悉,我们通常使用它来进行子线程到主线程的UI更新。不过,我们实际开发中,因为Handler而造成的内存泄漏是最常见的,比如说,我们平时处理一些网络数据获取的时候,会请求一个回调,然后我们会使用Handler进行处理。这个时候,如果我们没有考虑到内存泄露,就会造成比较严重的问题。

首先,我们平时使用Handler 都是这样的:

private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            //业务逻辑代码
        }
    };

我们来分析一下这种写法:

1.mHandler在这里是Handler的非静态内部类的一个实例,会持有外部类MainActiivty的引用

2.Handler的消息队列是在Looper线程中不断轮询处理消息,当我们的MainActivity退出
的时候,消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message又持
有mHandler的实例引用,而且,mHandler也持有外部类MainActivity的引用,导致系统
无法对MainActivity进行回收,而造成内存泄露

所以,这种写法无法保证mHandler的生命周期与MainActivity一样,很经常造成内存泄露。

而正确的写法是把Handler改成MainActivity的一个静态内部类,同时在其内部持有外部类的弱引用,这样就能解决好这个内存泄露问题:

private MyHandler mHandler = new MyHandler(this);

private static class MyHandler extends Handler{
    private WeakReference reference;
    public MyHandler(Context context){
        reference = new WeakReference(context);
    }

    @Override
    public void handleMessage(Message msg) {
        MainActivity mainActivity = (MainActivity) reference.get();
        if(mainActivity != null){
            //业务处理逻辑
        }
    }
}

2.4 尽量避免使用static变量

我们平时实际开发中,经常会使用static的变量,能够在不同的类和包中使用,但我们需要知道,static变量的还是有一些坑的:


1.占用内存,系统一般不会进行释放

2.当系统内存不够用的时候,会自动回收静态内存,这样就有可能导致我们的程序访问
某一个静态对象的时候发生不可预测的错误。

3.当Android App退出的时候,进程并没有马上退出,app的一些静态变量还存在内存中,这是不安全的。

因此,我们使用static变量的时候的,必须考虑好真的有没有必要使用static模型,一旦static使用的不合理,会造成大量的内存浪费。很多时候,我们可以在Application
里声明定义全局变量,或者使用持久化数据存储来保存全局变量。

2.5 资源未关闭造成的内存泄漏

资源未关闭的情况,这个我们平时开发中,应该会比较重视,因为特别容易出现内存泄露,最终导致程序内存溢出而崩溃,因为容易呈现,所以我们知道其必要性。

当我们使用了BroadcastReceiver,ContentObserverr,File,Cursor,Stream,Bitmap等资源的时候,使用完一定要记得及时关闭或者销毁。

例如,我们一般在某个Activity中register了某一个广播BroadcastReceiver,在Activity结束的时候没有调用unregister,这明显就会造成内存泄露。

还有的时候使用查询数据库,读取文件等一些资源型对象的时候,一定要记得调用关闭的方法。

还就是Bitmap的调用了,这个东西特别占用内存,使用完可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存。

2.6 AsnycTask造成的内存泄露

其实AsnycTask造成的内存泄露的原理与Handler是一样的,主要还是因为非静态匿名内部类持有外部类的引用,在AsnycTask的doInBackground的方法中,可能还有任务正在处理,从而导致外部类Activity不能被释放。

解决方案也可以使用静态内部类,也可以在Activity的onDestory方法中调用cancle方法,我这里就不贴代码了。


干货总结

以上介绍了我们Android开发中,经常遇到的六种内存泄露的情况,实际上内存泄露远不及这六种,牵扯到方方面面,很多时候,有些内存泄露并不能百分百的去解决,需考虑一些系统的权衡来制定方案。关于内存泄露,有些点在我们编码过程中还是需要再次重申一下:

1.恰当使用单例模式,Handler机制

2.资源对象使用完了,一定要记得关闭或者释放掉

3.老生常谈的ListView,一定要记得使用缓存convertView

4.Bitmap对象使用完了要记得调用recycle()方法来释放底层C那一块的内存

5.可能的话,将Activity的相关的context,用Application的context来替代

6.集合当中存放的对象引用,某个对象使用完了,记得从这个集合当中清理掉该引用

7.匿名内部类也要慎用,可能的话,用静态内部类替代,避免持有外部类引用而导致外部类无法被回收

8.熟悉内存泄露检查和分析的工具,如MAT,LeakCanary,Android Monitor等工具

你可能感兴趣的:(Android性能调优篇之内存泄露)