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

一隻修仙的猿發表於2021-03-17

前言

很高興遇見你 ~

關於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和螢幕重新整理機制還有這麼一個聯絡。

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

如果文章對你有幫助,還希望可以點贊鼓勵一下作者。

推薦文獻

相關文章