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>
同步屏障是通過 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 機制之 Thread Local
Handler 機制之 Callable、Future 和 Fulure Task
篇幅原因,就不在這裡為大家贅述了,有需要的小夥伴:可以私信發送"面試"即可領取這份《 Android Handler 消息機制學習手冊》,助你早日成為底層原理大師!
最後大家如果覺得手冊内容有用的話,可以點贊分享一下哦~