Handler 源码解析(三)—— Handler 内存泄漏
Handler 源码解析系列文章:
1. 匿名内部类是导致 Handler 内存泄漏的本质原因吗?
很多人说,导致 Handler 内存泄漏的原因是:如果 Handler 发送了一个延迟很长时间或者周期性的消息,而在消息处理前 Activity 已经被销毁,Handler 仍然持有对 Activity 的引用,可能导致内存泄漏。
我们都知道匿名内部类会持有外部类的引用,当我们在 Activity 中创建如下 Handler 实例时,会提示有内存泄漏风险:
那么导致该风险的根本原因是匿名内部类持有外部类的引用吗?
我们再看一个例子:
我们经常使用匿名内部类给控件添加点击事件,但在这里从未出现内存泄漏风险提示,也从未见过谁分析此处会存在内存泄漏的风险。可以看出,导致 Handler 出现内存泄漏的本质原因并不是匿名内部类持有外部类的引用。根据这个,我们仅仅可以知道 Handler 对象持有了 Activity.this。
根据可达性分析,被 GCRoots 直接或间接引用的对象是不可以被回收的。那 Handler 出现内存泄漏时,一定是被某个 GCRoots 直接或间接引用着。
回过头来,我们可以得出结论:匿名内部类会持有外部类的引用,但外部类释放时,匿名内部类也会被释放,这并不是导致 Handler 发生内存泄漏的本质原因,但可以作为一个间接原因。
那么GCRoots 又是谁,下文接着分析。
2. Handler 内存泄漏原因
2.1 在主线程中创建 Handler 对象
在主线程中创建 Handler 对象:
// MainActivity.java
Handler handler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
// 修改 TextView 内容
}
};
new Thread(new Runnable() {
@Override
public void run() {
Message msg = Message.obtain();
...
handler.sendMessageDelayed(msg, 20000);
}
}).start();
然后在子线程中发送一个延迟消息,立刻销毁 MainActivity ,会发生内存泄漏。 操作手顺:
- 点击按钮,打开 MainActivity 页面。
- 在 20s 内销毁 MainActivity 页面。
- 手动 GC。
通过 Profiler 进行分析,会发现内存发生泄漏,引用链:
2.1.1 sMainLooper 作为 GCRoot
通过匿名内部类持有外部类的对象,我们可以知道Handler 持有了 MainActivity.this。
在上一篇文章中,我们提到 Handler 中消息入队方法 enqueueMessage()
:
// Handler.java
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
第4行处,msg.target
引用了 Handler 的对象。也就是 Message 的对象 msg 持有了 handler 的引用。
第10行处,调用了 MessageQueue 的 enqueueMessage()
方法并传入了 msg:
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
synchronized (this) {
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
消息被添加至消息队列后,MessageQueue 中的 mMessages 会有对该消息的引用,所有待处理的消息被组织成一个单向链表使用 next 属性来指示下一个消息的位置。MessageQueue 对象持 有了 msg 的引用。
MessageQueue 又被谁持有呢,在 Handler 的构造函数中:
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
第3行处,根据 mQueue = mLooper.mQueue
,推测应该在 Looper 中进行了赋值,接着看 Looper 的构造函数:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
可以发现,在创建 Looper 对象时,同时创建了一个 MessageQueue 实例。 Looper 对象持有了 MessageQueue 对象。同时可以发现,Looper 中的 mQueue 为 final 对象,Looper 对应的 mQueue 不可以被修改:
// Looper.java
final MessageQueue mQueue;
Looper 对象又被谁持有了呢?查看 ActivityThread 中的 main() 方法,其中:
public static void main(String[] args) {
...
Looper.prepareMainLooper();
...
Looper.loop();
...
}
第3行,Looper.prepareMainLooper()
,进一步查看 Looper 的 prepareMainLooper() 方法:
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
第 2 行,通过 prepare 方法创建了 Looper 对象,在第 7 行,sMainLooper 持有了创建的 Looper 对象。
// Looper.java
private static Looper sMainLooper;
sMainLooper 是 static 修饰的,就是我们所说的 GCRoot。综上,存在如下引用链:
2.1.2 活动中的线程作为 GCRoots
其实还存在另外一条引用链, 查看prepareMainLooper()
的 prepare()
方法:
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
第5行,new 出一个 Looper 对象并调用了 ThreadLocal 的 set 方法:
// ThreadLocal.java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
// ThreadLocal.java
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// Thread,java
ThreadLocal.ThreadLocalMap threadLocals = null;
通过 set 方法可以发现,Looper 对象放到了 ThreadLocalMap 中,而第4行的 ThreadLocalMap 对象,是通过当前线程获得的。当前 thread 持有 ThreadLocalMap 对象。ThreadLocalMap 对象通过 Entry 持有 Looper 对象。
这里有个容易搞错的点,Looper 对象是 Entry 节点中的一个 value,并不是被 sThreadLocal 持有。通过 sThreadLocal.set(new Looper(quitAllowed))
将 Looper 对象作为 value 值添加到 ThreadLocalMap 的 Entry中:
// ThreadLocal.java ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {
...
tab[i] = new Entry(key, value);
...
}
// ThreadLocal.java ThreadLocalMap
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
我们可以发现,Entry 通过弱引用去引用 key 值(ThreadLocal 对象),通过强引用去引用 value 值(Looper 对象)。
所以 ThreadLocal.ThreadLocalMap 通过 Entry 强引用了 Looper 对象。而当前 Thread 持有了 ThreadLocalMap 对象。
活动的线程也是 GCRoot ,不能被回收。如果线程一直处于运行中,则一直会存在如下引用链:
2.2 在子线程中创建 Handler 对象
// 仅作为测试代码
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
handler = new Handler(Looper.myLooper()){
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.what == 1){
binding.text.setText("111111111");
}
}
};
handler.sendEmptyMessageDelayed(1, 20000);
Looper.loop();
}
}).start();
同样发送延迟消息,只不过本次的 Handler 对象是在子线程中创建的,子线程中的 Looper 对象是通过第5行调用 Looper.prepare()
直接创建的。
与主线程不同的是,这次不会调用 prepareMainLooper()
方法了,自然也就不存在以 sMainLooper 为 GCRoot 的引用链。另一条引用链同主线程分析时一样,存在。只要创建 Looper 对象的线程存在,就会存在如下引用链,从而导致内存泄漏:
上述的引用关系会一直保持,直到 Handler 消息队列中的所有消息被处理完毕。在 Handler 消息队列还有未处理的消息 / 正在处理消息时,此时若需销毁外部类 MainActivity ,但由于上述引用关系,垃圾回收器(GC)无法回收MainActivity,从而造成内存泄漏。
造成内存泄露的两个关键条件:
- 存在 “未被处理 / 正处理的消息 -> Handler 实例 -> 外部类” 的引用关系
- Handler 的生命周期 > 外部类的生命周期
3. Handler 内存泄露的解决方案
3.1 静态内部类 + 弱引用
将Handler
的子类设置成静态内部类。静态内部类不持有外部类的引用。此外,可使用 WeakReference 弱引用持有外部类,保证外部类能被回收。
public class MainActivity extends AppCompatActivity {
private UIHandler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new UIHandler(this, Looper.myLooper());
new Thread(new Runnable() {
@Override
public void run() {
Message msg = Message.obtain();
msg.what = 1;
msg.obj = "Hello";
handler.sendMessage(msg);
}
}).start();
}
// 设置为:静态内部类
private static class UIHandler extends Handler{
// 定义弱引用实例
private final WeakReference<Activity> mReference;
// 在构造方法中传入需持有的Activity实例
public UIHandler(Activity activity, Looper looper) {
super(looper);
// 使用 WeakReference 弱引用持有 Activity 实例
mReference = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
...
break;
case 2:
...
break;
}
}
}
}
3.2 清空消息队列
当外部类结束生命周期时,清空Handler内消息队列
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
最终会调用 MessageQueue 中的 removeCallbacksAndMessages()
:
void removeCallbacksAndMessages(Handler h, Object object) {
if (h == null) {
return;
}
synchronized (this) {
Message p = mMessages;
// Remove all messages at front.
while (p != null && p.target == h
&& (object == null || p.obj == object)) {
Message n = p.next;
mMessages = n;
p.recycleUnchecked();
p = n;
}
// Remove all messages after front.
while (p != null) {
Message n = p.next;
if (n != null) {
if (n.target == h && (object == null || n.obj == object)) {
Message nn = n.next;
n.recycleUnchecked();
p.next = nn;
continue;
}
}
p = n;
}
}
}
该函数中为什么进行两次循环?
一个线程中,消息队列与 Handler 实例的比例为 1:n。如下图所示,一个消息队列中的消息可能由不同的 Handler 对象发送过来的,而 mHandler.removeCallbacksAndMessages(null)
移除的是指定 Handler 对象对应的消息。
若当前消息队列队头消息 mMessages 为想要清空 Handler 对象所发出的,则进行第一次循环,否则进行第二次循环。
转载自:https://juejin.cn/post/7336858258776653862