深入学习、理解 Handler 有助于在开发过程中减少内存泄漏、尽量减少内存抖动
本篇主要内容:
- Handler 源码整体框架
- Handler 实现线程间通信的方案思想
Handler 的作用是什么
这个问题就相当于是问 Handler 是什么。众所周知,Handler 作用于线程之间的通信,那么为什么线程之间通信需要用到 Handler 呢?
例1:
先来看一段伪代码:
首先如图分别有线程一和线程二,线程一给 message 赋值,然后休眠 1s,线程二读取 message,这样做在线程二可不可以读取到 message 呢?当然可以。但是这种方式依赖于全局变量。
这里因为 message 是全局变量,线程对于全局变量是共享的。这种方式虽然可以做到线程间的通信,但实际中经常需要在多个线程之间进行切换,所以 Handler 实际上解决的并不是线程之间通信问题,而是线程之间切换的问题。
紧接着,当线程二接收到线程一表白的消息之后,需要对线程一做出回应,把自己的心意传达给对方:
直接回应当然传达不到线程一。
因此,在线程切换的同时,Handler 又涉及到了一个 Message 机制,这个 Message 机制又顺带解决了 Handler 之间通信的问题。
小结:Handler 并不是为了解决线程之间通信的问题,而是解决线程切换的问题。
Handler 是如何解决线程切换的问题
只要是进行线程的切换,不管怎样最终都会演变成 Handler 的这种架构。
例2:
先来看一段伪代码:
线程一对线程二表白后,就会不停的循环等待线程二的回应,休眠 1s 后线程二回应了 status=true,由于线程一不断的等待终于等来了线程二的回应 true,开始执行 happy(),此时 happy() 方法运行在线程一里面。
Handler 的原理是什么
面试常问:Looper.loop() 是否还会继续往下执行(是否会产生阻塞)
和前面例2的代码差不多,写个简单的代码验证一下:
先点击 btn1 实例化 Handler,再调用 sendMessageToThreadHandler() ,并没有弹出 loop结束,也没有因为在子线程弹 toast 而崩溃。
由此可见,Looper 的作用等价于 for( ; ; ){}。
Handler 中的四大天王
Handler 中最重要的四个角色,分别是 Handler、Message、MessageQueue、Looper。
这里以电子厂的流水线来举例这四个角色的作用分别是什么:
首先左边有一个传送带,右边是源源不断来的货物。现在需要把货物依次放在传送带上,这时这个放的动作,就需要一个类似于 put 方法,这个方法就是 sendMessage();而当这个货物传到左边的时候,需要一个类似于 get 的方法,把这个货物放到对应的地方,这个方法就是 handlerMessage(),这两个方法都需要一个类来承载,他就是 Handler。
而这个货物需要放在传送带上才能把货物源源不断的从右边运到左边,这个传送带就是 MessageQueue。
那这个传送带能自己跑吗?当然不能,因为他没有电机和电源。Looper 的作用就是这个电机和电源。
小结一下四大天王:
Handler:提供一个放的方法 sendMessage() 和一个取的方法 handlerMessage()
Message:货物
MessageQueue:传送带
Looper:电机
电机旋转,源源不断带动传送带运行,传送带是队列,如果没有人去取他,也运行不了。货物按照先来先到的顺序依次被运输到左边。有的货物加急需要马上就运到左边,有的消息需要延迟一段时间才运到左边,这些货物的顺序都是通过 MessageQueue 来维持他们的顺序的。
Looper.loop() 就相当于电机,Thread 就相当于电源。
Handler 是怎样把消息加入到队列的
在下一章中会深入分析 Handler 的源码,这里只是简单介绍下。
首先从发送消息的地方作为切入点进行分析:
我们调用 sendMessage(),马上回调用 sendMessageDelayed():
sendMessageDelayed() 又会调用 sendMessageAtTime():
接着 sendMessageAtTime() 会调用 enqueueMessage():
追踪源码,不管怎么发消息,条条大路通罗马,最终都会执行到 enqueueMessage() 这个方法。那么这个 enqueueMessage() 是干嘛的呢?前面举例分析了 Handler 的作用之一就是提供方法往传送带(MessageQueue)上放货物,所以这里 enqueueMessage() 方法最终拿到 MessageQueue 的对象,调用 MessageQueue.enqueueMessage() 方法,把消息放进传送带。
手写 Handler 源码
在实际开发过程中,我们一般都是这么用 Handler:
sendMessage() 之后,马上就到 handleMessage() 收到了消息。那么他是怎么从本章的例2一步一步演进过来的呢?
新建一个 Java Library,先创建一个空的 Message 类,再创建 Handler 类,创建两个方法,一个用于发送消息,一个用于处理消息:
public class Handler {
public void sendMessage(Message msg){
}
public void handleMessage(Message msg){
}
}
新建一个 ActivityThread,作为程序的入口:
public class ActivityThread {
public static void main(String[] args) {
}
}
第一步,首先看看怎么去使用。先给 Message 定义一个 String 类型的变量:
public class Message {
String obj;
public Message() {
}
public Message(String obj) {
this.obj = obj;
}
}
当调用 sendMessage 的时候暂时先马上去调用 handleMessage:
public void sendMessage(Message msg){
handleMessage(msg);
}
new 一个 Handler,重写 handleMessage ,再调用 sendMessage 发送一个消息:
public class ActivityThread {
public static void main(String[] args) {
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
System.out.println("--->" + msg.obj);
}
};
handler.sendMessage(new Message("---------weijun---------"));
}
}
运行 main 函数:
以上示例抛弃了传送带、电源点击,把货物放进去之后就马上取出来,存和取是同时发生的。那如果同时有一万条消息,但是每秒钟只能处理一百条,或者前一条消息没处理好,会造成什么问题?
1.当大量消息来的时候会造成阻塞 OOM
2.不能做到线程通信 + 线程切换
在多线程的环境下,这种速率不一致的问题通常都存在。而 Handler 是一个典型的生产者与消费者的模式,正好可以解决这个问题。
第二步,创建一个队列 MessageQueue,其中维护一个阻塞队列 BlockingQueue。根据之前源码分析,消息最终通过 enqueueMessage() 放进队列,通过 next() 从队列中取出:
public class MessageQueue {
BlockingQueue<Message> queue = new ArrayBlockingQueue<>(100);
public void enqueueMessage(Message msg){
try {
queue.put(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public Message next(){
Message msg = null;
try {
msg = queue.take();
}catch (InterruptedException e){
e.printStackTrace();
}
return msg;
}
}
由于是生产者与消费者模式,那必定会有两个线程协作,一个存,一个取。因此,在 Handler 中维护一个队列 MessageQueue,要使这个传送带(MessageQueue)不停的运行,需要一个死循环的方法 looper(),在 looper() 里面把消息取出来通过 handleMessage() 处理,在 sendMessage() 的时候把消息放进队列:
public class Handler {
MessageQueue messageQueue = new MessageQueue();
public void looper(){
for (;;){
Message msg = messageQueue.next();
handleMessage(msg);
}
}
public void sendMessage(Message msg){
messageQueue.enqueueMessage(msg);
}
public void handleMessage(Message msg){
}
}
接下来现在主线程里面通过 sendMessage 去往队列里面发消息,专门有个地方遍历这个队列(looper方法),这是一个典型的生产者与消费者模型。sendMessage 是生产者,他会不断生产消息加入到队列,由于之前就已经在不断的进行循环了,那这个队列不会被阻塞,就会取到这个数据,取到后调用 handleMessage,由于 handleMessage 被重写了,所以会交给重写方进行操作。
public class ActivityThread {
public static void main(String[] args) {
final Handler[] handler = {null};
new Thread(){
@Override
public void run() {
handler[0] = new Handler(){
@Override
public void handleMessage(Message msg) {
System.out.println("--->" + msg.obj);
}
};
handler[0].looper();
}
}.start();
for (int i = 0;i <10;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler[0].sendMessage(new Message("---------weijun---------" + i));
}
}
}
暂时不用管为什么 Handler 放数组里面。运行结果如下:
当我们在主线程把消息丢到队列,不管子线程有没有发生阻塞,主线程永远都不会阻塞。因此,这就能解决大量消息问题(大量生产,慢慢消化,超过上限就会 OOM)。
但是这种写法只能存在一个 Handler(因为需要调用 Handler.looper(),looper() 之后的代码都不会被执行)。
那如果需要多个 Handler 的实例,但是要保证其队列只有一个,也就是队列要和线程进行绑定,应该怎么做呢?这个时候 Looper 就登场了。如果我们把队列放到 Looper 里面去,Handler 就解放了。
第三步,在刚才演进的过程中发现,如果有队列就一定要有 for 循环,如果有 for 循环,那就一定会堵塞,那么阻塞后面的代码都不会被执行,因此一个线程只能有一个队列,此时就需要一个机制来保证只有一个队列。怎么办呢?Looper 机制保证
一个线程只能有一个队列。
创建一个 Looper,把队列 MessageQueue 放到 Looper 里面:
public class Looper {
static MessageQueue messageQueue;
public static void prepare(){
messageQueue = new MessageQueue();
}
}
如果外部调用多次 prepare() ,那这个队列会重新初始化,肯定不行。
现在就可以借助 ThreadLocal ,要了解 ThreadLocal 就一定要了解线程。一个线程只能有一个消息队列,而队列和 Looper 绑定,因此需要保证 Looper 只有一个。所以怎么办呢?
线程就是一个 Thread 对象,其内部有一个 ThreadLocal.ThreadLocalMap ,至于这个 ThreadLocal.ThreadLocalMap 是干什么的后面会提。等于说每一个 Thread 对象,都要给他匹配一个队列,队列跟着 Thread 对象一起走就好了,他们肯定是一一匹配的。接下来只需要利用 ThreadLocal 去找到 ThreadLocalMap 。
现在保证 Looper 唯一,那么 MessageQueue 也唯一了:
ThreadLocal<Looper> threadLocal = new ThreadLocal<>();
ThreadLocal 内部的 ThreadLocalMap 本质上也是一个 HashMap ,用于维护此线程的一些参数。
首先来看看 ThreadLocal 的 set() 方法:
首先拿到当前线程对象,获取当前对象里面的 map,此时我们可以把 Looper 对象给放进这个 map 里面,他保存在这个线程的内部,其 key 就是当前对象,后面只需要取当前线程就能知道其内部的 ThreadLocalMap 是否已经有了 Looper 对象:
threadLocal.set(new Looper());
threadLocal.get();
把 Looper 存进去后,那么现在这个 Looper 的生命周期是多长呢?没错,Looper 现在的生命周期就是当前他所在线程的生命周期一样的长度。
所以在给线程设置一个 Looper 之前,先取一下:
if (threadLocal.get() != null){
throw new RuntimeException("Only one looper may be created per thread");
}
threadLocal.set(new Looper());
小结一下,一个线程怎么保证只有一个 Looper:
1.利用 ThreadLocal.ThreadLocalMap 将 Looper 对象给存进去
2.Looper.prepare() 的时候再去取这个对象是否为空
接下来在 looper() 方法取出 Looper 对象再循环就行了。
那么问题又来了,此时 Looper 对象只有一个,但是 handler 对象却可以有很多个,那么 handleMessage() 应该在哪儿调用呢?
查找 Handler 源码,最终发现 Message 持有一个 Handler:
模仿源码,修改我们的代码:
public class Message {
String obj;
Handler target;
public Message() {
}
public Message(String obj) {
this.obj = obj;
}
}
public class Looper {
MessageQueue messageQueue;
static ThreadLocal<Looper> threadLocal = new ThreadLocal<>();
private Looper(){
messageQueue = new MessageQueue();
}
public static Looper myLooper() {
return threadLocal.get();
}
public static void prepare(){
if (threadLocal.get() != null){
throw new RuntimeException("Only one looper may be created per thread");
}
threadLocal.set(new Looper());
}
public static void looper(){
final Looper me = threadLocal.get();
final MessageQueue queue = me.messageQueue;
for (;;){
Message msg = queue.next();
msg.target.handleMessage(msg);
}
}
}
让能保证唯一性的 Looper 持有队列,在 prepare() 的时候创建新的队列放进当前线程,在 looper() 的时候先取出当前线程所拥有的 Looper 对象,获取其内部的队列 MessageQueue,然后开启死循环,不断的从队列里面取出消息 Message,由 Message 内部持有的 Handler 对象通过 handleMessage() 把消息传递下去。
修改我们的 ActivityThread,就像平时使用 Handler 那样使用:
public class ActivityThread {
private static Handler handler;
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
Looper.prepare();
handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
System.out.println(Thread.currentThread().getName() + " -> msg" + msg.obj);
}
};
Looper.looper();
}
}.start();
for (int i = 0;i <4;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " -> sendMessage");
handler.sendMessage(new Message("---------weijun---------" + i));
}
}
}
可以看到成功的在主线程发送消息,在子线程去处理。
像这种方式,就完美解决了线程切换的问题,也解决了一个线程只能对应一个队列、一个 for 循环的问题。
Handler 为什么会造成内存泄漏,其他内部类为什么没有这个问题
首先来看看平时大多数人的写法:
为什么这样做会造成 Activity 内存泄漏呢?很多人回答是内部类持有外部类的引用造成的。既然这样的话,为什么下面这个 Person 类,他没有造成内存泄漏呢?
这样写,MyHandler 虽然会持有 MainActivity 的引用,但是这并不是导致内存泄漏的原因。
由上面可以得出,现在的持有链是:
MyHandler --> MainActivity
由于前面查看源码得知,Message 内部会持有一个 Handler 的对象,于是持有链变成了:
Message --> MyHandler --> MainActivity
在刚才手写 Handler 源码的时候,是谁又持有了 Message 呢?
没错就是我们的消息队列。
所以持有链继续变化:
MessageQueue --> Message --> MyHandler --> MainActivity
那么又是谁持有了 MessageQueue ?
因为他要保证队列的唯一,所以 Looper 持有 MessageQueue ,持有链变成了:
Looper --> MessageQueue --> Message --> MyHandler --> MainActivity
那么谁又持有 Looper ?
是当前的线程,所以本质又回到了线程,持有链变成了:
线程 --> Looper --> MessageQueue --> Message --> MyHandler --> MainActivity
由此可以得出结论,内存泄漏的本质是长生命周期对象持有短生命周期对象。比如说短生命周期的 Activity,当他返回的时候应该要被销毁,但是有个长生命周期对象持有他,所以导致这个 Activity 迟迟得不到销毁,这就产生了内存泄漏。
MessageQueue 从哪里开始运行的
先从当前 App 的入口开始,也就是 ActivityThread。
ActivityThread类是Android APP进程的初始类,它的main函数是这个APP进程的入口。APP进程中UI事件的执行代码段都是由ActivityThread提供的。也就是说,Main Thread实例是存在的,只是创建它的代码我们不可见。ActivityThread的main函数就是在这个Main Thread里被执行的。
在 main() 函数里面,他回去调用 Looper.prepareMainLooper(),而 prepareMainLooper() 也只是封装了一下,最终也会调用 prepare():
接下来看看 prepare():
和刚才手写的 prepare() 一样,对吧。所以他会先去检查当前线程是否有这个 Looper 对象,如果没有的话就会往当前线程里面取存一个 Looper 对象。接下来检查 looper():
可以看到他首先也是调用的 myLooper(),myLooper() 其实就是调用的 sThreadLocal.get(),和刚才手写的一样。
接着通过 Looper 把队列 MessageQueue 取出来,然后开始死循环。在死循环里面通过 MessageQueue.next() 取出 Message。
Message 取出来后,最终会调用其内部的 Handler.dispatchMessage() ,最终调用到 handleMessage()。
源码地址