开篇废话

通过我之前的两篇文章

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

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 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,一定要记得使用缓存convertView4.Bitmap对象使用完了要记得调用recycle()方法来释放底层C那一块的内存5.可能的话,将Activity的相关的context,用Application的context来替代6.集合当中存放的对象引用,某个对象使用完了,记得从这个集合当中清理掉该引用7.匿名内部类也要慎用,可能的话,用静态内部类替代,避免持有外部类引用而导致外部类无法被回收8.熟悉内存泄露检查和分析的工具,如MAT,LeakCanary,Android Monitor等工具

更多相关文章

  1. Nginx系列教程(四)| 一文带你读懂Nginx的动静分离
  2. Android为什么推荐使用SparseArray来替代HashMap?
  3. Mac上用于Cocos2dx的Poco静态库编译(支持Android和IOS)
  4. Android中内存泄露的原因分析:
  5. Android(安卓)2.1 中 JNI 层 camera 的应用
  6. DDMS的使用、内存溢出的调试和模拟器的启动命令参数
  7. Android(安卓)APP内存优化之图片优化
  8. Android(安卓)自绘动画实现与优化实战——以 Tencent OS 录音机
  9. Android(安卓)Training - 管理应用的内存

随机推荐

  1. 浏览器中实现深度学习?有人分析了7个基于J
  2. 联邦学习 OR 迁移学习?No,我们需要联邦迁移
  3. python 基础语法1
  4. mfs changelog日志解析
  5. crmeb 多商户系统 首页精品推荐 推荐单品
  6. crmeb 多商户系统 首页精品推荐 推荐单品
  7. 2021年北京高新技术企业认定条件及时间
  8. 使用频率最高的21条Linux命令,让你多出一
  9. 《都挺好》弹幕比剧还精彩?394452条弹幕数
  10. 北京高新技术企业认定最新条件及相关要求