Android 中常见的内存泄漏总结

在 Android 开发中,稍有不慎就容易引起内存泄漏,我们经常听到内存泄漏,但是什么是内存泄漏呢?

内存泄漏:无用对象持续占用内存使得内存资源得不到及时释放,导致内存资源被浪费。

可以看到内存泄漏是由于没有及时释放内存资源造成的,因此在不经意间我们就有可能造成了内存泄露。本文我们就来汇总下平时的编码过程中容易发生内存泄漏的地方并举例加以说明以及给出解决措施。

1. 集合类造成的内存泄露

1
2
3
4
5
6
Vector v = new Vector(10);
for (int i = 1; i<100; i++) {
Object o = new Object();
v.add(o);
o = null;
}

上面这一例子是典型的集合类造成的泄漏。

这个例子中循环申请了 Object 对象,虽然最后将每个对象进行了释放,但是 Vector 依然持有该对象的引用,这对于 GC 来说是不能进行回收的。

所以,避免该方式的内存泄漏最简单的方法是将 Vector 置为空(v = null)

2. 单例造成的内存泄漏

单例模式在我们编写代码的过程中经常会用到,但是使用不慎也是极其容易引起内存泄漏。

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

上面这个例子是一个普通的单例模式,我们在创建的时候传入了 Context,但是对于传入的 Context 是非常有讲究的,一不小心就很可能造成内存泄漏。

  1. 传入 Application 的 Context。因为 Application 的生命周期就是整个应用的生命周期,所以这不会有问题。
  2. 传入 Activity 的 Context。单传入的 Activity 要退出的时候,由于单例对象是静态的,其生命周期就是整个应用的生命周期,导致 Activity 在退出的时候单例对象一直持有 Activity 的引用使得 Activity 不能被回收,因而便造成了内存泄漏。

避免内存泄漏的方式可以参考下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在应用的 Applicaiton 中添加一个静态方法
public static Context getContext() {
return getApplicationContext();
}
*****************************************************************
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager() {
this.context = App.getContext();// 使用Application 的context
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}

这样创建的单例对象可直接调用 Application 的 Context,避免了内存泄漏。

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

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

有时候某个 Activity 会被频繁启动,为了避免资源重复创建,就可能出现上面这种写法。

我们知道非静态内部类会默认持有外部类的引用。因此,这个 TestResource 会默认持有 MainActivity 的引用,而该非静态内部类又创建了一个静态实例,所以该实例的生命周期和应用一样长。如果我们要销毁这个 MainActivity,这个静态实例会持有 MainActivity 的引用,导致 MainActivity 的资源无法回收,造成了内存泄漏。

正确的做法是将该内部类设为静态内部类,这样静态内部类就不持有外部类的引用了,避免了内存泄漏。

4. 匿名内部类造成的内存泄漏

这种情况下造成的内存泄漏和上一种分析的状况有些类似,先看下面这一例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleOne();
}
private void exampleOne() {
new Thread() {//匿名内部类,非静态的匿名类会持有外部类的一个隐式引用
@Override
public void run() {
...
}
}.start();
}
}

非静态匿名内部类会默认持有外部类的引用(这和上一个例子中提到的非静态内部类会默认持有外部类的引用是一样的),因此这个新创建的线程会持有 MainActivity 的引用。而如果要销毁这个 Activity 之前,线程还在运行的话就会造成该线程持有 MainActivity 的引用,造成 MainActivity 的资源无法回收导致内存泄漏。

针对上述情况有两种解决方法。

  1. 把非静态的线程匿名类定义成静态的内部类,这样静态的内部类就不会持有外部类的隐式引用。
  2. 在销毁 MainActivity 的 onDestroy() 方法中结束运行的线程。

5. Handler 造成的内存泄漏

我们为了避免发生 ANR 而不在主线程进行耗时操作经常要用到 Handler,但是使用的时候极有可能造成内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends Activity {
private final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60);
finish();
}
}

例子中在 MainActivity 中声明了一个延迟一分钟的 Message。当我们 finish() 掉 MainActivity 的时候,如果 MeassageQuene 里面还有 Message,那个延迟执行的 Message 会继续存在主线程中,它会持有 handler 的引用,而因为 handler 是非静态内部类,它默认持有 MainActivity 的隐式引用,从而造成了内存泄漏。

解决方法应该是把 handler 声明为静态,这样就不持有 MainActivity 的引用了。同时通过弱引用的方式引入 Activity,具体代码如下:

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
public class MainActivity extends Activity {
private static class MyHandler extends Handler {
private final WeakReference<MainActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private final MyHandler handler = new MyHandler(this);
private static final Runnable mRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handler.postDelayed(mRunnable, 1000 * 60);
finish();
}
}

这里提到了 WeakReference,再贴个图了解下其他几种引用类型。

Java引用类型

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

这种类型的内存泄漏时最直接的。比如对于使用了 BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap,监听器等资源,应该在 Activity 销毁之前及时关闭或者注销,否则就会导致资源不会被回收造成内存泄漏。

总结

本文罗列了 Android 常见的几种内存泄漏情况,如下。

1
2
3
4
5
6
1. 集合类造成的内存泄漏
2. 单例造成的内存泄漏
3. 非静态内部类创建静态实例造成的内存泄漏
4. 匿名内部类造成的内存泄漏
5. Handler 造成的内存泄漏
6. 资源未关闭造成的内存泄漏

内存泄漏的发生场景可以为如下一句话:长生命周期的对象持有短生命周期对象的引用且该引用不能被回收。

因此在写代码的时候要保持对生命周期的敏感,尤其注意单例、静态对象、全局性集合等的生命周期。