一、概述
Android内存泄露是所有开发者都会遇到的问题,内存泄露的本质原因是因为本该被回收的对象因为被其他对象持有其引用,从而造成无法被回收继续占用内存;Android为每一个应用程序分配了一定的内存空间,当由于内存泄露造成应用程序占用内存越来越大直到没有多余的内存分配时,就会造成OOM,即内存溢出,当程序发生内存溢出时,会直接崩溃,后果是很严重的。下面我们来分析一下造成内存泄露的原因和解决办法。
二、内存泄露的本质原因
我们知道,Android的内存回收是基于java的垃圾回收机制的,一般来说java gc会自动回收掉无用内存,理应不会出现内存泄露,造成内存泄露的原因仅仅是因为外部原因(开发者没有处理好对象的生命周期),即其他生命周期更长的对象持有本该被回收对象的引用,造成本该被回收的对象无法被回收,只有当持有其引用的对象被回收后,它才能被回收。
三、常见的内存泄露原因以及解决办法
根据上面,我们知道,造成内存泄露的原因是因为本该被回收的对象被其他生命周期更长的对象所引用,造成对象无法被回收,所有要解决内存泄露,最根本的办法就是切除引用关系,下面我们分析一下常见的内存泄露例子。
1、回收对象被集合引用
原因:当添加集合元素到集合中时,集合会持有该集合元素的引用,如果集合对象的生命周期大于集合元素的生命周期,那么即使将集合元素设置为空,集合元素实例还是不会被回收。例如:
List<Person> personList = new ArrayList<>();
Person personOne = new Person("张三");
Person personTwo = new Person("李四");
personList.add(personOne);
personList.add(personTwo);
personOne = null; // 即使将personOne设置为null,personOne所指向的对象实例也不会被回收,因为在personList中还持有对象实例的引用
解决办法:如果想回收集合元素,需要将集合元素从集合中移除,并将集合元素设置为空,切除引用关系。
List<Person> personList = new ArrayList<>();
Person personOne = new Person("张三");
Person personTwo = new Person("李四");
personList.add(personOne);
personList.add(personTwo);
personList.remove(personOne); // 将需要回收的对象从集合中移除,使集合不持有其引用
personOne = null;
2、回收对象被静态对象引用
原因:静态对象的生命周期是全局的,和应用程序的生命周期保持一致,如果某个对象被静态对象所引用,那么它的生命周期也将变成全局的,无法被系统回收。例如:
public ClassName {
private static Context context;
public static void setContext(Context a_context) {
context = a_context;
}
}
setContext(activity);
上面程序静态对象context将持有activity实例的引用,因为静态对象的生命周期是全局的,即使activity的生命周期结束之后,activity实例也不会被回收,从而造成内存泄露。
由于被静态对象引用造成内存泄露的一个典型的例子是单例模式,如下:
public class Utils {
private static Utils utils = null;
private Context context;
private Utils(Context context) {
this.context = context;
}
public static Utils getInstance(Context context) {
if (utils == null) {
utils = new Utils(context);
}
return utils;
}
}
如果构造单例对象传入的context是一个activity对象,由于静态对象utils的成员context持有activity的引用,即使activity的生命周期结束,activity也不会被回收,特别对于一些比较大的activity,很容易造成OOM。
解决办法:
a、使用弱引用(WeakReference)替代强引用持有实例;
b、如果对象是Context,可使用Application的context,Application的context生命周期是全局的,和应用保持一致。
3、非静态内部类或者匿名类
原因:非静态内部类和匿名类默认是持有外部类的引用的,如果非静态内部类或者匿名类对象的生命周期大于外部类对象的生命周期,就会造成外部类对象无法被回收,从而产生内存泄露。例如:
public class MainActivity extends Activity
private static InnerClass innerClass = null;
public class InnerClass {
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (innerClass == null) {
innerClass = new InnerClass(); // 非静态内部类默认持有外部类的引用
}
上面的代码中,静态对象innerClass的生命周期是全局的,并且由于它是一个非静态内部类的对象,默认持有外部类的引用,所以即使activity的生命周期结束后,activity还是会被静态对象innerClass所引用,从而造成内存泄露。
解决办法:
1、将非静态内部类声明为static,静态内部类没有持有外部类的引用;
2、尽量避免非静态内部类的实例为静态
非静态内部类或者匿名类造成内存泄露的典型实例是使用多线程的情况,我们使用多线程时,很多时候会将线程类定义为非静态内部类或者匿名类,而一旦线程的生命周期大于外部类的生命周期时,就会造成内存泄露,例如:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread() {
@Override
public void run() {
try {
/**
* 线程休眠10秒
* 由于定义的线程类是一个匿名类,默认持有外部类的引用,所以当该线程结束之前,
* 即使外部类Activity的生命结束了,Activity实例也不会被回收
*/
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
解决办法:
1、将线程类声明为静态内部类
2、在activity的onDestory方法中强制结束线程
如下:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mThread = new Thread() {
@Override
public void run() {
try {
/**
* 线程休眠10秒
* 由于定义的线程类是一个匿名类,默认持有外部类的引用,所以当该线程结束之前,
* 即使外部类Activity的生命结束了,Activity实例也不会被回收
*/
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
mThread.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
mThread.stop();
}
或者
public static class MyThread extends Thread {
@Override
public void run() {
try {
/**
* 线程休眠10秒
* 由于定义的线程类是一个匿名类,默认持有外部类的引用,所以当该线程结束之前,
* 即使外部类Activity的生命结束了,Activity实例也不会被回收
*/
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4、使用资源未释放
原因:对于有些资源,我们再使用之后需要调用其回收方法进行回收,否则资源将不会被回收,从而造成内存泄露,比如,广播(BroadCast Receiver),文件(InputStream
),数据库游标(Cursor)、图片资源(Bitmap)等。
解决办法:在需要销毁时调用其回收或者关闭方法,表示不再使用该资源
bitmap.recycle();
inputStream.close();
cursor.close();
unregisterReceiver(broadcastReceiver);
5、使用Handler造成的内存泄露
四、内存泄露分析工具
即使了解了造成内存泄露的原因,也难免会造成内存泄露,这种时候我们可以借助工具来分析内存泄露的原因,我经常使用的内存泄露工具有两个,一个是LeakCanary和Android stuido自带的Memory monitor
1、LeakCanary
LeakCanary是一个检查Android内存泄露的开源框架,我们需要在程序代码中集成该框架,它的集成很简单,分为两个步骤:
a、在build.gradle中依赖leakcanary
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.2'
}
b、自定义应用的Application类,在onCreate方法中加入leakcanary的监控
public class LeakApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {//1
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}
集成之后,当出现内存泄露时,leakcanary会给出提示
2、Memory Monitor
Memory Monitor是Android Studio自带的内存分析工具,可以监控实时监控程序的内存变化以及内存的占用情况,可以具体到某个对象内存的占用情况,下面是使用截图:
五、总结
1、Android内存泄露的本质原因是因为本应该被回收的对象被其他生命周期更长的对象持有其引用,导致java GC无法回收该对象占用的内存;
2、解决内存泄露的根本办法在于解除需要被回收对象的引用关系;
3、常见的内存泄露
a、回收对象被集合引用
b、回收对象被静态对象引用(静态对象的生命周期是全局的)
c、使用非静态内部类或者匿名内部类(默认持有外部类的引用)
d、使用多线程(使用非静态内部类或者匿名类的方式构造线程类,如果线程的生命周期大于外部类的生命周期,则会造成内存泄露)
e、使用Bitmap、动态广播、文件流、数据库游标等资源,在结束后未关闭资源
f、使用Handler导致的内存泄露
4、内存泄露的分析工具
a、leackcanary
b、memory monitor