Android性能调优篇之内存泄露

作者: 进击的欧阳 | 来源:发表于2017-08-15 14:00 被阅读148次

    开篇废话

    通过我之前的两篇文章

    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,具体操作截图如下:

    触发GC触发GC

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

    导出堆状态导出堆状态

    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,也就是存在内存泄露

    内部类分析内部类分析

    这种情况,我们需要把匿名内部类修改为静态内部类,静态内部类,这样静态内部类就不会持有外部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<Context> reference;
        public MyHandler(Context context){
            reference = new WeakReference<Context>(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性能调优篇之内存泄露

        本文链接:https://www.haomeiwen.com/subject/khamrxtx.html