最近面试问到了这个问题,感觉自己答得不是很好,特此写一下总结,如果想快速了解原理直接拖到总结就行。
1.下载源码
直接进入handle源码,发现其内部就一行throw new RuntimeException(“Stub!”),但是实际运行中并没有抛出该错误,该方法也并没有语法报错。因此可能是系统设计者故意隐藏此部分的实现源码。
使用的Android Studio或者其他IDE看jar包的源码的时候,编译工具只不让你看方法的实现,原因有下: 1.Android SDK自带的Source源码包很小,并没有包括所有的Android Framework的源码,仅仅提供给应用开发参考用,一些比较少用的系统类的源码并没有给出,所以有时候你会看到throw new RuntimeException(“Stub!”)。 2.出于安全或者某些原因,这些API不能暴露给应用层的开发者,当然,这些API在ROM中是实际存在的,有些开发者发现了一些可以修改系统行为的隐藏API,在应用层通过反射的方式强行调用这些API执行系统功能,这种手段也是一种HACK。
我们直接下载就好了。
2.sendMessageDelayed (Message msg, long delayMillis)
我们知道要发送延时消息,用的是 boolean sendMessageDelayed (Message msg, long delayMillis) 这个接口,API文档的描述其实已经给了答案: 翻译一下就是 调用这个方法时会将这个message根据delayMillis插入到MessageQueue的合适地方。 sendMessageAtTime最终会调用到queue.enqueueMessage。
3.queue.enqueueMessage
我们再来看一下queue.enqueueMessage 有点长,首先是一些属性的非空判断,比如我们需要通过target值来判断是哪个handler发过来的消息的。
PS:(handler的同步屏障就是一个target为空的msg,用来优先执行异步方法的)
同步屏障有一个很重要的使用场所就是接受垂直同步Vsync信号,用来刷新页面view的。因为为了保证view的流畅度,所以每次刷新信号到来的时候,要把其他的任务先放一放,优先刷新页面。
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;
}
when参数的含义是:分发当前Message的具体时间 根据when然后把消息加入到MessageQueue的消息队列里面,分2种情况处理
1.队列为空,直接插入并返回 2.队列非空,加入队列,接下来主要就是将这个msg根据实际执行时间进行排序插入到queue里面(看里面的for循环)。 好了,现在queue也构建完成了,假设我现在第一条消息就是要延迟10秒,怎么办呢。
如果我现在是looper,我要遍历这个messageQueue,那肯定要调用next方法。
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;
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);
}
// ……………
可以看到这里也是一个for循环遍历队列**,核心变量就是nextPollTimeoutMillis**。可以看到,计算出nextPollTimeoutMillis后,此时调用,nativePollOnce(long ptr, int timeoutMillis),入参nextPollTimeoutMillis即为需要延迟的时间,等待延迟时间后在触发获取Message,其中是具体是调用了epoll_wait这个方法。
4.epoll_wait
这是linux的一个I/O多路复用机制,
Linux里的I/O多路复用机制:举个例子就是我们钓鱼的时候,为了保证可以最短的时间钓到最多的鱼,我们同一时间摆放多个鱼竿,同时钓鱼。然后哪个鱼竿有鱼儿咬钩了,我们就把哪个鱼竿上面的鱼钓起来。这里就是把这些全部message放到这个机制里面,那个time到了,就执行那个message。 我们来看看epoll_wait的声明。
int epoll_wait(int epfd,struct epoll_event * events, int maxevents,int timeout);
epoll_wait()系统调用等待文件描述符epfd引用的epoll实例上的事件。事件所指向的存储区域将包含可供调用者使用的事件。 epoll_wait()最多返回最大事件。 maxevents参数必须大于零。 timeout参数指定epoll_wait()将阻止的最小毫秒数。 (此间隔将四舍五入为系统时钟的粒度,并且内核调度延迟意味着阻塞间隔可能会少量溢出。) 指定超时值为-1会导致epoll_wait()无限期阻塞,而指定的超时时间等于零导致epoll_wait()立即返回,即使没有可用事件。
当插入一条消息后,会唤醒epoll_wait方法,从native层回到framework的next方法 我们最初在sendMessageDelayed里面设置的delayMillis 当delay的时间到了后,就和正常的消息处理流程完全一样了。 那如果我在这段时间又插入了一个新的message怎么办,所以handler每次插入message都会唤醒线程,重新计算插入后,再走一次这个休眠流程。
5.其他问题
1.如果队列中有延时消息和普通消息两种怎么办? 消息队列虽然叫队列,但是其实是一个单链表,Handler可以调用sendMessageAtFrontOfQueue(Messagemsg),postAtFrontOfQueue(Runnable r),将消息插入队头,最先取出,最先执行,之后再处理队列中的其他消息。 如果队列中只有延迟消息,此时发送一个普通消息,普通消息会插入队头,最先处理,而不会等延迟消息取出后,再取出普通消息。 只有当表头来了新消息,才会唤醒Loop来获取,Message要么立即执行,要么Loop刷新自我唤醒的定时器继续睡眠。 2.Handler发送消息的 Delay 可靠吗? 面试的时候我以为是要精确发送的,但是 答案是不靠谱的,引起不靠谱的原因有如下
1.发送的消息太多,Looper负载越高,任务越容易积压,进而导致卡顿 2.消息队列有一些消息处理非常耗时,导致后面的消息延时处理
3、当整个链表都是延迟执行的message时,如果此时插入的message也是延时执行的,是否一定要唤醒呢?
如果插入的message并非插入表头,说明拿的下一个message也不是自己,完全可以让线程继续休眠,没有必要唤醒,因为此时的定时器到期唤醒后拿到的正是待返回和执行的表头message。
总结:
1.传入延时时间,调用对应方法 sendMessageDelayed(Message msg, long delayMillis)最终调用的是enqueueMessage()将msg插入队列中。即不管发送的消息有没有延迟,都会先插入队列中,如果有延迟的话,则会在一个for的死循环中遍历消息队列并将传入消息msg插到单链表中合适的位置。 2.构造队列 事实上,消息队列是按照消息处理的时间when,按照从近到远的顺序排列的,最先要执行的任务放在消息队列的头部,依次排列。 3.唤醒线程 Looper.loop()通过MessageQueue中的next()去取消息,调用nativiePollOnce这个native方法,其内部会根据传入的nextPollTimeoutMillis,在延迟这么长时间之后唤醒线程从消息队列中读取消息,nativePollOnce函数内部会调用epoll_wait方法,设置超时时间为nextPollTimeoutMillis,epoll_wait在这个超时时间之后,就会唤醒线程,开始处理消息队列中的消息。
精简版总结:
消息队列在插入消息的时候是按照消息的触发时间顺序排序的,先执行的消息放在单链表的头部,最后执行的消息放在单链表的尾部;
在消息执行的过程中,通过native层设置epoll_wait的超时事件,使其在特定时间唤醒线程开始出现消息。
感谢网上大佬们的博客给我了足够的参考,如果有侵权请联系我删除,欢迎指出文章中的不足之处以及讨论,您的建议是我进步的动力,非常感谢您的阅读!
|