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