[译] Android 中由 Handler 和内部类引起的内存泄漏

原文链接:How to Leak a Context: Handlers & Inner Classes

作者:Alex Lockwood

泄露场景

在 Android 中我们经常用 Handler 来处理异步消息,把非主线程的操作结果放在主线程中更新 UI。请思考一下下面的代码有没有问题:

1
2
3
4
5
6
7
8
9
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
}

虽然看起来好像没什么问题,但是这段代码可能引起内存泄露。Android Lint 工具会给出以下警告:

In Android, Handler classes should be static or leaks might occur, Messages enqueued on the application thread’s MessageQueue also retain their target Handler. If the Handler is an inner class, its outer class will be retained as well. To avoid leaking the outer class, declare the Handler as a static nested class with a WeakReference to its outer class

但是什么东西泄漏了,内存泄漏怎么发生的我们还是不清楚。接下来我们慢慢分析泄漏的原因:

  1. 当一个 Android 应用启动时,框架会分配到一个 Looper 实例给应用的主线程。这个 Looper 的主要工作就是处理一个接着一个的消息对象。在 Android 中,所有 Android 框架的事件(比如 Activity 的生命周期方法的调用和按钮的点击等)都是放到消息中,然后加入到 Looper 要处理的消息队列中,由 Looper 依次处理。主线程的 Looper 的生命周期和应用的一样长。

  2. 当在主线程中初始化一个 Handler 时,它就会关联到 Looper 的消息队列。发送到消息队列的消息本身就持有 Handler 的引用,只有这样 Looper 在处理这个条消息的时候才能调用 Handler#handleMessage(Message) 处理消息。

  3. 在 Java 中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用,而静态的内部类则不会。

上面的代码还是很难察觉哪里会导致内存泄漏,那么再看看下面这个例子:

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 Activity {
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();
}
}

当 Activity 是 finished 的时候,被延迟的消息在被处理之前依然会存活在主线程的消息队列中 10 分钟。这个消息持有 Activity 的 Handler 的引用,而 Handler 隐式地持有其外部类(SampleActivity)的引用。这些引用会一直保持直到消息被处理完成,而这会导致 Activity 无法被垃圾回收器回收并且泄漏应用的所有资源,因此就产生了内存泄漏。

注意上面的匿名 Runnable 类也隐式地持有 SampleActivity 的引用,这也会阻止 Context 被回收。

解决方法

要解决这个问题,在继承 Handler 的时候要么放在一个单独的类文件中,要么用静态内部类替代。因为静态内部类不会持有其外部类的隐式引用,所以不会产生 Context 泄漏。如果要在 Handler 中调用外部 Activity 的方法,可以持有 Activity 的弱引用。同样的道理,要解决匿名 Runanable 类引起的内存泄漏,只需要把它设为静态的成员属性(静态的匿名内部类不会持有其外部类的隐式引用)。

修改后的代码如下:

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 Activity {
/**
* 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();
}
}

静态内部类和非静态内部类的区别是我们每一个 Android 开发人员应该掌握的。当内部类的实例的生命周期可能大于 Activity 的生命周期时,避免使用非静态的内部类。个人更倾向于用静态内部类或持有 Activity 的弱引用。

END
Johnny Shieh wechat
我的公众号,不只有技术,还有咖啡和彩蛋!