Android中常见的内存泄漏及解决方法

从毕业到现在已经做了好几个项目,在项目中经常遇到内存泄漏的问题,于是就想整理开发中比较常见的内存泄漏的问题,查阅,借鉴网上各种博客列举的案例和解决办法,自己整理成笔记方便以后参考。内存泄漏简单的说,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收。


##集合类泄漏
在Android开发中经常使用集合类,集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是静态变量 ,如果没有相应的删除机制,很可能导致集合所占用的内存只增不减,最终导致内存泄漏,我们应该在onDestroy() 将集合清空,并将置为null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SimpleActivity extends AppCompatActivity {
private HashMap<String,Object>sMap=new HashMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple);
for (int i = 0; i <10 ; i++) {
Object obj=new Object();
sMap.put(""+i,obj);
}
}
@Override
protected void onDestroy() {
sMap.clear();
sMap=null;
super.onDestroy();
}
}

##单例造成的内存泄漏

由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
public class App {
private static App instance;
private Context context;
private App(Context context) {
this.context = context;
}
public static App getInstance(Context context) {
if (instance == null) {
instance = new App(context);
}
return instance;
}
}

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。

2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

正确的方式应该改为下面这种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class App {
private static App instance;
private Context context;
private App(Context context) {
this.context = context.getApplicationContext();// 使用Application 的context
}
public static App getInstance(Context context) {
if (instance == null) {
instance = new App(context);
}
return instance;
}
}

或者这样写,连 Context 都不用传进来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context,
...
context = getApplicationContext();
...
/**
* 获取全局的context
* @return 返回全局context对象
*/
public static Context getContext(){
return context;
}
public class App {
private static App instance;
private Context context;
private App() {
this.context = MyApplication.getContext();// 使用Application 的context
}
public static App getInstance() {
if (instance == null) {
instance = new App();
}
return instance;
}
}

##匿名内部类/非静态内部类和异步线程

非静态内部类创建静态实例造成的内存泄漏

有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SimpleActivity extends AppCompatActivity {
private static TestResource sResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple);
if(sResource == null){
sResource = new TestResource();
}
//...
}
class TestResource {
//...
}
}

这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为:

将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下:

其中: NO1表示 Application 和 Service 可以启动一个 Activity,不过需要创建一个新的 task 任务队列。而对于 Dialog 而言,只有在 Activity 中才能创建

##匿名内部类

android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends AppCompatActivity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}

ref1和ref2的区别是,ref2使用了匿名内部类。我们来看看运行时这两个引用的内存:

可以看到,ref1没什么特别的。

但ref2这个匿名类的实现对象里面多了一个引用:

this$0这个引用指向MainActivity.this,也就是说当前的MainActivity实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。

###Handler 造成的内存泄漏

Handler 的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。

由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SampleActivity extends AppCompatActivity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}

在该 SampleActivity 中声明了一个延迟10分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class SampleActivity extends AppCompatActivity {
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}

综述,即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。

前面提到了 WeakReference,所以这里就简单的说一下 Java 对象的几种引用类型。

Java对引用的分类有 Strong reference, SoftReference, WeakReference, PhatomReference 四种。

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:

首先定义一个HashMap,保存软引用对象。

1
private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();

再来定义一个方法,保存Bitmap的软引用到HashMap。

使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。

另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

ok,继续回到主题。前面所说的,创建一个静态Handler内部类,然后对 Handler 持有的对象使用弱引用,这样在回收时也可以回收 Handler 持有的对象,但是这样做虽然避免了 Activity 泄漏,不过 Looper 线程的消息队列中还是可能会有待处理的消息,所以我们在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列 MessageQueue 中的消息。

下面几个方法都可以移除 Message:

1
2
3
4
5
6
7
8
9
public final void removeCallbacks(Runnable r);
public final void removeCallbacks(Runnable r, Object token);
public final void removeCallbacksAndMessages(Object token);
public final void removeMessages(int what);
public final void removeMessages(int what, Object object);

##尽量避免使用 static 成员变量

如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。

这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,yi’wei如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了电量、流量,你的app不得不被用户卸载或者静默。

这里修复的方法是:

不要在类初始时初始化静态成员。可以考虑lazy初始化。
架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

##避免 override finalize()

1、finalize 方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是:
虚拟机调用GC的时间不确定
Finalize daemon线程被调度到的时间不确定

2、finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了 finalize 方法,再次被 GC 时也不会再执行了,原因是:

含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该Object的,而在 finalize 方法执行的时候,该 object 所对应的 finalize Reference 会被释放掉,即使在这个时候把该 object 复活(即用强引用引用住该 object ),再第二次被 GC 的时候由于没有了 finalize reference 与之对应,所以 finalize 方法不会再执行。

3、含有Finalize方法的object需要至少经过两轮GC才有可能被释放。

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

对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

注册(监听)和取消注册成对使用

对于一些第三方库如 EventBus ,ButterKnife….等。例如EventBus 的在onCreate使用register注册,在onDestroy使用unregister解绑释放资源。

##总结

  1. 对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部静态对象引用而泄露。

  2. 尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。

  3. 对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:

    将内部类改为静态内部类
    静态内部类中使用弱引用来引用外部类的成员变量
    
  4. Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable.

  5. 在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的集合(使用 map.clear() ; map = null)等,最好遵循谁创建谁释放的原则。

  6. 正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。

  7. 对EventBus ,ButterKnife….等注册监听和解绑监听成对使用

热评文章