Android Framework | 消息机制的冷门知识点,你能答出来几个?
作为Android的基础知识,消息机制已被无数人写过。笔者曾经也写过一篇深入分析的文章,但总体而言乏善可陈,并无新颖之处。最近恰好重新整理了一下思路,想着可以从细节的角度出发,对一些冷门的知识点做一个归纳。记录于此,供大家批评讨论。
本文所有代码基于Android Q (10.0)
1. 哪个消息在前?哪个消息在后?
假设线程1此时正在处理一个消息,线程2通过如下方式(方式Ⅰ)往线程1的消息队列中插入两个消息。请问消息A和消息B哪个先被处理呢?
handler.sendMessage(msgA);
handler.sendMessage(msgB);
那如果是通过下面这种方式(方式Ⅱ),消息A和消息B又是哪个先被处理呢?
handler.sendMessageAtFrontOfQueue(msgA);
handler.sendMessageAtFrontOfQueue(msgB);
答案是通过方式Ⅰ发送时,消息A先被处理;通过方式Ⅱ发送时,消息B先被处理。具体解释如下:
/frameworks/base/core/java/android/os/Handler.java
746 private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
747 long uptimeMillis) {
748 msg.target = this;
749 msg.workSourceUid = ThreadLocalWorkSource.getUid();
750
751 if (mAsynchronous) {
752 msg.setAsynchronous(true);
753 }
754 return queue.enqueueMessage(msg, uptimeMillis);
755 }
Handler类中所有的sendMessage方法,最终都是调用MessageQueue.enqueMessage方法将消息加入到队列之中。调用时传入两个参数:msg和uptimeMillis。msg自然是想要发送的消息,而uptimeMillis则是消息预计发送的时间。
SystemClock.uptimeMillis(): Returns milliseconds since boot, not counting time spent in deep sleep.
1.1 当我们调用sendMessage发送消息时
/frameworks/base/core/java/android/os/Handler.java
610 public final boolean sendMessage(@NonNull Message msg) {
611 return sendMessageDelayed(msg, 0);
612 }
669 public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
670 if (delayMillis < 0) {
671 delayMillis = 0;
672 }
673 return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
674 }
695 public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
696 MessageQueue queue = mQueue;
697 if (queue == null) {
698 RuntimeException e = new RuntimeException(
699 this + " sendMessageAtTime() called with no mQueue");
700 Log.w("Looper", e.getMessage(), e);
701 return false;
702 }
703 return enqueueMessage(queue, msg, uptimeMillis);
704 }
最终传入MessageQueue.enqueMessage的uptimeMillis是sendMessageDelayed中临时获取的当下时间。当消息B获取到的uptime大于A时,B在队列中必然插入到A的后面。但是由于uptimeMillis的单位是毫秒,所以A和B完全可能获取到一样的uptimeMillis(在一毫秒内完成两次消息发送的动作)。如果二者的uptimeMillis一样,那么他们的顺序又该怎么排列呢?
/frameworks/base/core/java/android/os/MessageQueue.java#enqueueMessage
569 if (p == null || when == 0 || when < p.when) {
570 // New head, wake up the event queue if blocked.
571 msg.next = p;
572 mMessages = msg;
573 needWake = mBlocked;
574 } else {
575 // Inserted within the middle of the queue. Usually we don't have to wake
576 // up the event queue unless there is a barrier at the head of the queue
577 // and the message is the earliest asynchronous message in the queue.
578 needWake = mBlocked && p.target == null && msg.isAsynchronous();
579 Message prev;
580 for (;;) {
581 prev = p;
582 p = p.next;
583 if (p == null || when < p.when) {
584 break;
585 }
586 if (needWake && p.isAsynchronous()) {
587 needWake = false;
588 }
589 }
590 msg.next = p; // invariant: p == prev.next
591 prev.next = msg;
592 }
一个消息若是加入到队列中来,只可能是两种情况:
- 消息加入到队列头部。
- 消息加入到别的消息后面。
对于第一种情况,假设A先加入到队列头部,只有当B的when(uptime)小于A的when时,B才会加入到A的前面;对于第二种情况,同样是只有B的when小于A的when时,B才会加入到A的前面。当B的when和A相等时,B只会加入到A的后面。因此,A先被处理。
1.2 当我们调用sendMessageAtFrontOfQueue发送消息时
/frameworks/base/core/java/android/os/Handler.java
718 public final boolean sendMessageAtFrontOfQueue(@NonNull Message msg) {
719 MessageQueue queue = mQueue;
720 if (queue == null) {
721 RuntimeException e = new RuntimeException(
722 this + " sendMessageAtTime() called with no mQueue");
723 Log.w("Looper", e.getMessage(), e);
724 return false;
725 }
726 return enqueueMessage(queue, msg, 0);
727 }
sendMessageAtFrontOfQueue和sendMessage最大的不同在于它传入的uptime为0。0作为一种特殊的uptime,它表示将消息加入到队列头部。
/frameworks/base/core/java/android/os/MessageQueue.java#enqueueMessage
569 if (p == null || when == 0 || when < p.when) {
570 // New head, wake up the event queue if blocked.
571 msg.next = p;
572 mMessages = msg;
573 needWake = mBlocked;
如上代码验证了这一点,当when == 0时,则不论队列中已有的消息是什么状态,新来的消息都会被添加到队首。因此,如果A已经被加入到队列中,当再次调用sendMessageAtFrontOfQueue将B加入队列时,B会抢占A的队首位置,因此B先被处理。
2. mIdleHandlers为什么要拷贝数组?
/frameworks/base/core/java/android/os/MessageQueue.java
338 synchronized (this) {
......
......
394 mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
395 }
396
397 // Run the idle handlers.
398 // We only ever reach this code block during the first iteration.
399 for (int i = 0; i < pendingIdleHandlerCount; i++) {
400 final IdleHandler idler = mPendingIdleHandlers[i];
401 mPendingIdleHandlers[i] = null; // release the reference to the handler
402
403 boolean keep = false;
404 try {
405 keep = idler.queueIdle();
406 } catch (Throwable t) {
407 Log.wtf(TAG, "IdleHandler threw exception", t);
408 }
409
410 if (!keep) {
411 synchronized (this) {
412 mIdleHandlers.remove(idler);
413 }
414 }
415 }
mIdleHandlers是ArrayList
关键的原因在于两点:
- mIdleHandlers中成员的更改必须要持有this(MessageQueue对象)同步锁,否则可能造成线程间的数据竞争状态。
- IdleHandler.queueIdle方法的执行时间是未知的,因此不能在持有this同步锁的状态下去执行该方法。如果是持有this同步锁,且queueIdle方法的执行时间过长,那么其他调用Handler.sendMessage的线程将可能因为等锁而发生阻塞。这其实是多线程编程中一条基本法则:在同步块中只做必要的事,少做耗时的事。
基于以上两点要求,只能通过复制数组的方式,既保证mIdleHandlers的更新被this同步锁保护,又将可能的耗时操作从同步块中挪出。
3. 当消息队列头部是已激活的同步屏障时,还能够处理同步消息么?
同步屏障一个主要的作用就是屏蔽队列中已有的同步消息,使得它们无法被及时处理。通过这种方式可以将线程的执行权让渡给异步消息,从而让异步消息享受到VIP的待遇。
由于同步屏障的本质也是一个消息(该消息target为null,以区别开普通消息),所以只要后续的同步消息添加在它前面,就依然可以正常得到处理。
对于一个已经激活的同步屏障而言,将一个新的同步消息添加在它之前最简单的方法就是sendMessageAtFrontOfQueue。通过该方法可以将一个同步消息添加到队首,因此也就获得了被处理的权利。
4. Delivery Time 和 Dispatch Time
- Delivery Time = Dispatch Start - message.when,表示该消息实际轮询与理论轮询的时间差。
- Dispatch Time = 消息的实际处理时间。
对于system_server进程而言,其主线程、UiThread和FgThread都会设置slow detection。当线程中消息的delivery time或dispatch time大于阈值时,将会有相应的warning log输出。
默认dispatch的阈值是100ms,而delivery的阈值是200ms。
Slow dispatch warning:
Slow delivery warning:
03-17 02:57:06.409 914 1243 W Looper : Drained
Delivery time的warning log有一种特殊形式:Drained。
估计很多人看到这句Log有点不知所云,这其实是Android为了减少无效输出所做的优化。此话怎讲?
一旦某个消息的delivery time超过阈值,便意味着两种可能:
- 前面有太多的消息需要处理,虽然每个消息处理时间都不长,但是雪崩来了,没有一片雪花是无辜的。
- 前面某些消息的处理时间过长。
这两种情况的本质都是前面的消息对当前消息的影响,因此这种影响具有传递性。当一个消息报出slow delivery time的警报时,它后面的消息大概率也会报出这个警报。但是这些警报本质上反映的问题是同一个,所以为何要将重复的信息输出多次呢?
为了减少slow delivery警报重复输出,Android采用如下代码进行过滤:
/frameworks/base/core/java/android/os/Looper.java
231 if (slowDeliveryDetected) {
232 if ((dispatchStart - msg.when) <= 10) {
233 Slog.w(TAG, "Drained");
234 slowDeliveryDetected = false;
235 }
236 } else {
237 if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
238 msg)) {
239 // Once we write a slow delivery log, suppress until the queue drains.
240 slowDeliveryDetected = true;
241 }
242 }
当slow delivery的警告输出一次以后,后面的消息即便超出阈值,也不会再输出log。只到当某一个消息的delivery time ≤ 10ms时,才会输出一句新的log:Drained。
Drained的原意是排干、耗尽。用在这里表示原本拥塞的MessageQueue现在已经变得顺畅,当前消息的delivery time≤10ms表示此前消息的负面影响已经消散。所以当这句话输出以后,新一轮的slow delivery检测又重新开始生效。
另外,由于delivery time的计算需要message.when的参与,而通过sendMessageAtFrontOfQueue发送的消息其when为0。所以对于这一类消息,是不会有slow delivery的警报的。