前言
Handler
可以说小伙伴们用的非常多了,可以说Handler
是支撑整个Android系统运行的基础,本质上Android系统都是由事件驱动的。而处理事件的核心就在于Handler
。接下来我们就从简单的使用,到源码分析让你彻彻底底明白Handler
的本质。不会再让你发出为什么Looper.loop
不会堵塞主线程,Handler是如何切换线程等这类疑惑。
作者:Mlx
链接:https://juejin.im/post/6866015512192876557
简单使用
一般是在主线程中实现一个Handler,然后在子线程中使用它。
class HandlerActivity: AppCompatActivity() {
private val mHandler = MyHandler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 在子线程中通过自定义的 Handler 发消息
thread {
mHandler.sendEmptyMessageDelayed(1, 1000)
}
}
// 自定义一个 Handler
class MyHandler: Handler() {
override fun handleMessage(msg: Message) {
Log.i("HandlerActivity", "主线程:handleMessage: ${msg.what}")
}
}
}
或者有时候需要在子线程中创建运行在主线程中的Handler
class HandlerActivity: AppCompatActivity() {
private var mHandler: Handler? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
thread {
//获得main looper 运行在主线程
mHandler = MyHandler(Looper.getMainLooper())
mHandler!!.sendEmptyMessageDelayed(1, 1000)
}
}
// 自定义一个 Handler
class MyHandler(): Handler() {
override fun handleMessage(msg: Message) {
Log.i("HandlerActivity", "子线程:handleMessage: ${msg.what}")
}
}
}
这就是小伙伴们一般常用的两个用法。大家注意到了在第二个用法中出现了一个Looper.getMainLooper()
,使用它作为参数,即使MyHandler
是在子线程中定义的,但是它的handleMessage
方法依然运行在主线程。我们看一下这个参数究竟是什么东东~
public Handler(@NonNull Looper looper) {
this(looper, null, false);
}
可以看到这个Looper
就是我们上面传入的参数Looper.getMainLooper()
,也就说明了handleMessage
方法具体运行在哪个线程是和这个Looper
息息相关的。那么这个Looper
究竟是何方神圣,它是怎么做到线程切换的呢?
概述
我们先来看一张图
这就是整个Handler
在Java层的流程示意图。可以看到,在Handler
调用sendMessage
方法以后,Message
对象会被添加到MessageQueue
中去。而这个MessageQueue
就是被包裹在了Looper
中。那么Looper
对象是干什么的呢?它和Handler
是什么关系呢?我们来看一下他们具体的职责把~
-
Handle
消息机制中作为一个对外暴露的工具,其内部包含了一个Looper
。负责Message
的发送及处理-
Handler.sendMessage()
:向消息队列发送各种消息事件 -
Handler.handleMessage()
:处理相应的消息事件
-
-
Looper
作为消息循环的核心,其内部包含了一个消息队列MessageQueue
,用于记录所有待处理的消息;通过Looper.loop()
不断地从MessageQueue
中抽取Message
,按分发机制将消息分发给目标处理者,可以看成是消息泵。注意,线程切换就是在这一步完成的。 -
MessageQueue
则作为一个消息队列,则包含了一系列链接在一起的Message
;不要被这个Queue的名字给迷惑了,就以为它是一个队列,但其实内部通过单链表的数据结构来维护消息列表,等待Looper
的抽取。 -
Message
则是消息体,内部又包含了一个目标处理器target
,这个target
正是最终处理它的Handler
哦?原来他们的职责是这样啊,可我还是不懂他们到底是怎么运行起来的。就像你告诉我医生是负责治病,警察是抓坏人的,他们具体是如何去做的呢?
Handler
从我们大家最熟悉的sendMessage
方法说起。sendMessage
方法见名思意,就是发送一个信息,可是要发送到哪里去呢,这是代码:
public final boolean sendMessage(@NonNull Message msg) {
return sendMessageDelayed(msg, 0);
}
调用了sendMessageDelayed
方法:
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
继而调用sendMessagAtTime
方法:
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
眼尖的小伙伴就会发现,等等不对,这代码中出了一个叛徒,啊不对,出了一个奇怪的东西。没错,就是刚才流程图中出现的这个MessageQueue
。你看,我没有胡说吧,这个MessageQueue
是实打实存在的,并且被作为参数一起传给了enqueueMessage
方法。其实无论你是如何使用Handler发送消息,结果都会走到enqueueMessage
方法中。
这是方法的调用链:
可以看到无论如何,最后都会走到enqueueMessage
方法中。这个enqueueMessage
方法具体做了什么事呢:
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);
}
enqueueMessage
一共做了两件事情,一个是给Message
赋值,一个是调用传进来的这个MessageQueue
的enqueueMessage
方法。注意啊,最后这个enqueueMessage
方法是在MessageQueue
中的,已经不再是Handler
的方法了,也就是说,调用走到了这里。事件的流向已经不归Handler
管了。
Handler::enqueueMessage
方法中第一行msg.target = this;
,这个this是什么呢?这个this
在handler
方法中自然是handler
本身了,也就是说这一行代码将handler
自身赋值给了Message
对象的target
字段。我们可以看以下这个target
字段的定义:
//简化后的代码
public final class Message implements Parcelable{
@UnsupportedAppUsage
/*package*/ Handler target;
}
啊,这样明白了,也就是说每个发出去的Message
都持有把它发出去的Handler
的引用,对不对?
没错事实就是这样,每个发出去的Message
对象内部都会有个把它发出去的Handler
对象的引用,也可以理解Message
这么做的目的,毕竟Handler
把它发射出去了,它不得知道是谁干的,好随后找它报仇么。那么我们继续下一步,msg.setAsynchronous(true)
这一行代码是设置异步消息的,这里暂时先不管它。我们先看queue.enqueueMessage(msg, uptimeMillis)
这行代码。也就是从这行代码,Message
就可以和Handler
说拜拜了您讷。
MessageQueue
Handler
这个mQueue
就是上文我们提到过的MessageQueue
对象,在上面的介绍说也说了,这货就是个骗子,明明起名是Queue
,却是单链表。你可能误会Google工程师了,名字也确实没什么错了,从机制上看确实很像队列。队列是什么特性啊,先进先出对吧。这个先后就是按时间来划分的,时间靠前的就在前面时间靠后的就在后面。而在这个单链表中也确实是这样实现的,按照时间的先后排序。这个就先不多讲了,一会讲如何实现的消息延时发送的时候会讲到这个。
到这里你可能有疑惑了,这个MessageQueue
是什么鬼,从哪里冒出来的。你可能还记得,在上面的sendMessageAtTime
方法中有这么一行:
MessageQueue queue = mQueue;
那么这个mQueue是在哪里被赋值的呢?当然是在构造方法中啦~
public Handler(@Nullable Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
不对啊, 你TM骗我,在最开始你继承的Handler可没有这几个参数。哎呀,小伙子别心急,你看这个无参构造方法不也调用的这个方法么。
public Handler() {
this(null, false);
}
在这个有参数的构造方法中呢,可以看到有这么两行:
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
我们在Handler
中使用的mQueue
就是在这里赋值的。这里的赋值可不简单,它拿的是人家Looper
的MessageQueue
作为自己的MessageQueue
,而且在上面的代码中有一个很关键的点,就是调用Looper.myLooper()方法中获取这个Looper对象,如果是空的话就要抛出异常。这一点非常关键,我们先做个记号,一会回过头来会看这一行代码。你就会明白它的作用了。
现在先不研究Looper
,我们继续看我们的MessageQueue
。上面说到,最后发送消息都调用的是MessageQueue
的queue.enqueueMessage(msg, uptimeMillis)
方法。现在我们已经拿到了queue
,进去看看这个方法它做了什么。
// MessageQueue.java
//省略部分代码
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
msg.recycle();
return false;
}
msg.markInUse();
msg.when = when;
//【1】拿到队列头部
Message p = mMessages;
boolean needWake;
//【2】如果消息不需要延时,或者消息的执行时间比头部消息早,插到队列头部
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 {
//【3】消息插到队列中间
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;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
主要分为3个步骤(见以上代码标注)。
-
mMessages
是队列的第一消息,获取到它 - 判断消息队列是不是空的,是则将当前的消息放到队列头部;如果当前消息不需要延时,或当前消息的执行时间比头部消息早,也是放到队列头部。
- 如果不是以上情况,说明当前队列不为空,并且队列的头部消息执行时间比当前消息早,需要将它插入到队列的中间位置。
如何判断这个位置呢?依然是通过消息被执行的时间。
通过遍历整个队列,当队列中的某个消息的执行时间比当前消息晚时,将消息插到这个消息的前面。
可以看到,消息队列是一个根据消息【执行时间先后】连接起来的单向链表。
想要获取可执行的消息,只需要遍历这个列表,对比当前时间与消息的执行时间,就知道消息是否需要执行了。
好了,MessageQueue
在Java
层的分析到这里就结束了。
等等,这就结束了?
没错,到这一步,消息已经添加到了这个名为队列实为单链表的队列中。
不对啊,我handleMessage
方法如何被调用呢?消息添加进去就完了?说好的线程切换呢?
其实到这一步真的就结束了,最起码在Java层是结束了。消息到这一步被添加到了队列中,Handler
和MessageQueue
在发送的过程中做的工作已经做完了。但是既然有队列,那么不可能说光添加不读取把。不然我添加了有什么用?
是的,接下来就是Looper
大展神威的时候到了。
Looper
在上面提到了,Handler
中的MessageQueue
对象其实就是Handler
中的Looper
它的MessageQueue
,Handler
往MessageQueue
中添加消息,其实就是往Handler
的Looper
所持有的MessageQueue
中添加对象。可能有点绕,但是需要明白的是这个MessageQueue
是Looper
的,不是Handler
的。明白了这一点,你就能很好的理解后面发生的事情。
可能有的小伙伴会说了,这个Looper
哪来的,我创建Handler
的时候从没看见过它出现啊。没错,在使用Handler
的时候它确实没出现过,但是大家还记得Handler
中两个参数的那个构造方法嘛?就是下面这个:
//Handler.java
//省略部分代码
public Handler(@Nullable Callback callback, boolean async) {
//敲黑板,划重点就是这一句!!!!
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
在这一句中Handler
通过Looper.myLooper
方法获取到了Looper
对象,当然,也有可能没获取到。不过,你如果没获取到就惨了,就要抛异常了。
在职责分析中我们提到了, 这个Looper
对象作为消息循环的核心,不断从它的MessageQueue
中取出消息然后进行分发。
说人话可以不?
刚才说到MessageQueue
那个队列中那么多的消息没人拿,MessageQueue
的老板Looper
看不下去了,说你这也太浪费了,来我拿吧,然后它专门负责一个个拿,然后看这是谁发的,然后让谁去处理。
那我们看看这个Looper.myLooper()
方法做了什么事情呢。它是如何返回一个Looper
对象的呢?
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
sThreadLocal
又是什么鬼?咱们看一下它的定义
//sThreadLocal.get() will return null unless you've called prepare().
@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
可以看到这个sThreadLocal
是一个ThreadLocal
类,并且它的泛型是Looper
对象。ThreadLocal
提供了线程的局部变量,每个线程都可以通过set()
和get()
来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。简要言之:往ThreadLocal
中填充的变量属于当前线程,该变量对其他线程而言是隔离的。
呀呵,源代码中还有行注释,这行注释的意思是除非您已调用prepare(),否则sThreadLocal.get()将返回null。这行注释就有趣了,刚才我还寻思这个ThreadLocal
的get方法得有数据才能返回,可这个数据是啥时候塞进去的呢?你这注释就告诉我了,只有我调用了prepare()
方法,才有值啊。那我们就去看看这个方法做了些什么。
public static void prepare() {
prepare(true);
}
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));
}
复制代码
可以看得出,最后调用了是prepare(boolean quitAllowed)
方法,而这个方法首先判断,如果sThreadLocal
有值,就抛异常,没有值才会塞进去一个值。其实很好理解,就是说prepare
方法必须调用但也只能调用一次,不调用没有值,抛异常,调用多次也还抛异常。我好难哦~不过大家还记得上面我们重点关注的一个内容吗,在Handler中的有参构造函数中有这么一行代码会报异常:
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
如果Looper
为空就抛异常,现在我们知道了,什么时候Looper
为空呢?没有调用prepare
方法的时候会为null
,也就是说在构造Handler
之前,必须得有Looper
对象,换言之,在构造Handler之前,必须调用Looper
的prepare
方法创建Looper
。这句话非常重要,所以我又是下划线又是加粗的,一定要记住这句话。在后面自定义一个Looper的时候会用到。
接下来再看看这行sThreadLocal.set(new Looper(quitAllowed));
做了什么吧,它是如何塞进去的呢
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set
方法首先获取到了当前的线程,然后获取一个map
。这个map
是以键值对形式存储内容的。如果获取的map
为空,就创建一个map
。如果不为空就塞进去值。要注意的是,这里面的key
是当前的线程,这里面的value
就是Looper
。也就是说,线程和Looper
是一一对应的。也就是很多人说的Looper
和线程绑定了,其实就是以键值对形式存进了一个map
中。没什么高大上的。你来你也行。
而这个Looper的构造方法我们也得去看一下:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
在Looper
的构造方法中,可以看到它创建了一个MessageQueue
,没错就是那个被Handler
无耻使用的MessageQueue
。需要注意的一点,上面的分析中提到了prepare
方法必须调用但也只能调用一次,调用以后就会创建Looper对象,也就是说一个线程中只会创建一个Looper
对象,而一个Looper
对象也只会创建一个MessageQueue
对象。
现在我们来梳理一下这个流程哈~
首先创建一个无参数的Handler
,在这个Handler
的构造方法中又去获取Looper
对象,当然获取Looper
对象其实是为了它的MessageQueue
,Handler
巴结上了人家Looper
对象的MessageQueue
以后,发送消息的时候,把要发送的消息给了MessageQueue
,添加到了队列中。是不是感觉缺少了什么?没错,好像在这个里面Looper
的作用没体现出来,说好的分发消息呢?而且你刚刚说了得调用prepare()
方法才会创建Looper
,可我没调用过这个方法啊。那这个Looper
谁创建的?
刚才提到了,Looper
在创建的时候会被当成value
塞入到一个map
中去,这个map
是ThreadLocal
。而key
就是创建Looper
时所在的线程。也就是所谓的Looper
和线程绑定。我们一般在用的时候从没创建过Looper
,但是我们知道handle
中的回调handleMessage
方法是运行在主线程中的。Looper
的职责不就是分发消息么,也就是说Looper
对象在主线程中把消息分发给了Handler
。那么这下就明白了,在我们创建Looper
的时候,Looper
所在的线程是主线程,换言之,与这个Looper
绑定的线程就是主线程。
明白了,我这就去和面试官对线。
既然是主线程,那么大家应该知道,主线程是谁创建的?ActivityThread
类。ActivityThread
类也正是整个app的入口。以前我也很好奇,既然Android
是用Java
写的,按理说Java
不应该是有个什么main
方法么?怎么我写Android
没用过这个main
方法呢?其实呢,在ActivityThread
中就有这个main
方法,它是程序的入口,也就说当你点开app
以后,首先会进入到这个main
方法中,然后做了一大堆事情,这里就不分析了。你只需要知道,这个main
方法才是真正的入口。
那我们来看看这个main方法到底干了什么事情:
//ActivityThread.java
//省略部分代码
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
Process.setArgV0("<pre-initialized>");
//1 敲黑板,划重点,就是这一句!
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
//2 敲黑板,划重点,这一句!
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
这段代码是不是很符合我们平常写的java程序呢?熟悉的main方法又回来了。main方法中可以看到,它调用了Looper的prepareMainLooper
方法:
public static void prepareMainLooper() {
//设置不允许退出的Looper
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
可以看到注释1,这个方法最终还是调用了Looper
的prepare
方法,这个方法干嘛的?创建Looper
并且把它和当前线程一起塞进map
中的啊。当前线程是哪个线程?主线程啊!
一切到这里就真相大白了,在APP
启动的时候,入口方法中已经自动帮我们创建好了Looper
,并且也自动的帮我们和主线程绑定了。也就是说我们平常用的Handler
中的Looper
就是主线程中创建的这个Looper
。细心的小伙伴应该会发现,这个prepareMainLooper
方法你是不能调用的。为啥?因为这个方法在入口的时候执行了一次,所以里面的sMainLooper
不为Null了,如果你在调用一次,不就要抛异常了么~
现在Looper
也有了,Looper
的MessageQueue
也有了。接下来该分发消息了吧?我Handler
发送消息可是已经很久过去了,你这里分析一大通,我还干不干活了?
好,我们现在先假设一个场景。
你买了一个快递,你知道迟早会给你送到,但是不确定到底什么时候才会送到。你想早点拿到快递应该怎么做?
你会不停的问快递公司,我的快递到哪了,到哪了。当然,现实中一般都是等快递员打电话才去拿快递~问题在于,这是程序。
Looper
虽说要分发消息,但是它又不知道你什么时候会发送消息,只能开启一个死循环,不断的尝试从队列中拿数据。这个死循环在哪里开始的?没错就是注释2处,Looper.loop()
开启了一个死循环,然后不断的尝试去队列中拿消息。
// Looper.java
public static void loop() {
//拿到当前线程的Looper
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//拿到Looper的消息队列
final MessageQueue queue = me.mQueue;
// 省略一些代码...
//1 这里开启了死循环
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
try {
msg.target.dispatchMessage(msg);
//省略一些代码...
} catch (Exception exception) {
//省略一些代码...
throw exception;
} finally {
//省略一些代码...
}
//省略一些代码...
msg.recycleUnchecked();
}
}
在循环中Looper
不停的取出消息,拿到Message
对象以后,会去调用Message
的target
字段的dispatchMessage
方法,这个target
字段还有印象吗?没错,就是发送它的Handler
,message
在被发送出去的时候就已经暗暗记下了是谁发送出去的。现在轮到它报仇了~
我们可以跟进看一下这个dispatchMessage
方法:
//Handler.java
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
可以看到,消息会先分发给Meesgae
的callback
,我们没有定义这个callback
,那我们接下来看,还有一个mCallback
。这个mCallback
是创建Handler
的时候可以选择传一个CallBack
回调,里面依然是handleMessage
方法。也就是说你可以自定义一个类继承Handler
,重写handleMessage
方法,也可以直接new
一个Handler
传一个回调。当然,这个都很简单,我就不再赘述了,大家可以自行尝试体验。
我们关注重点,当Looper
拿到Message
以后,并且根据Message
的target
字段找到了发送消息的Handler
,紧接着调用了Handler
的handleMessage
方法。重点来了,这个Looper
是在哪个线程运行的?主线程,它调用方法是在哪个线程运行的?依然是主线程!handleMessage
方法此时在哪个线程运行的?依然是主线程!不知不觉中,线程已经切换过来了,神奇不?其实并不神奇,其实就是主线程中的Looper
不断的尝试调用handleMessage
方法,如果有消息就调用成功了,此时handleMessage
方法就是在主线程中调用的。而handler
在哪个线程,Looper
并不关心,我反正只在主线程调用你的handleMessage
方法。这就是线程切换的本质。就是没有线程切换,主线程的Looper
不断的尝试调用而已。
可能有的小伙伴已经懵逼了,我们再次从头到尾梳理一下哈~
-
mainThread
中ActivityThread
首先创建了一个运行在主线程的Looper
,并且把它和主线程进行了绑定。 -
Looper
又创建了一个MessageQueue
,然后调用Looper.loop
方法不断地在主线程中尝试取出Message
-
Looper
如果取到了Message
,那么就在主线程中调用发送这个Message
的Handler
的handleMessage
方法。 - 我们在主线程或者子线程中通过
Looper
.getMainLooper
为参数创建了一个Handler
。 - 在子线程中发送了
Message
,主线程中的Looper
不断循环,终于收到了Message
,在主线程中调用了这个Handler
的handleMessage
方法。
这里需要注意的是,Looper.loop
方法中取到的Looper
对象并不一定就是主线程的,因为它是取出当前线程的Looper
对象。只不过在ActivityThread
这里是主线程,所以拿到的是主线程的Looper
对象。所以如果我们要在子线程中创建一个Looper
也是可以的,一会我们就实现一下。
到这里可能有的小伙伴还是懵逼,我还是不太明白怎么切换的线程。我们通过一个比喻很好的解释一下
首先有一个小学生小明,小明的任务是写作业。然后有一个老师,老师的任务是批改作业。这个班里还有一个学习委员,学习委员的任务就是负责收作业然后交给老师去批改。
一般情况下,老师是学校已经聘请好的,我们不需要自己去聘请老师。老师一般也就只在办公室批改作业,办公室我们可以理解为主线程。学校就是我们的app
。老师就是Looper
对象。而小明同学就是Handler
,学习委员就是MessageQueue
,作业就是Message。老师(Looper
)在办公室(主线程)不断的从学习委员(MessageQueue
)那里拿到下一本要批改的作业(Message
),老师突然发现作业里有错误,老师很生气,于是就从作业本上的姓名知道了是谁写的这个作业(对应Message
的target
字段),于是老师把小明(Handler
)叫到办公室(主线程),让小明在办公室(主线程)把作业改好(handleMessage
)。在这个例子中,小明作为Handler,他可以在任何地方写作业(sendMessage
),也就是说他可以在家里写作业,可以在教室写作业,也可以在小公园写作业,这里的各个地方就是不同的线程。但是写完作业以后一定要交给学习委员,学习委员手里有一摞作业,这一摞作业就是消息的队列,而学习委员就是MessageQueue
,他负责收集作业,也就是收集Message
。老师在办公室批改作业,发现出错了,就把小明叫到了办公室,让小明在办公室改错。在这里,办公室就是主线程,老师不会管小明是在哪里写的作业,老师只是关心作业出错了,需要小明在办公室里改错。小明在办公室里改错这就是handleMessage
方法运行在了主线程。但是也有个问题,不能说你小明在办公室改错改个没完没了,那岂不是影响了后面同学作业的批改?如果小明真的改错改的没完没了,也就是在主线程上作耗时操作很久,那么老师也无法进行下一个同学的作业批改,时间一长,教学就没法进行了。这就是著名的ANR问题。不知道这样比喻,小伙伴们能不能理解线程切换的意思和ANR的意思。如果还不能理解,那么你来砍我吧~
Looper和ANR
很多面试官喜欢问,Looper
的loop
方法是个死循环,而loop
方法又是运行在主线程的,主线程上有死循环为什么不会导致ANR存在呢?
其实这里面很有趣的一个点就是,很多小伙伴把Looper
的loop
方法当做一个普通方法来看待,所以才会有这样的疑问。但是这个loop
方法并不是一个普普通通的方法。
我们先思考一点,如果我们写一个app
,里面一行代码也不写的话,app会不会崩溃?
答案显而易见,是不会的。
可是在上面提到了,本质上App
就是一个Java
程序,Java
程序就有main
方法,在ActivityThread
类中也确实有这个main
方法。我们一般写java
程序的时候,是不是main
方法中的代码执行完,程序也就结束了。但是app
并没有,只要你不退出,它一直运行。那这是为什么呢?
很多小伙伴应该想到了,没错,让程序不退出的话,写一个死循环,那么main
方法中的代码永远不会执行完,这样程序就不会自己退出了。Android当然也是这么干的,而且不止Android,基本上所有的GUI程序都是这么干的。正是因为Looper.loop
方法这个死循环,它阻塞了主线程,所以我们的app才不会退出。那你可能有疑问了,那既然这里有死循环了,那我其他的代码怎么运行?界面交互怎么办?你问到点子上了。
本质上Android就是事件驱动的程序,界面刷新也好,交互也好,本质上都是事件,这些事件最后通通被作为了Message
发送到了MessageQueue
中。由Looper
来进行分发,然后在进行处理。用人话来说就是,我们的Android程序就是运行在这个死循环中的。一旦这个死循环结束,app也就结束了。
那么ANR
是什么呢?ANR
是Application Not Responding也就是Android程序无响应。为什么没响应呢?因为主线程做了耗时操作啊。可我还是不明白,明明Looper
的loop
方法就是阻塞了主线程,为什么不ANR
呢。那我们就来说道说道,什么是响应?响应就是界面的刷新,交互的处理等等对吧。那么这个响应是谁来响应的?没错,就是loop
方法中进行响应的。没响应什么意思?就是loop方法中被阻塞了,导致无法处理其他的Message
了。
所以结论就来了,主线程做耗时操作本质上不是阻塞了主线程,而是阻塞了Looper
的loop
方法。导致loop
方法无法处理其他事件,导致出现了ANR事件。对比小明这个比喻的话,就是因为小明在办公室里没完没了的改作业,占用了老师的时间,让老师没法批改下一个同学的作业,才导致了教学活动无法正常进行。而老师不断的批改作业,这本身就是正常的教学活动,也正是因为老师不断批改作业,同学们才有提高,教学才能继续。
Handler在Java层几个需要注意的点
-
子线程Looper
如果要创建Handler,必须通过Looper.prepare()方法创建Looper,在主线程中ActivityThread已经帮我们创建好了,我们不需要自己去创建,但如果在子线程中创建Handler,要么使用Looper的mainLooper,要么自己调用Looper.prepare()方法创建属于这个线程的looper对象。如下是创建了一个子线程的Looper对象:
class LooperThread extends Thread { public Handler mHandler; public void run() { Looper.prepare(); mHandler = new Handler() { public void handleMessage(Message msg) { //TODO 定义消息处理逻辑. } }; Looper.loop(); } }
-
消息池
在生成消息的时候,最好是用
Message.obtain()
来获取一个消息,这是为什么呢?// Message.java public static Message obtain() { synchronized (sPoolSync) { if (sPool != null) { Message m = sPool; sPool = m.next; m.next = null; m.flags = 0; // clear in-use flag sPoolSize--; return m; } } return new Message(); }
可以看到,obtain方法是将一个Message对象的所有数据清空,然后添加到链表头中。sPool就是个消息池,默认的缓存是50个。而且在Looper的loop方法中最后一行是这样的
msg.recycleUnchecked();
Looper在分发结束以后,会将用完的消息回收掉,并添加到回收池里。
-
Handler导致的内存泄露问题
什么是内存泄露?简而言之就是该回收的东西没有回收。在Handler中一般是这样使用:
class HandlerActivity: AppCompatActivity() { private val mHandler = MyHandler() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 在子线程中通过自定义的 Handler 发消息 thread { mHandler.sendEmptyMessageDelayed(1, 1000) } } // 自定义一个 Handler class MyHandler: Handler() { override fun handleMessage(msg: Message) { Log.i("HandlerActivity", "主线程:handleMessage: ${msg.what}") } } }
乍一看没有问题,但是有没有想过一个问题,就是说再发送延时消息之前,app推出了,那么handleMessage方法还会执行吗?答案是会的。为什么?我明明退出了,为什么还会执行呢?其实这和java有关系
MyHandler
是HandlerActivity
的内部类,会持有HandlerActivity
的引用。在进入页面以后,发送了一个延时 1s 的消息,如果
HandlerActivity
在 1s 内退出了,由于Handler
会被Message
持有,保存在其target
变量中,而Message
又会被保存在消息队列中,这一系列关联,导致HandlerActivity
在退出的时候,依然会被持有,因此不能被GC
回收,这就是内存泄漏!当这个 1s 延时的消息被执行完以后,HandlerActivity
会被回收。虽然最终结果还是会被回收,但是内存泄露问题我们也必须去解决,如何解决?
1.将
MyHandler
改为静态类,这样它将不再持有外部类的引用。可以将HandlerActivity
作为弱引用放到MyHandler
中使用,页面退出的时候可以被及时回收。2.页面退出的时候,在
onDestroy
中,调用Handler
的removeMessages
方法,将所有的消息 remove 掉,这样也能消除持有链。 -
同步消息屏障
什么是同步消息屏障?
在Looper的loop方法中通过
Message msg = queue.next();
这么一行代码拿到Message进行分发,这个MessageQueue的next方法中有这么一行://MessageQueue.java //省略部分代码 Message next() { for (;;) { if (nextPollTimeoutMillis != 0) { Binder.flushPendingCommands(); } nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // Try to retrieve the next message. Return if found. final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; //1 这一行很关键,同步消息屏障的关键点所在 if (msg != null && msg.target == null) { do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } } } }
注释1下面的这一行代码,首先会判断
msg
不为null,然后紧接着判断msg
的target
为null。我们知道message
的target
就是发送它的handler
,所有的message
都有一个handler
,这里怎么可能没有handller
呢?针对同步消息还真的是所有的message
都有handler
,而这里是异步消息。满足target == null
的消息就是异步消息。同步屏障
是用来阻挡同步消息执行的。说得好,那么同步屏障有什么用呢?似乎在日常的应用开发中,很少会用到同步屏障。那么,同步屏障在系统源码中有哪些使用场景呢?Android 系统中的 UI 更新相关的消息即为异步消息,需要优先处理。简而言之,如果在启动绘制之前,用户(开发者)插入了一个非常耗时的消息到队列中,那就会导致
UI
不能按时绘制,导致卡顿掉帧。,同步消息屏障就可以用来保证UI
绘制的优先性。
Handler在C++层
如果你的目标是理解Handler在Java层是如何实现的,下面就不需要看了。下面主要讲解Handler在C++层是如何工作并实现的。
首先,细心的小伙伴们可能会有疑问。Looper一直处于死循环中,就像老师一直不断的问学习委员要作业批改,老师也是人,不会累么?你问对了,老师当然不会一直不断的问学习委员要作业,正常情况下,是有人交了作业以后,学习委员送过来,老师才会去批改。没有作业的时候,老师可能在休息,可能在玩游戏。Looper也是一样,在消息队列为空的时候,Looper实际上处于休眠状态,只要当有Handler发送消息的时候,Looper才会被唤醒,去进行分发消息。那么是怎么实现的呢?
在整个消息机制中,MessageQueue
是连接Java
层和Native
层的纽带,换言之,Java
层可以向MessageQueue
消息队列中添加消息,Native
层也可以向MessageQueue
消息队列中添加消息。
这是MessageQueue
中的Native方法:
// MessageQueue.java
private native static long nativeInit();
private native static void nativeDestroy(long ptr);
private native void nativePollOnce(long ptr, int timeoutMillis);
private native static void nativeWake(long ptr);
private native static boolean nativeIsPolling(long ptr);
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
在MessageQueue的构造方法中是这样的:
//MessageQueue.java
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
调用了nativeInit
方法,在native
层创建了native
层的MessageQueue
,mPtr
是保存了NativeMessageQueue
的指针,后续的线程挂起和线程的唤醒都要通过这个指针来完成,其实就是通过Native层的MessageQueue来完成。
//android_os_MessageQueue.cpp
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
//初始化native消息队列
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
nativeMessageQueue->incStrong(env); //增加引用计数
return reinterpret_cast<jlong>(nativeMessageQueue);
}
这个是NativeMessageQueue的构造方法:
//android_os_MessageQueue.cpp
NativeMessageQueue::NativeMessageQueue()
: mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread(); //功能类比于Java层的Looper.myLooper();
if (mLooper == NULL) {
mLooper = new Looper(false); //创建native层的Looper
Looper::setForThread(mLooper); //保存native层的Looper到TLS,功能类比于Java层的ThreadLocal.set();
}
}
Looper的构造方法是这样的:
//Looper.cpp
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
mWakeEventFd = eventfd(0, EFD_NONBLOCK); //构造唤醒事件的fd
AutoMutex _l(mLock);
rebuildEpollLocked(); //重建Epoll事件
}
void Looper::rebuildEpollLocked() {
if (mEpollFd >= 0) {
close(mEpollFd); //关闭旧的epoll实例
}
mEpollFd = epoll_create(EPOLL_SIZE_HINT); //创建新的epoll实例,并注册wake管道
struct epoll_event eventItem;//新建唤醒监听事件
memset(& eventItem, 0, sizeof(epoll_event)); //把未使用的数据区域进行置0操作
eventItem.events = EPOLLIN; // 设置监听内容可读事件
eventItem.data.fd = mWakeEventFd;
//将唤醒事件(mWakeEventFd)添加到epoll实例(mEpollFd)
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
for (size_t i = 0; i < mRequests.size(); i++) {
const Request& request = mRequests.valueAt(i);
struct epoll_event eventItem;
request.initEventItem(&eventItem);
//将request队列的事件,分别添加到epoll实例
int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem);
}
}
等等,你上来给我这一大段C++代码,我怎么可能看得懂。还有这个epoll是什么?不是讲如何Looper怎么休眠和唤醒的么?
没错,就是讲的Looper
怎么休眠和唤醒的。Looper
的休眠和唤醒都是在Native层实现的,实现的原理是Linux上的epoll机制。
什么是epoll
机制呢?
epoll
你可以简单的理解为一个监听事件,在Linux
上通过epoll
机制监听一个事件,没什么事的时候我就让出CPU,进行休眠,当这个事件触发的时候我就从沉睡中唤醒开始处理。就像按钮的点击事件一样,点击了,监听到这个点击事件就会触发按钮的onClick
方法。不过在LInxu上是通过文件的读写来完成的。类比于
include <sys/epoll.h>
// 创建句柄 相当于初始化onClickListener
int epoll_create(int size);
// 添加/删除/修改 监听事件 相当于addOnClicklistener
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 进入等待 这就相当于onCLick方法了
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_wait
就类似于Java中的onCLick
方法,当监听的文件有变化的时候,它就会收到结果。其实更像是Kotlin协程中的suspend方法,就一直在等着,阻塞着,有结果才会进行下一步。onClick方法是使用接口回调的形式来实现的,是非阻塞的。而epoll_wait
方法是阻塞的。
在上面的Looper
构造方法中,调用了rebuildEpollLocked
方法,这个方法就是设置监听器的,可以理解为setOnClickListener
,不过它监听的是文件的可读事件。 即eventItem.events = EPOLLIN;
这行代码。什么是可读事件?就是说,文件里面有内容了是不是就可以读了,没错就是这样喵~
好了,事件也已经监听了,那么Looper是在哪沉睡的呢?
是在MessageQueue中的这行代码:
//MessageQueue.java
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
for (;;) {
...
nativePollOnce(ptr, nextPollTimeoutMillis); //阻塞操作
...
}
就是通过这行代码进行阻塞操作。
调用关系是这样的:
MessageQueue::nativePollOnce
->NativeMessageQueue::pollOnce()
->Looper::pollOnce()->Looper::pollInner
int Looper::pollInner(int timeoutMillis) {
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
//1\. 等待事件发生或者超时,如果nativeWake()方法中向管道写端写入字符,则该方法会返回;
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
mPolling = false;
mLock.lock();
if (mEpollRebuildRequired) {
mEpollRebuildRequired = false;
rebuildEpollLocked();
goto Done;
}
if (eventCount < 0) {
if (errno == EINTR) {
goto Done;
}
result = POLL_ERROR;
goto Done;
}
if (eventCount == 0) {
result = POLL_TIMEOUT;
goto Done;
}
//循环遍历,处理所有的事件
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
//唤醒事件
if (fd == mWakeEventFd.get()) {
if (epollEvents & EPOLLIN) {
////已经唤醒了,则读取并清空管道数据【7】
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
// 处理其他事件,Handler没有
// 省略一些代码...
}
}
Done: ;
//省略一些代码...
// Release lock.
mLock.unlock();
//省略一些代码...
return result;
}
代码到了注释1处就开始了阻塞,也就是所谓的休眠。那么什么时候才能唤醒它呢?超时了,或者文件发生了变化,可以读了就可以唤醒了。注意,这个超时就是在Java层设置的延时发送,也就是说Java的sendMessageDelayed
方法最后是通过epoll
设置超时的机制实现延迟发送的。
不知道大家注意到没有,在我们发送Message的时候有这么一行代码:
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
// 省略一些代码...
synchronized (this) {
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 {
//消息插到队列中间
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;
}
if (needWake) {
// 敲黑板划重点:唤醒
nativeWake(mPtr);
}
}
return true;
}
在最后nativeWake(mPtr);
这行代码进行了唤醒。不过必须neekWake
为true
的时候才会唤醒,那么neekWake
什么时候才是True呢?
两种情况会唤醒线程:
-
(队列为空 || 消息无需延时 || 或消息执行时间比队列头部消息早) && (线程处于挂起状态时(
mBlocked
=true
)) - 【线程挂起(
mBlocked
=true
)&& 消息循环处于同步屏障状态】,这时如果插入的是一个异步消息,则需要唤醒。
唤醒操作具体是如何去做的?
调用链是这样的:
MessageQueue::nativeWake
--->android_os_MessageQueue_nativeWake()
--->NativeMessageQueue::wake()
--->Looper::wake()
//Looper.cpp
void Looper::wake() {
uint64_t inc = 1;
// 向管道mWakeEventFd写入字符1
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
if (errno != EAGAIN) {
ALOGW("Could not write wake signal, errno=%d", errno);
}
}
}
前面说了,epoll_wait
在监听文件的可读事件,那么现在有消息来了,我要触发这个事件只需要往文件里随便写点什么就可以,Looper里面只是写了一个字符1。成功的唤醒了线程。然后开始轮询取出消息分发。
总结
Handler
在C++层也有自己的一套消息轮询机制,和Java
的基本一样,这里就不做分析了。
Handler是构成整个Android系统的基础,正是Looper
的死循环才让Android
程序能够不退出。所有的类似于屏幕刷新,UI互动都是一种事件,通过Handler
发送给了Looper
来进行分发。整个Android
程序可以说就是运行在这个死循环中。
Looper
就是不断批改作业的老师,MessageQueue
就是催你交作业的学习委员,Message
就是作业,上面写了写作业人的名字,Handler
就是写作业的小明。
在一个线程中只能有一个Looper
,也只能有一个MessageQueue
,但是可以有多个Handler
,MessageQueue
也可以处理多个Handler发来的消息。
Looper
的唤醒与挂起是靠Linux
中的epoll
机制来实现的,通过对文件的可读事件的监听来实现唤醒。
整个过程中,MessageQueue
是实现Java层与C++层的互动的纽带,Native方法基本都是靠MessageQueue
来实现的。
Handler
与线程的绑定是依靠ThreadLocal
中的map
来实现的。另外,消息处理流程是先处理Native Message
,再处理Native Request
,最后处理Java Message
。理解了该流程,也就明白有时上层消息很少,但响应时间却较长的真正原因。
参考并且致谢:
网友评论