關於Handler同步屏障你可能不知道的問題

前言

很高興碰見你 ~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,也就是咱們沒法調用這個方法。那,講了這麼多有什麼用?

咳咳~不要慌,但咱們能夠發異步消息啊。在系統添加同步屏障的時候,不就能夠趁機上車了,是吧。

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

  • 使用異步類型的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和屏幕刷新機制還有這麼一個聯繫。

溫故而知新,古人誠不欺我。

若是文章對你有幫助,還但願能夠點贊鼓勵一下做者。

推薦文獻

相關文章
相關標籤/搜索