前言
很高興遇見你 ~
關於handler的內容,基本每個android開發者都掌握了,網路中的優秀部落格也非常多,我之前也寫過一篇文章,讀者感興趣可以去看看:傳送門。
這篇文章主要講Handler中的同步屏障問題,這也是面試的熱門問題。很多讀者覺得這一塊的知識很偏,實戰中並沒有什麼用處,僅僅用來面試,包括筆者。我在Handler機制一文中寫到:其實同步屏障對於我們的日常使用的話其實是沒有多大用處。因為設定同步屏障和建立非同步Handler的方法都是標誌為hide,說明谷歌不想要我們去使用他
。
筆者在前段時間面試時被問到這個問題,之後重新思考了這個問題,發現了一些不一樣的地方。結合了一些大佬的觀點,發現同步屏障這個機制,並不如我們所想完全沒用,而還是有他的長處。這篇文章則表達一下我對同步屏障機制的思考,希望對你有幫助。
文章主要內容是:先介紹什麼同步屏障,再分析如何使用以及正確地使用。
那麼,我們開始吧。
什麼是同步屏障機制
同步屏障機制是一套為了讓某些特殊的訊息得以更快被執行的機制。
注意這裡我在同步屏障之後加上了機制二字,原因是單純的同步屏障並不起作用,他需要和其他的Handler元件配合才能發揮作用。
這裡我們假設一個場景:我們向主執行緒傳送了一個UI繪製操作Message,而此時訊息佇列中的訊息非常多,那麼這個Message的處理可能會得到延遲,繪製不及時造成介面卡頓。同步屏障機制的作用,是讓這個繪製訊息得以越過其他的訊息,優先被執行。
MessageQueue中的Message,有一個變數isAsynchronous
,他標誌了這個Message是否是非同步訊息;標記為true稱為非同步訊息,標記為false稱為同步訊息。同時還有另一個變數target
,標誌了這個Message最終由哪個Handler處理。
我們知道每一個Message在被插入到MessageQueue中的時候,會強制其target
屬性不能為null,如下程式碼:
MessageQueue.class
boolean enqueueMessage(Message msg, long when) {
// Hanlder不允許為空
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
...
}
而android提供了另外一個方法來插入一個特殊的訊息,強行讓target==null
:
private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
// 把當前需要執行的Message全部執行
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
// 插入同步屏障
if (prev != null) {
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
程式碼有點長,重點在於:沒有給Message賦值target屬性,且插入到Message佇列頭部。當然原始碼中還涉及到延遲訊息,我們暫時不關心。這個target==null的特殊Message就是同步屏障
MessageQueue在獲取下一個Message的時候,如果碰到了同步屏障,那麼不會取出這個同步屏障,而是會遍歷後續的Message,找到第一個非同步訊息取出並返回。這裡跳過了所有的同步訊息,直接執行非同步訊息。為什麼叫同步屏障?因為它可以遮蔽掉同步訊息,優先執行非同步訊息。
我們來看看原始碼是怎麼實現的:
Message next() {
···
if (msg != null && msg.target == null) {
// 同步屏障,找到下一個非同步訊息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
···
}
如果遇到同步屏障,那麼會迴圈遍歷整個連結串列找到標記為非同步訊息的Message,即isAsynchronous返回true,其他的訊息會直接忽視,那麼這樣非同步訊息,就會提前被執行了。
注意,同步屏障不會自動移除,使用完成之後需要手動進行移除,不然會造成同步訊息無法被處理。我們可以看一下原始碼:
Message next() {
...
// 阻塞時間
int nextPollTimeoutMillis = 0;
for (;;) {
// 阻塞對應時間
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// 同步屏障,找到下一個非同步訊息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
// 如果上面有同步屏障,但卻沒找到非同步訊息,
// 那麼msg會迴圈到連結串列尾,也就是msg==null
if (msg != null) {
···
} else {
// 沒有訊息,進入阻塞狀態
nextPollTimeoutMillis = -1;
}
···
}
}
}
可以看到如果沒有即時移除同步屏障,他會一直存在且不會執行同步訊息。因此使用完成之後必須即時移除。但我們無需操心這個,後面就知道了。
如何傳送非同步訊息
上面我們瞭解到了同步屏障的作用,但是會發現postSyncBarrier
方法被標記為@hide
,也就是我們無法呼叫這個方法。那,講了這麼多有什麼用?
咳咳~不要慌,但我們可以發非同步訊息啊。在系統新增同步屏障的時候,不就可以趁機上車了,是吧。
新增非同步訊息有兩種辦法:
- 使用非同步型別的Handler傳送的全部Message都是非同步的
- 給Message標誌非同步
給Message標記非同步是比較簡單的,通過setAsynchronous
方法即可。
Handler有一系列帶Boolean型別的引數的構造器,這個引數就是決定是否是非同步Handler:
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
// 這裡賦值
mAsynchronous = async;
}
在傳送訊息的時候就會給Message賦值:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
// 賦值
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
但是非同步型別的Handler構造器是標記為hide,我們無法使用,但在api28之後新增了兩個重要的方法:
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);
}
通過這兩個api就可以建立非同步Handler了,而非同步Handler發出來的訊息則全是非同步的。
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}
如何正確使用
上面我們似乎漏了一個問題:系統什麼時候新增同步屏障?。
非同步訊息需要同步屏障的輔助,但同步屏障我們無法手動新增,因此瞭解系統何時新增和刪除同步屏障是非常必要的。只有這樣,才能更好地運用非同步訊息這個功能,知道為什麼要用和如何用。
瞭解同步屏障需要簡單瞭解一點螢幕重新整理機制的內容。放心,只需要瞭解一丟丟就可以了。
我們的手機螢幕重新整理頻率有不同的型別,60Hz、120Hz等。60Hz表示螢幕在一秒內重新整理60次,也就是每隔16.6ms重新整理一次。螢幕會在每次重新整理的時候發出一個 VSYNC
訊號,通知CPU進行繪製計算。具體到我們的程式碼中,可以認為就是執行onMesure()
、onLayout()
、onDraw()
這些方法。好了,大概瞭解這麼多就可以了。
瞭解過 view 繪製原理的讀者應該知道,view繪製的起點是在 viewRootImpl.requestLayout()
方法開始,這個方法會去執行上面的三大繪製任務,就是測量佈局繪製。但是,重點來了:
呼叫
requestLayout()
方法之後,並不會馬上開始進行繪製任務,而是會給主執行緒設定一個同步屏障,並設定 ASYNC 訊號監聽。
當 ASYNC 訊號的到來,會傳送一個非同步訊息到主執行緒Handler,執行我們上一步設定的繪製監聽任務,並移除同步屏障
這裡我們只需要明確一個情況:呼叫requestLayout()方法之後會設定一個同步屏障,知道ASYNC訊號到來才會執行繪製任務並移除同步屏障。(這裡涉及到Android螢幕重新整理以及繪製原理更多的內容,本文不詳細展開,感興趣的讀者可以點選文末的連線閱讀。)
那,這樣在等待ASYNC訊號的時候主執行緒什麼事都沒幹?是的。這樣的好處是:保證在ASYNC訊號到來之時,繪製任務可以被及時執行,不會造成介面卡頓。但這樣也帶來了相對應的代價:
- 我們的同步訊息最多可能被延遲一幀的時間,也就是16ms,才會被執行
- 主執行緒Looper造成過大的壓力,在VSYNC訊號到來之時,才集中處理所有訊息
改善這個問題辦法就是:使用非同步訊息。當我們傳送非同步訊息到MessageQueue中時,在等待VSYNC期間也可以執行我們的任務,讓我們設定的任務可以更快得被執行且減少主執行緒Looper的壓力。
可能有讀者會覺得,非同步訊息機制本身就是為了避免介面卡頓,那我們直接使用非同步訊息,會不會有隱患?這裡我們需要思考一下,什麼情況的非同步訊息會造成介面卡頓:非同步訊息任務執行過長、非同步訊息海量。
如果非同步訊息執行時間太長,那即時是同步任務,也會造成介面卡頓,這點應該都很好理解。其次,若非同步訊息海量到達影響介面繪製,那麼即使是同步任務,也是會導致介面卡頓的;原因是MessageQueue是一個連結串列結構,海量的訊息會導致遍歷速度下降,也會影響非同步訊息的執行效率。所以我們應該注意的一點是:
不可在主執行緒執行重量級任務,無論非同步還是同步。
那,我們以後豈不是可以直接使用非同步Handler來取代同步Handler了?是,也不是。
同步Handler有一個特點是會遵循與繪製任務的順序,設定同步屏障之後,會等待繪製任務完成,才會執行同步任務;而非同步任務與繪製任務的先後順序無法保證,在等待VSYNC的期間可能被執行,也有可能在繪製完成之後執行。因此,我的建議是:如果需要保證與繪製任務的順序,使用同步Handler;其他,使用非同步Handler。
最後
技術深挖,總是能學到一些更加不一樣的知識。當知識的廣度越來越廣,知識之間的聯絡會迸發出不一樣的火花。
第一次學習Handler,僅僅知道可以傳送訊息並執行;第二次學習Handler,知道了其在Android訊息機制重要地位;第三次學習Handler,知道了原來Handler和螢幕重新整理機制還有這麼一個聯絡。
溫故而知新,古人誠不欺我。
如果文章對你有幫助,還希望可以點贊鼓勵一下作者。
推薦文獻
- “終於懂了” 系列:Android螢幕重新整理機制—VSync、Choreographer 全面理解:胡飛洋作者一篇關於繪製和螢幕重新整理很好的文章,閱讀後可以對非同步訊息有更加深層次的理解。
- RxAndroid 2.1.0 has a new API:2018年RxAndroid決定新增非同步api的一篇文章,解釋了為什麼要使用非同步訊息。