本文内容:
- Handler相关问题
- Handler发送消息流程
- Handler处理消息流程
- IdleHandler
- 异步消息与同步屏障
Handler的相关问题
- Handler线程通信机制的原理?
- 一个线程中最多有多少个Handler,Looper,MessageQueue?
- Looper死循环为什么不会导致应用卡死,会耗费大量资源吗?
- Looper阻塞之后谁来唤醒它,哪些情况会唤醒Looper线程?
- Handler线程如何进行切换的?
- Handler的消息优先级?
Handler通信机制的原理
Handler通信机制的核心其实是生产者消费者模型,如何理解这个模型,举个很简单真实世界的例子:假如很多人在超市买东西(生产者,这里产生的是账单),买完东西就会产生一个账单(消息),再假设只有一个收银员(消费者,由收银员结账消费账单),这时候就会出现排队结账的现象(消息队列)。当有买完东西要结账的时候(产生一个消息),收银员就要对这些账单进行一个一个的进行结账(消费消息),当没有人要结账的时候(消息队列为空),收银员就可以休息了(线程睡眠)。
这是一种非常常见的模型,通常用于多个线程进行生产(生产者线程),一个线程进行消费掉(消费者线程)。而Handler就是这种模型的典型应用,那Handler中对应的角色呢
Handler中的角色
从上面生产者消费者模型可以看出这个模型有四个角色
- 生产者,即产生消息的角色,对应就是
Handler
,产生消息的过程就是Handler.sendMessage()
方法 - 消费者,即消费消息的角色,对应的是
Looper
,消费消息的过程就是调用Looper.loop()
方法 - 消息,对应就是
Message
- 消息队列,对应就是
MessageQueue
接下来我们就可以回答两个问题了
-
一个线程中最多有多少个Handler,Looper,MessageQueue?
答:一个线程中可以有n个Handler(生产者可以有多个),但是Looper(消费者)和MessageQueue(消息队列)只会有一个
-
Looper死循环为什么不会导致应用卡死,会耗费大量资源吗?
答:因为消息队列为空的时候,Looper线程会进行阻塞,直到有合乎条件的消息产生
Handler的类图
Handler发送消息流程
在查看发送消息流程先来看一下Handler使用
Handler handler;
new Thread(()->{
Looper.prepare();
handler = new Handler(Looper.myLooper()){
@Override
public void handleMessage(Message msg) {
Log.d("jonny", "handleMessage: " + msg.what);
}
};
synchronized (MainActivity.this) {
notifyAll();
}
Looper.loop();
}).start();
synchronized (this) {
while (null == handler) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
handler.sendEmptyMessage(0);
给子线程发送一个消息看起来很难吧,当然还有比较简单的方法就是用HandlerThread,简化了子线程创建Handler的麻烦,这里是为了说明Handler创建
HandlerThread thread = new HandlerThread("thread-0");
thread.start();
Handler handler = new Handler(thread.getLooper());
handler.sendEmptyMessage(0);
Looper.prepare()
这是一个Looper对象创建的方法,里面很简单,创建Looper对象,并将引用设置到ThreadLocal中,ThreadLocal其实并不存放Looper对象,它会转给ThreadLocalMap对象以Thread为key,Looper为value存储,而它的持有者是线程,并不是ThreadLocal,ThreadLocal感觉更多是一个方便使用的接口
Looper初始化(消费者)
在构造方法中创建消息队列MessageQueue
MessageQueue初始化(消息队列)
在构造方法中对native层的Handler和Looper进行初始化
Handler初始化(生产者)
- 尝试通过调用
Looper.myLooper()
获取当前线程的Looper对象,如果获取不到,则说明没有进行创建,抛出运行时异常 - 通过Looper对象获取消息队列MessageQueue
- 对于参数async,可以将其标记为该Handler(生产者)是VIP即可,优先处理他们的消息,VIP不开放给开发者使用
Handler.enqueueMessage()
Handler所有发送消息的方法最终还是汇聚到enqueueMessage
- 设置Message的target对象为当前Handler
Message.target = this
,方便后面Looper获取到消息之后知道给谁用 - 如果当前的Handler(生产者)是VIP,则将Message(消息)打上VIP的标记,调用
Message.setAsynchronous()
将Message的flags标记FLAG_ASYNCHRONOUS
- 调用
MessageQueue.enqueueMessage()
插入到消息队列中
MessageQueue.enqueueMessage()
- 判断msg.target是否为null,如果是的话则抛出异常
- 添加锁,防止并发导致插入顺序问题,甚至会出现环
- 判断发送的是否正在使用的Message,有几种情况会将其标记为使用
- 插入MessageQueue中,下面会说到
- 调用
Message.postSyncBarrier()
插入同步屏障的时候,因为这个动作并不是通过equeueMessage()
方法 - 在
MessageQueue.next()
获取到Message的时候,这个地方不太理解,因为上面两个动作已经将MessageQueue中的Message标记了
- 如果正在退出,则返回false,说明发送消息失败
- 调用
Message.markInUse()
将Message标记为FLAG_IN_USE
- 设置when,这个when代表延迟到这个时间才能向Looper(消费者)发送Message,然后将使用一个临时变量
p
进行操作Message队列 - 如果Message队列中没有消息或者队列头的时间小于当前Message时间,则将当前Message作为队列头,还有一件事情是标记是否需要唤醒,这个待会再说
- 否则的话则依次遍历Message队列,找到第一个when时间比当前Message.when还大的Message,然后插入到这个Message前面,如果没有,则将其插入到队列尾部,由此可以看出,MessageQueue中的Message队列是按照when时间来进行排序的,这个先记着,后面Looper.loop()中还会再讲到
- 如果
needWake
标记为true,则唤醒Looper线程(消费者线程)
在enqueueMessage()
有两种情况需要唤醒Looper线程,还有另一种情况会取消唤醒
- 当前Message在队列头的时候,此时如果Looper线程是阻塞的(mBlocked为true)
- 唤醒的第二种情况有三个条件
- Looper线程正在阻塞当中
p.target == null
代表队列头是一个同步屏障- 当前的Message为异步的,即是VIP Handler发送的消息
- 有一个情况是取消唤醒,那就是当在当前异步Message.when之前还有一个异步消息的时候,说明此时无需唤醒,则取消唤醒
Handler处理消息流程
在我们的demo中最后一步调用Looper.loop()
方法就是一个消费者线程获取消息的地方
Looper.loop()
- 调用
myLooper()
获取当前线程中的Looper对象,这是个静态方法,从sThreadLocal中获取之前prepare()
方法设置的Looper对象,这也是防止其他线程通过Looper对象调用Looper.loop()方法 - 判断是否调用了多次
Looper.loop()
方法,这意味着手动唤醒,这种是要怎么实现的 - 设置
mInLoop
为true,并且获取当前Looper对象的MessageQueue对象 - 建立死循环
- 调用
MessageQueue.next()
方法 - 判断获取的Message是null,则说明正在退出,因为如果MessageQueue中的Message队列为空的话,是阻塞而不是返回null Mssage
- 接下来是一些性能跟踪的log
- 调用
Handler.dispatchMessage()
,还记得Handler.enqueueMessage()
将Message.target = this
,现在就将Message重新交由Handler处理了 - 最后调用
Message.recycleUnchecked()
重置Message,方便以后获取,无需重新创建Message
MessageQueue.next()
- 设置
nextPollTimeoutMillis = 0
pendingIdleHandlerCount = -1
,先记着这两个值,后面再进行讨论 - 调用
nativePollOnce()
进行阻塞,这时除非之前讨论的两种唤醒情况或者超时,还有一种是移除同步屏障的时候,否则消费者线程会阻塞在这里 - 如果队列头为同步屏障,即队列头的
Message.target == null
,则说明有异步(VIP)消息存在,尝试去找到异步消息 - 如果
now < msg.when
则说明还没到时间,需要继续睡眠,这里有两种情况- 存在同步屏障,异步消息的when时间还没到
- 队列的头的消息的when时间还没到,还记得之前有说过MessageQueue中的消息队列是按照when来排序的,所以这里只需要判断队列头的消息有没有到达即可,因为其余的when时间均大于队列头的时间
- 否则将消息移除(异步消息或者队列头的消息),并返回找到的msg
- 否则如果当前队列中没有消息或者存在同步屏障却没有异步消息,则说明没有符合条件的消息,设置nextPollTimeoutMillis为-1,表示永久阻塞,直到被生产者线程唤醒
- 如果正在退出,则调用
dispose()
释放native代码资源,并返回null - 获取当前IdleHandler,这里有两个条件
pendingIdleHandlerCount < 0
,这种情况代表Looper.loop()
里面调用的时候,而非next()方法中重新唤醒的情况- 没有消息返回,准备阻塞
- 判断
pendingIdleHandlerCount <= 0
,如果没有IdleHandler,则直接进行阻塞 - 创建IdleHandler的数组
mPendingIdleHandlers
,大小最小为4 - 将添加的IdleHandler
mIdleHandlers
转为mPendingIdleHandlers
数组 - 遍历
mPendingIdleHandlers
中的IdleHandler,调用其queueIdle()
的方法,并获取返回值keep
,如果为false则将其从mIdleHandlers
中移除 - 将
pendingIdleHandlerCount
和nextPollTimeoutMillis
设置为0,- 将
pendingIdleHandlerCount
设置为0的原因是防止下一次next()
的循环的时候没有符合条件的消息再次调用IdleHandler, - 将
nextPollTimeoutMillis
设置为0的原因是可能执行完IdleHandler之后,又有符合条件的消息插入
- 将
Handler.dispatchMessage()
在这个方法中存在一个分发消息优先级
- 判断Message有没有设置callback(Runnable类型),有的话优先调用
Message.callback.run()
- 判断Handler中有没有设置mCallback(Callback),如果这个
Callback.handleMessage()
返回true,则被截获了 - 最后才是
Handler.handleMessage()
调用,这里有两个条件- Message没有设置callback
- Handler没有设置mCallback或者设置了但是
Callback.handleMessage()
返回false
IdleHandler
关于IdleHandler的几个问题
-
IdleHandler如何使用,由下面的代码就可以看出IdleHandler的添加必须在Looper线程(消费者线程)当中
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } Log.e("Test","IdleHandler1 queueIdle"); return false; } });
-
IdleHandler什么时候回调,只在第一次出现没有符合条件的消息的时候才会进行回调,三种情况
- 插入了同步屏障,但是还没有插入异步消息
- 队列中没有消息,队列头
mMessages == null
- 队列头或者异步消息的when时间大于当前时间,
如果出现这三种情况,next()方法中的循环会继续,并且阻塞,如果阻塞被唤醒之后发现还是上面三种情况,则IdleHandler不回再次执行,直到返回Message之后Looper.loop()方法调用
MessageQueue.next()
并且将pendingIdleHandlerCount
重置为-1,注意在执行完了IdleHandler之后将pendingIdleHandlerCount设置为了0才进行阻塞的//当pendingIdleHandlerCount == 0的时候不会获取mIdleHandlers.size(),即使有上面三种情况,只有将pendingIdleHandlerCount设置为-1之后才可能 if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { pendingIdleHandlerCount = mIdleHandlers.size(); } //pendingIdleHandlerCount == 0的时候会直接阻塞 if (pendingIdleHandlerCount <= 0) { // No idle handlers to run. Loop and wait some more. mBlocked = true; continue; }
-
为什么不直接使用mIdleHandlers,而是将其转为数组呢?这里应该是为了好判断吧,因为mIdleHandlers调用了
remove()
方法之后mIdleHandlers.size()
会变化
同步屏障与异步消息
-
同步屏障与异步消息的关系,异步消息相当于VIP消息需要Looper优先获取,而同步屏障则是开启VIP消息通道的标志,其本身也是一个Message,只不过普通的消息的target必须是一个正常的Handler,而同步屏障的target则是null,Looper线程被唤醒当看到有同步屏障(
msg.target == nul
l)的时候说明MessageQueue的消息队列中可能有异步消息,则尝试从消息队列中获取异步消息,具体的步骤上面已经说过了 -
同步屏障与异步消息的使用,典型的例子就是
Choreographer
和ViewRootImpl
进行刷新UI的消息了//ViewRootImpl.scheduleTraversals() mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); //Choreographer.postCallbackDelayedInternal() Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); msg.arg1 = callbackType; msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); //ViewRootImpl.doTraversal() mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
可以分为三个步骤:
- 插入同步屏障
- 发送异步消息
- 处理异步消息,然后将同步屏障移除
这三个步骤的顺序不能更改,否则异步消息可能会被作为同步消息处理
MessageQueue.postSyncBarrier()
- 创建Message,并将Message.arg1设置为token,为后面
removeSyncBarrier()
准备 - 将同步屏障消息插入到消息队列中,逻辑其实和
enqueueMessage()
一样,保持消息队列以when进行排序
这里不会进行唤醒Looper线程(消费者线程),而是插入异步消息的时候尝试进行唤醒
发送异步消息
两种方式:
- 创建Handler调用
new Handler(true)
设置为异步的,这样Handler发送的每一个消息都是异步的 - 调用
Message.setAsynchronous(true)
设置单个消息为异步的
异步消息的发送过程和同步消息相同,回看MessageQueue.enqueueMessage()
相关逻辑,注意异步消息needWake
的变化
MessageQueue.removeSyncBarrier()
- 首先在队列中找到token相同的同步屏障,如果没找到则要报出异常
- 如果同步屏障不是队列头,则无需唤醒,因为此时队列的头是同步消息,由超时进行唤醒
- 如果是队列头,则有两个条件可能会造成唤醒
mMessages == null
这种情况不知道为什么要唤醒- 队列头部的消息不是同步屏障(Message.target != null)
总结
有一些问题的答案不够明显,再来总结一下之前问题的答案
-
Looper阻塞之后谁来唤醒它,哪些情况会唤醒Looper线程?
答:有四种情况会唤醒Looper线程
- 插入的消息when值在队列中最小,即直接将消息插入到队列头中
- 插入异步消息,并且队列的头就是同步屏障
- Looper线程自己唤醒,即阻塞超时了,另外一种特殊情况是如果队列中没有同步消息或者有同步屏障却没有异步屏障则永久阻塞直到被唤醒,
nextPollTimeoutMillis = -1
的情况 - 移除同步屏障的时候如果消息队列为空或者有同步消息,则进行唤醒
-
Handler线程如何进行切换的?
答:首先必须说明的是线程的执行是以方法为单位的,与对象无关。在生产者消费者模型中,一般会有两类线程,生产者线程和消费者线程,生产者线程和消费者线程可以是同一个线程,对于Handler来说,Handler是生产者,Looper是消费者,所以
Handler.enqueueMessage()
MessageQueue.enqueueMessage()
均是在生产者线程中执行Looper.loop()
MessageQueue.next()
以及Handler.dispatchMessage()
则是在消费者线程中执行
线程的切换其实就是唤醒的过程
-
Handler的消息优先级?
详细见
Handler.dispatchMessage()
的分析