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的警報的。