天天看點

Handler 消息隊列中的同步屏障——Message

Message 分為3種:普通消息(同步消息)、屏障消息(同步屏障)和異步消息。我們通常使用的都是普通消息,而屏障消息就是在消息隊列中插入一個屏障,在屏障之後的所有普通消息都會被擋着,不能被處理。不過異步消息卻例外,屏障不會擋住異步消息

是以可以這樣認為:屏障消息就是為了確定異步消息的優先級,設定了屏障後,隻能處理其後的異步消息,同步消息會被擋住,除非撤銷屏障。

那什麼是同步屏障機制呢?

同步屏障機制是一套為了讓某些特殊的消息得以更快被執行的機制。

  • 注意這裡我在同步屏障之後加上了機制二字,原因是單純的同步屏障并不起作用,他需要和其他的Handler元件配合才能發揮作用。
  • 這裡我們假設一個場景:我們向主線程發送了一個UI繪制操作Message,而此時消息隊列中的消息非常多,那麼這個Message的處理可能會得到延遲,繪制不及時造成界面卡頓。同步屏障機制的作用,是讓這個繪制消息得以越過其他的消息,優先被執行。
  • MessageQueue中的Message,有一個變量isAsynchronous,他标志了這個Message是否是異步消息;标記為true稱為異步消息,标記為false稱為同步消息。同時還有另一個變量target,标志了這個Message最終由哪個Handler處理。

那麼這些同步消息什麼時候可以被處理呢?

那就需要先移除這個同步屏障,即調用 removeSyncBarrier()

我們的手機螢幕重新整理頻率有不同的類型,60Hz、120Hz 等。60Hz 表示螢幕在一秒内重新整理 60 次,也就是每隔 16.6ms 重新整理一次

  • 螢幕會在每次重新整理的時候發出一個 VSYNC 信号,通知 CPU 進行繪制計算。
  • 具體到我們的代碼中,可以認為就是執行 onMesure()、onLayout()、onDraw() 這些方法。好了,大概了解這麼多就可以了。

了解過 view 繪制原理的讀者應該知道,view 繪制的起點是在 viewRootImpl.requestLayout() 方法開始,這個方法會去執行上面的三大繪制任務,就是測量布局繪制;但是,重點來了:

調用 requestLayout() 方法之後,并不會馬上開始進行繪制任務,而是會給主線程設定一個同步屏障,并設定 ASYNC 信号監聽;當 ASYNC 信号的到來,會發送一個異步消息到主線程 Handler,執行我們上一步設定的繪制監聽任務,并移除同步屏障

這裡我們隻需要明确一個情況: 調用 requestLayout() 方法之後會設定一個同步屏障,知道 ASYNC 信号到來才會執行繪制任務并移除同步屏障

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n99" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n208" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">api29
 @Override
 public void requestLayout() {
 if (!mHandlingLayoutInLayoutRequest) {
 checkThread();
 mLayoutRequested = true;
 scheduleTraversals();//發送同步屏障
 }
 }</pre></pre>           

那,這樣在等待 ASYNC 信号的時候主線程什麼事都沒幹?

是的;這樣的好處是:

保證在 ASYNC 信号到來之時,繪制任務可以被及時執行,不會造成界面卡頓。但這樣也帶來了相對應的代價:

  • 我們的同步消息最多可能被延遲一幀的時間,也就是 16ms,才會被執行
  • 主線程 Looper 造成過大的壓力,在 VSYNC 信号到來之時,才集中處理所有消息

改善這個問題辦法就是:使用異步消息

當我們發送異步消息到 MessageQueue 中時,在等待 VSYNC 期間也可以執行我們的任務,讓我們設定的任務可以更快得被執行且減少主線程 Looper 的壓力

異步消息機制本身就是為了避免界面卡頓,那我們直接使用異步消息,會不會有隐患?

這裡我們需要思考一下,什麼情況的異步消息會造成界面卡頓: 異步消息任務執行過長、異步消息海量

  • 如果異步消息執行時間太長,那即時是同步任務,也會造成界面卡頓,這點應該都很好了解
  • 其次,若異步消息海量到達影響界面繪制,那麼即使是同步任務,也是會導緻界面卡頓的
  • 原因是 MessageQueue 是一個連結清單結構,海量的消息會導緻周遊速度下降,也會影響異步消息的執行效率。

是以我們應該注意的一點是:

不可在主線程執行重量級任務,無論異步還是同步

那,我們以後豈不是可以直接使用異步 Handler 來取代同步 Handler 了?是,也不是

  • 同步 Handler 有一個特點是會遵循與繪制任務的順序,設定同步屏障之後,會等待繪制任務完成,才會執行同步任務;
  • 而異步任務與繪制任務的先後順序無法保證,在等待 VSYNC 的期間可能被執行,也有可能在繪制完成之後執行。
  • 建議是:如果需要保證與繪制任務的順序,使用同步 Handler;其他,使用異步 Handler

在 Android 系統裡面為了更快響應UI重新整理在 ViewRootImpl.scheduleTraversals 也有應用:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n130" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n228" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;"> android.view.ViewRootImpl#scheduleTraversals
 void scheduleTraversals() {
 if (!mTraversalScheduled) {
 mTraversalScheduled = true;
 //開啟同步屏障
 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
 //發送異步消息
 mChoreographer.postCallback(
 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
 if (!mUnbufferedInputDispatch) {
 scheduleConsumeBatchedInput();
 }
 notifyRendererOfFramePending();
 pokeDrawLockIfNeeded();
 }
 }</pre></pre>           

postCallback() 最終走到了 ChoreographerpostCallbackDelayedInternal():

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n132" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n230" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;"> 最後調用 android.os.MessageQueue#enqueueMessage
 boolean enqueueMessage(Message msg, long when) {}</pre></pre>           
Handler 消息隊列中的同步屏障——Message

同步屏障是通過 MessageQueue的postSyncBarrier 方法插入到消息隊列的

  • MessageQueue 中的 Message,有一個變量 isAsynchronous,他标志了這個 Message 是否是異步消息;标記為 true 稱為異步消息,标記為 false 稱為同步消息
  • 同時還有另一個變量 target,标志了這個 Message 最終由哪個 Handler 處理

MessageQueue #postSyncBarrier 方法的源碼如下:

重點在于: 沒有給 Message 指派 target 屬性,且插入到 Message 隊列頭部 這個 target==null 的特殊 Message 就是同步屏障

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n142" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n235" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">android.os.MessageQueue#postSyncBarrier()
public int postSyncBarrier() {
 return postSyncBarrier(SystemClock.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++;
 //1、屏障消息和普通消息的差別是屏障消息沒有tartget。
 final Message msg = Message.obtain();
 msg.markInUse();
 msg.when = when;
 msg.arg1 = token;

 Message prev = null;
 Message p = mMessages;
 //2、根據時間順序将屏障插入到消息連結清單中适當的位置
 if (when != 0) {
 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;
 }
 //3、傳回一個序号,通過這個序号可以撤銷屏障
 return token;
 }
}</pre></pre>           
  • postSyncBarrier 傳回一個 int 類型的數值,通過這個數值可以撤銷屏障
  • postSyncBarrier方法是私有的,如果我們想調用它就得使用反射
  • 插入普通消息會喚醒消息隊列,但是插入屏障不會

添加異步消息有兩種辦法:

  • 使用異步類型的Handler發送的全部Message都是異步的
  • 給Message标志異步.
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n156" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n243" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;"> public Handler(boolean async) {
 this(null, async);
 }
 final boolean mAsynchronous;
 if (mAsynchronous) {
 msg.setAsynchronous(true);
 }</pre></pre>           

異步類型的 Handler 構造器是标記為 hide,我們無法使用,但在 api28 之後添加了兩個重要的方法:

  • 通過這兩個 api 就可以建立異步 Handler 了,而異步 Handler 發出來的消息則全是異步的
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n161" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n245" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">public static Handler createAsync(@NonNull Looper looper) {
 if (looper == null) throw new NullPointerException("looper must not be null");
 return new Handler(looper, null, true);
}


public static Handler createAsync(@NonNull Looper looper, @NonNull Callback callback) {
 if (looper == null) throw new NullPointerException("looper must not be null");
 if (callback == null) throw new NullPointerException("callback must not be null");
 return new Handler(looper, callback, true);
}

public void setAsynchronous(boolean async) {
 if (async) {
 flags |= FLAG_ASYNCHRONOUS;
 } else {
 flags &= ~FLAG_ASYNCHRONOUS;
 }</pre></pre>           

總結

  • 同步屏障可以通過 MessageQueue.postSyncBarrier 函數來設定。該方法發送了一個沒有 target 的 Message 到 Queue 中
  • 在 next 方法中擷取消息時,如果發現沒有 target 的 Message,則在一定的時間内跳過同步消息,優先執行異步消息。
  • 再換句話說,同步屏障為 Handler 消息機制增加了一種簡單的優先級機制,異步消息的優先級要高于同步消息
  • 在建立 Handler 時有一個 async 參數,傳 true 表示此 handler 發送的時異步消息。ViewRootImpl.scheduleTraversals 方法就使用了同步屏障,保證 UI 繪制優先執行

這篇文章,其實不難;主要是将 Android 消息屏障機制 IdelHandler (同步屏障 sync barrier,異步消息 )以及在開發當中經常用到的一些知識點,總結了一下

想要往向更深入學習難免需要尋找很多的學習資料輔助,我在這裡推薦網上整合的一套 《 Android Handler 消息機制學習手冊》;鑒于出自大佬之手,可以幫助到大家, 能夠少走些彎路

Handler 機制之 Thread

Handler 消息隊列中的同步屏障——Message

Handler 機制之 Thread Local

Handler 消息隊列中的同步屏障——Message

Handler 機制之 Callable、Future 和 Fulure Task

Handler 消息隊列中的同步屏障——Message
篇幅原因,就不在這裡為大家贅述了,有需要的小夥伴:可以私信發送"面試"即可領取這份《 Android Handler 消息機制學習手冊》,助你早日成為底層原理大師!
Handler 消息隊列中的同步屏障——Message

最後大家如果覺得手冊内容有用的話,可以點贊分享一下哦~