一、四大组件概述
Android的消息机制是由Handler、Message、MessageQueue,Looper四个类支撑,撑起了Android的消息通讯机制,Android是一个消息驱动系统,由这几个类来驱动消息与事件的执行
Handler:
- 用来发送消息和处理消息
- 无论使用的post ,还是send,都会执行enqueueMessage 方法,将消息加到队列中
- 发送的消息并不是立刻得到执行的,所以必须有个地方把它存起来,也就是MessageQueue
MessageQueue
- 基于单向链表,以触发时间的顺序排放在队列中,链表头部信息被触发的时间是最接近的
- 队列中的消息怎样让它在必要的时间得到执行,就需要依靠Lopper
有三种消息类型:
BarrierMessage:屏障消息
AsyncMessage:异步消息
Message:同步消息
Lopper
- 无限循环驱动器
- 在它内部有个loop 方法,开启无限循环,从头遍历这个队列,检查满足条件的消息,有就把它取出来进行分发执行,没有或者消息为空时,当前线程就会进入阻塞状态,从而释放掉CPU 的资源占用,当有新消息进来的时候就会唤醒当前线程,从而继续遍历队列中是否有满足条件的消息,所以Looper并不会真的一直无限循环下去
Message
消息
- long when:该消息被执行的时间戳,这个时间戳是它在队列中排队的唯一依据
- Message next:消息队列是一条单向链表,每一条消息都会包含下一条消息的引用关系,从而形成单向链表
- Handler target:代表着该message是由哪一个handler发送的,在消费这条消息时也由这个target来消费
- 一个线程最多存在一个Lopper
- 一个Looper对应一个MessageQueue
- 一个MessageQueue中可以存在多个Message对象
- 一个MessageQueue 或者一个Looper 对应多个handler
二、消息分发的优先级
- Message的回调方法:message.callback.run() 优先级最高
- Handler的回调方法:Handler.mCallback.handleMessage(msg)
- Handler的默认方法:handler.handleMessage(msg)
//1.直接在Runnable中处理任务
handler.post(runable = Runnable {
//这条消息在消费时,首先回调给Message中的callback,也就是runable对象
})
//2.使用Handler.callback来接收处理消息
val handler = object :Handler(callback{
//在创建handler的时候是可以传递一个callback 的,在消息分发的时候首先把消息分发给callback 来处理
return@callback true
})
//3.最常用的handler的handlerMessage方法
三、疑问点
1.在使用handler的时候并没有指定Looper ,这三个类是怎么关联起来的
在创建实例对象时虽然没有传递Looper对象,但是在构造函数的重载里会调用Looper.myLooper来获取当前线程绑定的Looper对象
2.主线程的Looper是在哪里创建的?
在ActivityThread的main方法中调用Looper.prepareMainLooper()创建了主线程的Looper对象,然后调用loop()开启消息队列循环,所以在主线程中创建Handler不用给他创建Looper
如果一个线程的Looper对象没有调用prepare 方法,它的Looper是为空的
3.Looper.myLooper是如何保证获取到的Looper是当前线程的Looper 对象?
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));
}
无论是主线程的Looper 还是子线程的Looper都只能调用prepare()方法或者prepareMain()方法,在prepare()方法中首先判断在这个当前线程这个Looper对象是否已经被创建过了,如果是,再次调用该方法就会报错。
这就是一个线程最多只能存在一个Looper 对象的保证
接下来把Looper 对象保存到了ThreadLocal中,在获取Looper 时也是从ThreadLocal 这个对象中得到的
ThreadLocal:
是用来存储数据的,使其成为线程的私有局部变量,通过它提供的get、set来访问
好处:可以在线程的任意地方去访问这个局部变量,不用传来传去
4.如何让子线程拥有消息分发的能力?如何在子线程中弹出Toast?
默认子线程是没有Looper 对象的,要主动调用Looper.prepare 方法以及 Looper.loop方法,在这两个方法中间就可以弹出Toast 了,在接着给它创建一个handler,在主线程中拿到这个handler对象,就可以处理在子线程中处理主线程发送过来的消息了
Toast在没有显示出来之前,它还没有被添加到窗口上,对它的操作就不会触发线程的检查
实现方式跟ActivityThread方式一样
需要注意的是一旦给子线程执行了这两个方法,要在必要的时候调用Looper.quit 方法,让Looper 退出循环,否则会一直循环下去,这个线程就不会被销毁,不会被回收
四、消息入队
一条消息被插入到MessageQueue时做了哪些事情?
发送一条消息时主要有两种方法,一种是post开头的,一种是send开头的,无论使用的哪种方式发送,都会以Message的形式插入到队列中
对于post,发送一条消息时,都会通过getPostMessage,把runnable对象包装成Message对象,再次调用sendMessageDelayd方法把它加入到队列中
享元设计模式,共用已创建的对象 避免重复创建对象
getPostMessage方法中在获取Message对象时,使用的是Message.obtain()方法
Message提供了消息复用池的能力,最多会缓存50条Message对象,通过.obtain()方法来获取 Message对象是可以复用的,不需要每次都去创建一个新的
采用了链表的形式来管理消息池,链表的插入和删除比ArrayList快
还提供了消息回收能力,当消息被分发完了之后就会调用recycleUnchecked,将Message的对象进行重置,情况数据,并将其插入到链表的头节点中,它是一个队头复用机制
通过new Message出来的对象会出现大量的临时的Message对象,会导致内存占用率过高
消息入队,按照消息被执行的时间戳when插入队列
- 无论是post、还是send,最终都会执行enqueueMessage,把消息插入到队列中
- postSyncBarrier:发送一条屏障消息
新消息插入时按照消息插入的时间插入到队列中
enqueueMessage
Handler.enqueueMessage
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
……
return queue.enqueueMessage(msg, uptimeMillis);
}
将Message的target赋值成当前的Handler,在下面会对这个target进行判空,空的话直接抛出异常
消息被消费的时候会通过handlerDispatchMessahe方法来完成
MessageQueue.enqueueMessage
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;
//mMessage始终是队头消息,拿到队头,这个队列才能执行增删改查的操作
Message p = mMessages;
boolean needWake;
//p == null 说明队头为null,则队列为空
//when == 0 或者新消息触发的时间戳为0
// when < p.when 或者消息被触发的时间小于队头消息被触发的时间
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
//满足条件就把新消息插入到队列的头部
//把新消息的next节点指向原来的头结点,然后将新信息赋值给头消息
msg.next = p;
mMessages = msg;
//如果当前Looper处于休眠状态,则本次插入消息之后需要唤醒
//mBlocked:是否处于阻塞状态,只有对列为空,队列当中没有可处理消息的时候,线程才会进入阻塞状态,mBlocked才会为true
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:新消息是异步消息
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
//for循环:找到一个合适的位置来插入这条新消息
for (;;) {
prev = p;
p = p.next;
//找到合适的位置退出for循环
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
//调整链表中节点的指向位置,实现消息插入队列的目的
//msg:新消息 p:下一条消息 prev:上一条消息
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
//Looper被唤醒
if (needWake) {
//线程被唤醒,使用nativeWake native方法
nativeWake(mPtr);
}
}
return true;
}
enqueueMessage在插入一条新消息时,主要是检查消息是否都具备target对象,否则消息是无法被处理的,还会选择性的决定唤醒当前的线程,继续轮询队列是否有符合条件的消息拿出来处理
postSyncBarrier 同步屏障消息
message.target == null ,这类消息不会真的执行,起到标记作用
MessageQueqe在遍历消息队列时,如果队头是同步屏障消息,那么会忽略同步消息,优先让异步消息得到执行
一般异步消息和同步屏障消息会一同使用
异步消息 & 同步屏障
使用场景:
- ViewRootImpl接收屏幕垂直同步信息事件用于驱动UI测绘
- ActivityThread接收AMS的事件驱动生命周期
- InputMethodMessage分发软键盘输入事件
- PhoneWindowManager分发电话页面各种事件
目的:
让重要的消息尽可能早的得到执行
注意:
- 开发过程中无法使用,只能系统源码使用
- 必须使用MessageQueue来发生,Handler是无法发送同步屏障消息的,只能用来发送异步消息和同步消息
MessageQueue#postSyncBarrier
public int postSyncBarrier() {
//使用uptimeMillis,意思是:发送了这条消息,在这期间如果设备进入休眠状态,那么消息是不会执行的,设备被唤醒之后才会执行
return postSyncBarrier(SystemClock.uptimeMillis());
}
- currentTimeMillis() 系统当前时间,即日期时间,可以被系统设置修改,如果设置系统时间,时间值也会发生改变
- uptimeMillis() 自开机后,经过的时间,不包括深度休眠的时间
sendMessageDelay.postDelay 也都使用了这个时间戳
问题:
如果使用handler发送一条消息,然后让设备进入休眠,也就是先熄屏,然后长时间不操作手机,这条消息会不会得到执行,为什么?
进入休眠之后,消息是不会被触发的,因为设备休眠之后uptimeMillis是不会被累加的
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
//从消息池复用,构建新消息体
final Message msg = Message.obtain();
msg.markInUse();
//并没有给target赋值
//区分是不是同步屏障,就看target是否等于null,等于null,就是同步屏障消息
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
//遍历队列所有消息,直到找到一个message.when > msg.when 的消息,决定新消息插入的位置
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
//如果找到了合适的位置则插入
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
//如果没有找到直接放队头
msg.next = p;
mMessages = msg;
}
return token;
}
}
屏障消息在插入队列时是没有主动唤醒线程的,因为屏障消息并不需要得到执行,也不需要唤醒这个线程去轮询它
屏障消息的移除,谁添加的就由谁来移除
比如ViewRootImpl,在接收到垂直同步信号的到达,发送一条异步消息,并发送了一条屏障消息,当接收到异步消息时,ViewRootImpl就会把同步屏障消息从队列中移除
问题:
ViewRootImpl是如何在UI测绘的工作优先得到执行的?
发送了同步屏障和异步消息
五、消息分发
Looper#loop()
队列中的消息之所以能得到分发,是由于Looper中的loop方法,会开启一个无限for循环消息的驱动器,在这个无限for循环中会调用MessageQueue的next方法,去获取一个可执行的msg对象
这个next方法可能使得当前线程进入一个阻塞的状态,此时这个方法不会有返回值,下面的分发代码就不会得到执行,所以这个无限for循环并不会一直空轮询下去,
目的:只是不让这个线程退出,因为一个线程任务执行完成,就会自动退出,如果想让它不退出,开启一个while循环等待一段时间,这里也是一样的道理
直到队列中有可处理的消息才会返回
for(;;){
//取出队列中的消息
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}
……
//分发消息
msg.target.dispatchMessage(msg);
……
//回收Message
msg.recycleUnchecked();
}
拿到消息之后调用msg.target.dispatchMessage(msg) 去分发消息
当消息处理完成之后调用msg.recycleUnchecked()方法回收Message,留着复用
总结:
loop方法的作用是从队列中去取消息,然后分发,然后回收
MessageQueue#next()
首先也是开启一个for循环,在消息分发时可能存在消息的插入、删除,而且队列是一个单向链表无法确切知道消息插入时是有多少的,当这个next方法找到一个合适的消息时就会退出for循环
int nextPollTimeoutMillis = 0;
for (;;) {
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;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
//找到了一条消息,但是它的时间,还没到需要执行的时机
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
//prevMsg不为空说明需要删除的是中间的消息,只需要上一条消息的next指向msg的next,为空说明要删除的这条消息是队头消息
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
//msg对象为空,说明队头对象为空,也就是当前队列为空,此时把nextPollTimeoutMillis置为-1,looper将进入永久休眠,直到有新消息到达
nextPollTimeoutMillis = -1;
}
}
通过nativePollOnce这个native方法进行阻塞,传递的nextPollTimeoutMillis,如果这个值是大于0的,就会使得当前线程进入阻塞,并且释放掉对CPU资源的使用权,尽管Looper中有个无限for循环,但是不会造成CPU资源的过多占用
由于nextPollTimeoutMillis第一次for循环时是等于0的,所以第一次不会使得这个线程进入阻塞的,但是如果在接下来的循环中没有找到一个合适的、需要处理的消息,这个nextPollTimeoutMillis会被更新,在第二次循环的时候如果这个值不等于0,这个线程就会进入阻塞状态,如果这个值等于-1,当前线程就会进入无限阻塞状态,直到有新消息插入时,才会被唤醒
nextPollTimeoutMillis等于多少,这个线程就会被阻塞多少ms,超时之后就会继续执行以下代码
延迟消息是怎么得到保证的?
首先是通过uptimeMillis去计算出应该被执行的时间戳,然后借助nativePoll阻塞一段时间,超时之后自动恢复 ,然后继续往下检查是否有满足条件的消息,如果有就拿出来去执行
如何去取消息?
把队头消息赋值给一个临时变量msg,防止插入消息,队头可能被改变,然后判断msg是否是同步屏障消息(也就是判断msg.target == null),如果是通过do-while循环,在while中判断这个msg 不是一个异步消息,也就是说如果这个消息是异步消息,就会退出do-while循环
这个屏障消息唯一的作用就是:当它处于队头时,next方法在检索消息时会跳过同步消息,会优先检索出所有的异步消息,让它们优先执行
msg.target对象如果不为空,不会执行do-while循环,就会按照时间顺序来检索分发消息
判断消息msg是否为空,说明队头对象为空,也就是当前队列为空,此时把nextPollTimeoutMillis置为-1,looper将进入永久休眠,线程进入无限阻塞状态,直到有新消息到达
不为空时,也就是找到了一条消息,检查是否到达需要执行的时机
如果没有,去更新nextPollTimeoutMillis值,等于应该执行的时间 - 当前时间 ,计算出还应该延迟多久
否则说明找到了这条需要处理的消息,首先需要从队列中移除掉
preMsg不等于null,说明被删除的这条消息是队列中间的这条消息,preMsg代表被删除消息的前一条消息,删除这条消息,只需要将preMsg的next节点指向这个消息的next节点
preMsg等于null,说明被删除的这条消息是队头消息,需要将mMessage指向要删除消息的下一个节点
最后把msg对象返回回去,让它去分发,去执行
idler.queueIdle()
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
判断队头Message是否为空,也就是队列中没有任务了,或者队头消息时间还没有到达可执行的时机,在第二次for循环中,线程即将进入阻塞状态,在进入阻塞状态之前,称之为空闲状态,判断有没有向MessageQueue中注册IdleHandler,用于监听这个状态
IdleHandler:
可以监听当前线程是否即将进入空闲状态,也就是说通过事件的监听来做一些延迟的初始化,以及数据加载、日志上报等工作,而不是有任务就提交,从而避免抢占重要的资源
如果pendingIdleHandlerCount大于0,就会去调用idler.queueIdle()方法
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
最后会将nextPollTimeoutMillis延迟时间置为0,既然业务层监听了线程的空闲状态,在queueIdle这个方法里,就有可能再次产生新的消息,为了让新消息尽可能早的得到执行,此时不需要让线程进入休眠了
nextPollTimeoutMillis = 0;文章来源:https://www.toymoban.com/news/detail-833003.html
在Android中存在两套消息机制,一套是Java的,一套是C++的,本质上是独立的,在Java中MessageQueue调用nativePollOnce的主要原因是借助native消息机制所实现的线程阻塞能力文章来源地址https://www.toymoban.com/news/detail-833003.html
六、问题解惑
到了这里,关于Android的消息机制--Handler的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!