一人一貓旅行記之Handler原理

2020-09-27 08:00:47

如果在主執行緒(也叫UI執行緒)中執行一些耗時操作,會出現ANR問題。為了避免ANR,需要將耗時操作,如網路請求啊、資料庫操作啊、讀取檔案等等的操作,開啟一個子執行緒來處理。
在耗時操作執行完畢後,直接在子執行緒中更新UI怎麼樣呢?
一般來說,會出現下面這個錯誤:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1048)
        at android.view.View.requestLayout(View.java:19781)
        at android.view.View.requestLayout(View.java:19781)
        at android.view.View.requestLayout(View.java:19781)
        at android.view.View.requestLayout(View.java:19781)
        at android.view.View.requestLayout(View.java:19781)
        at android.view.View.requestLayout(View.java:19781)
        at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3172)
        at android.view.View.requestLayout(View.java:19781)
        at android.widget.TextView.checkForRelayout(TextView.java:7368)
        at android.widget.TextView.setText(TextView.java:4480)
        at android.widget.TextView.setText(TextView.java:4337)
        at android.widget.TextView.setText(TextView.java:4312)
        at icbc.agree.tmpapppp.MainActivity$2.run(MainActivity.java:37)

從上面的程式碼提示,不難看出UI是隻能在主執行緒中更新的,這也是為什麼主執行緒也被稱為UI執行緒的原因。
但是我們在onCreate中啟動一個子執行緒去更改一個TextView的文字,會驚奇的發現程式竟然可以正常執行,這是怎麼回事呢?
其實是因為,檢查執行緒的工作是ViewRootImpl來完成的,但是在onCreate中啟動子執行緒,可能在ViewRootImpl物件建立之前便已經執行了,TextView的setText方法,不相信的話可以在setText之前sleep一段時間看看哈。

既然不可以在子執行緒更新UI,那麼我們需要一個機制來通知主執行緒進行UI更新,這便是Handler的職責!

Handler是如何實現的呢?
要想搞明白Handler的原理,我們需要認識幾個概念:
Message:Handler傳遞和處理的資料的載體
Message Queue:訊息佇列
Looper:一個不斷從訊息佇列中取出資料的迭代器

在這裡插入圖片描述
Handler的工作流程大致如上圖所示,Handler通過sendMessage等方法,傳送一個Message,將其放到MessageQueue中,Looper不斷的取出佇列中的資料,並進行分發處理。

接下來,從安卓系統原始碼再來看一下Handler的工作原理:
首先,我們看一下主執行緒中的工作

public static void main(String[] args) {
        <-省略部分不相關程式碼->
        Looper.prepareMainLooper();

        <-省略部分不相關程式碼->

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();

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

我們看到Looper.prepareMainLooper();這行程式碼,可以到Looper中看一下它的作用,其實最終是呼叫的prepare方法,而它的作用就是建立一個訊息佇列,程式碼如下:

		mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();

在主執行緒中的最後是呼叫的loop(),現在我們看一下對應的程式碼:

public static void loop() {
         final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
		<-省略程式碼部分->
        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);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

           <-省略程式碼部分->
        }
    }

我們分析下上面這段程式碼,我們獲取到Message Queue,然後開啟一個無限迴圈,通過queue.next()取出來Message,如果訊息為空,說明訊息佇列中沒有訊息了,所以停止迴圈
最後的msg.target.dispatchMessage(msg);應該很熟悉了吧?這個target就是傳送訊息的Handler。
現在我們再去看一下,Handler的部分:
無論是sendMessage還是sendEmptyMessage,最終都是呼叫的sendMessageAtTime,所以我們直接看sendMessageAtTime的原始碼部分。

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

繼續往下看

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

找到target了吧?有興趣的可以繼續往下找找程式碼,其實就是將Message加到訊息佇列中
好的,至此Handler完整的流程便梳理完了!