很高興碰見你 ~java
關於handler的內容,基本每一個android開發者都掌握了,網絡中的優秀博客也很是多,我以前也寫過一篇文章,讀者感興趣能夠去看看:傳送門。android
這篇文章主要講Handler中的同步屏障問題,這也是面試的熱門問題。不少讀者以爲這一塊的知識很偏,實戰中並無什麼用處,僅僅用來面試,包括筆者。我在Handler機制一文中寫到:其實同步屏障對於咱們的平常使用的話實際上是沒有多大用處。由於設置同步屏障和建立異步Handler的方法都是標誌爲hide,說明谷歌不想要咱們去使用他
。面試
筆者在前段時間面試時被問到這個問題,以後從新思考了這個問題,發現了一些不同的地方。結合了一些大佬的觀點,發現同步屏障這個機制,並不如咱們所想徹底沒用,而仍是有他的長處。這篇文章則表達一下我對同步屏障機制的思考,但願對你有幫助。api
文章主要內容是:先介紹什麼同步屏障,再分析如何使用以及正確地使用。網絡
那麼,咱們開始吧。異步
同步屏障機制是一套爲了讓某些特殊的消息得以更快被執行的機制。async
注意這裏我在同步屏障以後加上了機制二字,緣由是單純的同步屏障並不起做用,他須要和其餘的Handler組件配合才能發揮做用。ide
這裏咱們假設一個場景:咱們向主線程發送了一個UI繪製操做Message,而此時消息隊列中的消息很是多,那麼這個Message的處理可能會獲得延遲,繪製不及時形成界面卡頓。同步屏障機制的做用,是讓這個繪製消息得以越過其餘的消息,優先被執行。oop
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
,也就是咱們沒法調用這個方法。那,講了這麼多有什麼用?
咳咳~不要慌,但咱們能夠發異步消息啊。在系統添加同步屏障的時候,不就能夠趁機上車了,是吧。
添加異步消息有兩種辦法:
給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信號到來之時,繪製任務能夠被及時執行,不會形成界面卡頓。但這樣也帶來了相對應的代價:
改善這個問題辦法就是:使用異步消息。當咱們發送異步消息到MessageQueue中時,在等待VSYNC期間也能夠執行咱們的任務,讓咱們設置的任務能夠更快得被執行且減小主線程Looper的壓力。
可能有讀者會以爲,異步消息機制自己就是爲了不界面卡頓,那咱們直接使用異步消息,會不會有隱患?這裏咱們須要思考一下,什麼狀況的異步消息會形成界面卡頓:異步消息任務執行過長、異步消息海量。
若是異步消息執行時間太長,那即時是同步任務,也會形成界面卡頓,這點應該都很好理解。其次,若異步消息海量到達影響界面繪製,那麼即便是同步任務,也是會致使界面卡頓的;緣由是MessageQueue是一個鏈表結構,海量的消息會致使遍歷速度降低,也會影響異步消息的執行效率。因此咱們應該注意的一點是:
不可在主線程執行重量級任務,不管異步仍是同步。
那,咱們之後豈不是能夠直接使用異步Handler來取代同步Handler了?是,也不是。
同步Handler有一個特色是會遵循與繪製任務的順序,設置同步屏障以後,會等待繪製任務完成,纔會執行同步任務;而異步任務與繪製任務的前後順序沒法保證,在等待VSYNC的期間可能被執行,也有可能在繪製完成以後執行。所以,個人建議是:若是須要保證與繪製任務的順序,使用同步Handler;其餘,使用異步Handler。
技術深挖,老是能學到一些更加不同的知識。當知識的廣度愈來愈廣,知識之間的聯繫會迸發出不同的火花。
第一次學習Handler,僅僅知道能夠發送消息並執行;第二次學習Handler,知道了其在Android消息機制重要地位;第三次學習Handler,知道了原來Handler和屏幕刷新機制還有這麼一個聯繫。
溫故而知新,古人誠不欺我。
若是文章對你有幫助,還但願能夠點贊鼓勵一下做者。