溫習一下Handler

語言: CN / TW / HK
   三年,感覺就是一個坎,最近壓力有點大,總是在胡思亂想,想想自己的競爭力,除了知道自己從位元組快手工
   作過,剩下的就是CV操作,感覺啥都不會。特別是在這個大環境之下,感覺愈發慌亂了,想想,還是不要過於
   緊張,把之前學到的,看到的,記錄一個Doc吧,總結一些。

一定要學會的

Handler我們也用了很久了,內部的邏輯我想大家也都瞭然了,這我在記錄一下。當然,我們還是提出問題,跟著問題去分析。

  • Handler的簡單實用方式和場景。
  • Hanlder的訊息機制實現的大概原理。
  • 如何建立一個自己的Handler?
    • Android系統是怎麼建立的?
    • HandlerThread簡單原理。

詳細解析

簡單使用

現在我們用Handler大部分就是為了實現執行緒切換,傳送一個訊息,在主執行緒做一些我們預期的操作,比如更新UI等。沒有很多特別需要講解的。

Handler(Looper.getMainLooper()).post { // do something }

原理流程

傳送訊息

首先,我們通過Handler傳送一個訊息,我們分析一下。

public final boolean post(@NonNull Runnable r) { return sendMessageDelayed(getPostMessage(r), 0); }

  • 首先通過getPostMessage構建一個訊息物件Message。
  • 第二個引數,就是希望當前訊息的延遲執行的時間,單位毫秒,3000,就意味著希望當前的訊息3s後被執行。

我們看一下,訊息物件是如何構建的。

``` private static final int MAX_POOL_SIZE = 50;

private static Message getPostMessage(Runnable r) { Message m = Message.obtain(); m.callback = r; return m; }

public static Message obtain() { synchronized (sPoolSync) { if (sPool != null) { Message m = sPool; sPool = m.next; m.next = null; m.flags = 0; // clear in-use flag sPoolSize--; return m; } } return new Message(); } ``` - 會呼叫Message的obtain方法,獲取一個訊息物件。 - 構建訊息物件,這個sPool是一個快取佇列,是一個連結串列,維護的大小是MAX_POOL_SIZE。 - 當我們傳送訊息的時候就會構建一個訊息物件,這個訊息物件被使用之後,會清除標記位,放到快取佇列,方便下一次使用。 - 所以,經常建議,我們自己在構造Message物件的時候,不要new,要通過obtain方法獲取。 - 獲得訊息物件之後,把當前的Runnable繫結到訊息物件上,方便當前訊息被執行的時候,回到給到外界。

值得學習的地方:這個快取很值得我們學習,比如RecyclerView的快取池。為什麼呢?很多人說,不就是一個快取嗎,有很大用嗎?不不不,很有用。首先我們要支援哪些場景需要快取?

如果你的使用場景,存在頻繁建立物件的時候,就需要用到快取複用的思想。如果場景比較單一,大可不必(如果追求極致,使用也不是不可以。),比如Message物件的構建,頻繁發生,如果頻繁的建立物件,但是沒有複用。那麼就會導致虛擬機器頻繁GC,導致記憶體抖動,一旦GC就會Stop the world。帶來的後果也是致命的,可能給使用者直接的感受,就是卡頓,所以這點在效能優化上也很重要。

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); } 這個時候,獲取系統開機時間,加上延遲執行的時間,就是預期訊息執行的時間。很好理解。

``` 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);

} ```

這裡需要注意的是,mAsynchronous,如果這個值是true,就意味著,當前handler傳送的訊息都是非同步訊息,預設都是同步訊息。這個和同步屏障有關,不要被名字迷惑了,很簡單,後面說。

  • 當前訊息的target是當前的handler,持有一個引用
    • 這個也就是一些記憶體洩露的原因,handler持有activity,Message持有handler,messagQueue持有message,MessageQueue主執行緒一直存在執行,導致Activity洩露。

``` boolean enqueueMessage(Message msg, long when) { if (msg.target == null) { throw new IllegalArgumentException("Message must have a target."); } if (msg.isInUse()) { throw new IllegalStateException(msg + " This message is already in use."); }

synchronized (this) {
    if (mQuitting) {
        IllegalStateException e = new IllegalStateException(
                msg.target + " sending message to a Handler on a dead thread");
        Log.w(TAG, e.getMessage(), e);
        msg.recycle();
        return false;
    }

    msg.markInUse();
    msg.when = when;
    Message p = mMessages;
    boolean needWake;
    if (p == null || when == 0 || when < p.when) {
        // New head, wake up the event queue if blocked.
        msg.next = p;
        mMessages = msg;
        needWake = mBlocked;
    } else {
        // Inserted within the middle of the queue.  Usually we don't have to wake
        // up the event queue unless there is a barrier at the head of the queue
        // and the message is the earliest asynchronous message in the queue.
        needWake = mBlocked && p.target == null && msg.isAsynchronous();
        Message prev;
        for (;;) {
            prev = p;
            p = p.next;
            if (p == null || when < p.when) {
                break;
            }
            if (needWake && p.isAsynchronous()) {
                needWake = false;
            }
        }
        msg.next = p; // invariant: p == prev.next
        prev.next = msg;
    }

    // We can assume mPtr != 0 because mQuitting is false.
    if (needWake) {
        nativeWake(mPtr);
  • }
  • }
  • return true; } ``` 這個就是具體的,訊息入佇列的行為,Java層的行為。

  • 首先判斷mQuitting,如果當前訊息迴圈關閉了,直接返回false。

  • 沒有很複雜,很好懂,就是拿到當前msg的預期執行時間,並且插到訊息連結串列中,訊息連結串列是根據時間先後排列的。
  • needWake,需要喚醒訊息迴圈
    • 如果當前訊息佇列是空的,當前插入了一條訊息,成為了頭結點,需要把阻塞的訊息輪詢喚醒。很好理解,沒有訊息的時候,這個時候就會處於阻塞休眠狀態,有了新的訊息加入,如果阻塞就需要喚醒。
    • 如果當前訊息是最早的非同步訊息並且預期執行時間靠前,並且訊息頭結點是訊息屏障,也需要換新阻塞狀態的訊息輪詢。同樣的,如果有訊息屏障,會攔截同步訊息,只會處理了非同步訊息。如果這個時候來了新的優先順序更高的非同步訊息,就需要把阻塞態喚醒,如果優先順序不高,前面的非同步訊息都在休眠,你著急個啥嘛,哈哈哈
處理訊息

private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); }

首先是準備Looper,這個只能初始化一次,並且是ThreadLocal的,Looper適合執行緒繫結的,也就是說,是執行緒安全的,這個也是通過Handler實現執行緒切換的重要的一環。

``` public static void loop() { final Looper me = myLooper(); ......

for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return;
    }
  ......
    try {
        msg.target.dispatchMessage(msg);
        if (observer != null) {
            observer.messageDispatched(token, msg);
        }
        dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
    } catch (Exception exception) {
        if (observer != null) {
            observer.dispatchingThrewException(token, msg, exception);
        }
        throw exception;
    } finally {
        ThreadLocalWorkSource.restore(origWorkSource);
        if (traceTag != 0) {
            Trace.traceEnd(traceTag);
        }
    }
    ......

    msg.recycleUnchecked();
}

} ```

這個也很好理解,通過訊息佇列MessageQueue獲取訊息,執行這個訊息,並回收這個訊息物件。

  • 首先通過next()方法,去一個訊息出來。
  • msg.target.dispatchMessage(msg),每一個訊息持有一個handler的引用,回撥給handler,去處理這個事件。
  • msg.recycleUnchecked(),把這個訊息重置,放到sPool快取,方便下次使用。

``` Message next() { // Return here if the message loop has already quit and been disposed. // This can happen if the application tries to restart a looper after quit // which is not supported. final long ptr = mPtr; if (ptr == 0) { return null; }

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
    if (nextPollTimeoutMillis != 0) {
        Binder.flushPendingCommands();
    }

    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
        // Try to retrieve the next message.  Return if found.
        final long now = SystemClock.uptimeMillis();
        Message prevMsg = null;
        Message msg = mMessages;
        if (msg != null && msg.target == null) {
            // Stalled by a barrier.  Find the next asynchronous message in the queue.
            do {
                prevMsg = msg;
                msg = msg.next;
            } while (msg != null && !msg.isAsynchronous());
        }
        if (msg != null) {
            if (now < msg.when) {
                // Next message is not ready.  Set a timeout to wake up when it is ready.
                nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
            } else {
                // Got a message.
                mBlocked = false;
                if (prevMsg != null) {
                    prevMsg.next = msg.next;
                } else {
                    mMessages = msg.next;
                }
                msg.next = null;
                if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                msg.markInUse();
                return msg;
            }
        } else {
            // No more messages.
            nextPollTimeoutMillis = -1;
        }

        // Process the quit message now that all pending messages have been handled.
        if (mQuitting) {
            dispose();
            return null;
        }

        // If first time idle, then get the number of idlers to run.
        // Idle handles only run if the queue is empty or if the first message
        // in the queue (possibly a barrier) is due to be handled in the future.
        if (pendingIdleHandlerCount < 0
                && (mMessages == null || now < mMessages.when)) {
            pendingIdleHandlerCount = mIdleHandlers.size();
        }
        if (pendingIdleHandlerCount <= 0) {
            // No idle handlers to run.  Loop and wait some more.
            mBlocked = true;
            continue;
        }

        if (mPendingIdleHandlers == null) {
            mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
        }
        mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
    }

    // Run the idle handlers.
    // We only ever reach this code block during the first iteration.
    for (int i = 0; i < pendingIdleHandlerCount; i++) {
        final IdleHandler idler = mPendingIdleHandlers[i];
        mPendingIdleHandlers[i] = null; // release the reference to the handler

        boolean keep = false;
        try {
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }

        if (!keep) {
            synchronized (this) {
                mIdleHandlers.remove(idler);
            }
        }
    }

    // Reset the idle handler count to 0 so we do not run them again.
    pendingIdleHandlerCount = 0;

    // While calling an idle handler, a new message could have been delivered
    // so go back and look again for a pending message without waiting.
    nextPollTimeoutMillis = 0;
}

} ```

這個就是訊息的具體輪詢操作,依舊是一個死迴圈,知道有訊息返回,否則我們就休眠,我們一起看看。

  • nativePollOnce(ptr, nextPollTimeoutMillis) 這個方法是呼叫Native方法,休眠用的。我們之前說過,每一個Msg都有一個預期執行的時間,如果當前一個訊息也沒有獲取到,也不會直接退出迴圈,會直接迴圈。具體的大家可以瞭解一下Epoll(現在面試也問,真他媽卷。)
  • msg != null && msg.target == null,如果存在訊息屏障,直接遍歷message連結串列,找到非同步訊息,直接返回。
  • 否則,就直接遍歷連結串列,找到第一個節點(第一個節點預期執行時間最早),如果該執行了就直接返回,如果時間沒找到,就阻塞等待。
  • 如果還是沒有找到訊息msg物件,這個時候,就找到IdleHandler執行。
    • IdleHandler,之前我來快手面過,我還不知道是啥?哈哈哈,這個IdleHandler就是等待沒有訊息執行,空閒的時候,執行。
    • IdleHandler,通過不同的返回值,可以執行一次移除,還可以常駐,空閒就會重新執行一次。(我工作中目前沒有用過,我們可以自行看看系統中哪些用到IdleHandler)。
什麼是訊息屏障

我之前第一次聽說這個名詞,也感覺很蒙,啥玩意兒,這麼高深嗎?難道有什麼黑科技?其實不是,我們的訊息執行,訊息其實是一系列的Message,這些Message都有一個時間戳,標識當前要執行的時間。但是如果來了一條緊急的訊息怎麼辦呢?是的,屏障的作用就體現了。

``` 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++; final Message msg = Message.obtain(); msg.markInUse(); msg.when = when; msg.arg1 = token;

    Message prev = null;
    Message p = mMessages;
    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;
    }
    return token;
}

} ``` 一般我們構建Handler的實收或者建立Message物件的時候,傳遞asyncChronous=true,設定屏障訊息。這裡是MessageQueue設定訊息屏障。

  • 首先構造一個訊息物件,設定特殊的標記位置。handler=null,回顧之前,我們傳送訊息的時候,都會把訊息打上一個handler的引用,標識這個Msg是由哪一個handler發出來的。這裡不需要,屏障訊息不需要給handler反饋,就是充當訊息佇列的一個標識。
  • 遍歷訊息佇列,訊息佇列是根據執行時間從前到後排列的。屏障訊息也是一樣的,根據when插入到訊息連結串列之中。

if (msg != null && msg.target == null) { // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } 在MessageQueue的next()方法獲取訊息的時候,如果當前存在訊息屏障(msg.handler==null),這個時候,就獲取訊息屏障之後的非同步訊息(asyncChronous=true),忽略同步訊息。(訊息屏障在使用完畢之後,記得及時移除,否則屏障後面的同步訊息就會得不到執行。)

所以訊息屏障的目的就是優先處理非同步訊息,在Android繪製的時候,scheduleTraversals也是會發送一個這樣的訊息,目的就是保證繪製優先執行,UI儘可能快速的展示,防止畫面出現卡頓。

自己的Handler

Handler不是主執行緒特有的,任何一個執行緒都可以有一個自己的Handler,實現自己的訊息佇列,具體的方式如下。

使用流程

``` val threadWithLooper = object : Thread() {

var threadLooper: Looper? = null

override fun run() { Looper.prepare() threadLooper = Looper.myLooper() Looper.loop()

} } ```

  • 首先我們構建一個Thread,然後在run方法(執行緒啟動的時候),啟動訊息迴圈
    • 首先prepare,通過ThreadLocal構建一個Looper物件。(如果有人關心MessageQueue,這個也是在Looper初始化一起建立建立的,Looper適合MessageQueue繫結的,一比一的)。
    • 然後把Looper取出來儲存成區域性變數。

這樣會有什麼問題嗎?是的,併發問題。這是一個非同步執行緒,如果你在主執行緒或者其他執行緒,要建立Handler,去獲取ThreadWithLooper的Looper,這個時候Looper可能還沒有建立好,出現問題,我們就不得不在獲取Looper加鎖了。那麼有麼有好的方式呢?是的,HandlerThread。我們一起看看。

HandlerThread

``` @Override public void run() { mTid = Process.myTid(); Looper.prepare(); synchronized (this) { mLooper = Looper.myLooper(); notifyAll(); } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid = -1; }

public Looper getLooper() { if (!isAlive()) { return null; }

// If the thread has been started, wait until the looper has been created.
synchronized (this) {
    while (isAlive() && mLooper == null) {
        try {
            wait();
        } catch (InterruptedException e) {
        }
    }
}
return mLooper;

} ```

為了保證安全,一定可以獲得Looper,做了如下:

  • 首先,在獲取getLooper的時候,如果獲取不到(looper是null),這個時候就wait阻塞,while迴圈,直到looper成功賦值。
  • 在run方法的時候,給mLooper賦值,賦值成功之後,呼叫notifyAll,喚醒那些為了獲取Looper,處於wait狀態的執行緒。
系統是怎麼建立的?

我們知道App的入口,其實就是ActivityThread,從main函式入口的。當應用點開,程序建立,就會反射建立ActivityThread,就開始構建訊息的迴圈。

``` public static void main(String[] args) { ......

Looper.prepareMainLooper();

......
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");

} ```

沒有什麼區別,也是先準備Looper,然後讓訊息佇列迴圈起來。會不會有人問,為啥沒有和HandlerThread一樣,加鎖,wait,以及notifyAll?我個人理解,因為這個是程序建立,剛開始的時候會就反射執行main方法,準備Looper,這個時候檢視也沒有建立,也不會有什麼需求用到Handler去進行執行緒切換,去執行一些任務非要在主執行緒,應該是有依賴順序的。

總結

Handler我們可以學到的,總結一下:

  • handler,looper,queue之間的關係,分別是處理訊息,獲取訊息,存訊息,按照時間先後執行。
    • 他們之間的數量關係,N:1:1的,handler是一個傳送和處理的媒介,實現執行緒的切換。後面兩者才是訊息轉起來的核心。
    • 知道IdleHandler是什麼東西,實現空閒呼叫。
    • 知道常見記憶體洩露怎麼引起的,怎麼解決。
  • HandlerThread的出現
    • 可以非同步呼叫,處理一些稍中的任務,不阻塞UI執行緒。當然和執行緒池比起來還是有不足,畢竟單執行緒的。
    • 簡化實現訊息機制實現,解決併發打來的looper沒有準備好的問題。

擴充套件學習:

  • 學習快取佇列,MSG的複用。
    • 這個很有用,別面對象大量建立,頻繁GC。