書海拾貝|開發藝術探索之 android 的訊息機制

oven_小區發表於2019-03-02

提到訊息機制讀者應該都不陌生……從開發角度來說, Handler 是 Android 訊息機制的上層介面,這使得在開發過程中只需要和 Handler 互動即可。……通過它可以輕鬆將一個任務切換到 Handler 所在的執行緒中去執行。

正如開篇詞所說,“主執行緒中不能進行網路通訊等耗時操作,而子執行緒中不能進行 UI 更新”,是我在 android 開發入門遇到的第一個知識點(keng),但當時只是單純記憶,本篇將順著開發藝術探索的講述,梳理 android 的訊息機制有關知識。

###開篇知識小點 Handler 是 Android 訊息機制的上層介面,使用場景通常是更新 UI。 Android 訊息機制主要指 Handler 的執行機制,Handler 的執行需要底層的 MessageQueue 和 Looper 的支援。

  • MessageQueue:訊息佇列,內部儲存一組訊息,以佇列形式對外提供插入和刪除的工作,但其內部實現並非佇列,而是單連結串列的資料結構實現的,是一個訊息的 儲存單元,不能主動處理訊息。
  • Looper:訊息迴圈,以無限迴圈的形式查詢是否有新訊息,有的話就處理,否則等待。
  • ThreadLocal:Looper中的一個特殊概念,作用是可以在不同執行緒中互不干擾地儲存資料。Handler 建立的時候需要採用當前程式的 Looper 來構造訊息迴圈系統,此時通過 ThreadLocal 可以輕鬆獲取每個執行緒的 Looper。

注意:執行緒是預設沒有 Looper 的,如果需要使用 Handler 就必須為執行緒建立 Looper。主執行緒,即UI執行緒,是 ActivityThread ,ActivityThread 被建立時就會初始化Looper,所以主執行緒中預設可以使用 Handler。

###概述 幾乎所有的 Android 開發者都知道在 Android 中訪問 UI 只能在主執行緒中進行。CheckThread() 方法會對執行緒呼叫 UI 操作的正確性做出驗證,如果當前訪問 UI 的執行緒並非主執行緒,則會丟擲異常。 但, Android 建議不要在主執行緒使用耗時操作,以免導致程式無法響應,即ANR。在開發工作中,我們常常會遇到需要從服務端拉取資訊,並在 UI 中進行顯示。Handler 的存在就是為了解決在子執行緒中無法訪問 UI 的矛盾。

  • 為什麼在子執行緒中不允許訪問 UI 呢?因為 Android 的 UI 控制元件並非執行緒安全的,如果多執行緒併發訪問會導致 UI 控制元件處於不可預期的狀態。 *為什麼不加鎖?缺點有:1.加鎖會導致 UI 訪問邏輯變得複雜,其次鎖機制會降低 UI 訪問的效率,因為鎖機制會阻塞某些執行緒的執行。

* Handler的簡單使用

方法書裡沒有介紹,翻出萌新筆記貼一點: 簡單應用:

private Handler handler = new Handler() {
 public void handleMessage(Message msg) {
            switch (msg.what) {
                case UPDATE_TEXT:
                    // 在這裡可以進行UI操作
                   break;
                default:
                    break;
            }
        }

    };
複製程式碼
//在需要耗時操作的地方,開子執行緒
new Thread(new Runnable() {
    @Override
    public void run() {
//可以進行耗時操作
        Message message = new Message();
        message.what = UPDATE_TEXT;
        handler.sendMessage(message); //將Message物件傳送出去
        }
    }).start();
複製程式碼

過程如下:

  1. 首先在主執行緒中建立一個 Handler 物件,並重寫HandleMessage方法
  2. 在子執行緒中需要進行 UI 操作的時候建立一個 Message 物件,
  3. 通過 Handler 將資訊傳送出去,
  4. 該資訊被新增到 MessageQueue 中等待被處理,Looper 則會一直嘗試從 MessageQueue 中取出待處理資訊,最後分配到 Handler 的handleMessage() 方法中。

注意:在Activity中,並沒有顯式呼叫 Looper.prepare() 和Looper.loop() 方法,因為在 Activity 的啟動程式碼中,已經在當前 UI 執行緒呼叫了Looper.prepare() 和 Looper.loop() 方法,這就是前文提到 UI 執行緒預設可以使用 Handler 的原因。 runOnUiThread() 是一個非同步訊息處理機制的介面封裝,用法簡單但實際原理是一樣的。

Handler 的工作原理

Handler 建立時會採用當前執行緒的 Looper 來構建內部訊息迴圈系統,如果當前執行緒沒有 Looper ,那麼就會報錯。 解決方法:為當前執行緒建立 Looper ,或者在一個有 Looper 的執行緒中建立 Handler Handler 建立完畢之後,其內部的 Looper 以及 MessageQueue 就可以和 Handler 一起協同工作了,然後通過 Handler 的 post 方法將一個 Runnable 投遞到 Handler 內部的 Looper 中處理,也可通過 Handler 中的 send 傳送訊息,同樣在 Looper 內處理。post 的本質也是呼叫 send 。工作過程如圖:

Handler 工作過程.png
當 send 方法被呼叫,它會呼叫 MessageQueue 的 enqureMessage 方法將這個訊息放入訊息佇列,由 Looper 處理,最後訊息中的 Runnable 或者 Handler 的 handlerMessage 方法會被呼叫。Looper 是執行在建立 Handler 所在的執行緒中的,這樣一來, Handler 中的業務邏輯就會被切換到建立 Handler 所在的執行緒中執行,完成切換執行緒的目的。

Android 的訊息機制全面分析

ThreadLocal

一個執行緒內部的資料儲存類,通過它可以獨立儲存指定執行緒中的資料。日常開發中較少用到,但是 android 原始碼中有時會利用它實現一些看似複雜的問題。

  1. 一般來說,當某些資料是以執行緒為作用域並且不同執行緒有不同的資料父本的時候,就會使用 ThreadLocal。比如 Handler,需要獲取當前執行緒的 Looper ,且 Looper 作用域為執行緒,不同執行緒間的 Looper 互相獨立,這時候使用 ThreadLocal 則可以輕鬆實現 Looper 線上程中的存取。 否則,系統必須提供一個全域性的雜湊表供 Handler 查詢指定執行緒的 Looper,就必須存在類似於 LooperManage 這樣類,會使機制變得複雜。
  2. 可用於複雜邏輯下的物件傳遞,比如監聽器傳遞。當函式呼叫棧比較深的時候,如果把監聽器作為引數傳遞,會使程式設計變得糟糕;如果把監聽器作為靜態變數供執行緒訪問,則基本不具備擴充套件性。而使用 ThreadLocal ,每個執行緒都將擁有自己的監聽器,可以線上程內全域性,一鍵 get 到。 書上舉了簡單例子及原始碼,說明 ThreadLocal 在各個執行緒的資料儲存獨立性,因為例子較簡單而原始碼部分比較繁瑣,這裡不再贅述。總之,不同執行緒訪問 ThreadLocal 的 get 方法, ThreadLocal 將從各執行緒內部取出一個陣列,按照當前執行緒的索引,查詢相應的 value 值,所以執行緒不用,值不同。從原始碼分析可得, ThreadLocal 的 set 和 get 方法都僅能訪問當前執行緒的 localValue 物件的 table 陣列,因此在不同陣列中訪問同一個 ThreadLocal 的 set 和 get 方法可以互不干涉。 ###MessageQueue 前文已提過,訊息佇列實際上內部是用單連結串列實現的,包含兩大重要方法, enqueueMessage 和 next 。
  • enqueueMessage 原始碼
// android SDK-27

    boolean enqueueMessage(Message msg, long when) {
      ...
        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;
    }
複製程式碼

實際上就是單連結串列插入操作。

  • next 無限迴圈方法,如果訊息佇列中沒有訊息,會一直阻塞在這裡,直到有訊息到來。
 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;
        }
    }
複製程式碼

###Looper原始碼解析 在構造方法中建立一個 MessageQueue ,然後將當前執行緒物件儲存起來。

 private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
複製程式碼

通過Looper.prepare() 即可手動當前執行緒建立一個 Looper, 接著通過 Looper.loop() 開啟迴圈。 Looper 提供了 quit 和 quitSafely 兩種方法退出 Looper,前者直接退出,後者設定一個安全標記,等訊息佇列內所有訊息處理完畢之後才會安全退出。如果在子執行緒裡手動建立了 Looper 在所有訊息完成之後應該呼叫 quit 方法,否則這個子執行緒會一直處在等待狀態。

  • 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;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                //唯一的退出死迴圈條件
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (slowDispatchThresholdMs > 0) {
                final long time = end - start;
                if (time > slowDispatchThresholdMs) {
                    Slog.w(TAG, "Dispatch took " + time + "ms on "
                            + Thread.currentThread().getName() + ", h=" +
                            msg.target + " cb=" + msg.callback + " msg=" + msg.what);
                }
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }
複製程式碼

loop 方法是一個死迴圈,唯一跳出死迴圈的條件是 MessageQueue.next 方法返回 null 。當 Looper.quit 被呼叫,Looper 呼叫 MessageQueue.quit 或者 quitSafely 方法通知訊息佇列退出。next 是一個阻塞方法,如果未接到新訊息將一直等待,如果接到新訊息,則交給 dispatchMessage 處理,這個方法是在 handler 建立的 Looper 中執行的。 ###Handler 的工作原理 ####傳送訊息的典型過程

 public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }

public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }


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

rivate boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
複製程式碼

也就是說,Handler 呼叫 sendMessage 方法,依次呼叫 sendMessageDelayed,sendMessageAtTime,enqueueMessage 方法後,呼叫 queue.enqueueMessage 向訊息佇列插入了一條訊息,接下來,按上文分析, Looper 中呼叫 MessageQueue.next 方法得到訊息,最後將訊息交給 Handler.dispatchMessage 處理。 實現如下:

 public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
複製程式碼

Handler 處理訊息過程如下:

  1. 檢查 Message callback 是否為 null,不為 null 則通過 handleCallback 來處理訊息,Message 的 callback 是一個 Runnable 物件,實際上就是 Handler 的 post 方法傳遞的 Runnable 引數。
private static void handleCallback(Message message) {
        message.callback.run();
    }
複製程式碼

2.檢查 mCallback 是否為 null ,不為 null 就呼叫 mCallback 的 handlerMessage 方法

 /**
     * Callback interface you can use when instantiating a Handler to avoid
     * having to implement your own subclass of Handler.
     *
     * @param msg A {@link android.os.Message Message} object
     * @return True if no further handling is desired
     */
    public interface Callback {
        public boolean handleMessage(Message msg);
    }
複製程式碼

通過Callback 可以採用Handler handler = new handler(callback)的方式建立 Handler 物件。callback 的意義在於可以建立一個 Handler 例項但不需要派生 Handler 的子類。在日常開發中,最常見的方式就是派生一個 Handler 的子類並且重寫 handlerMessage 方法,當不想用該方式的時候可以採用 callback 實現。

Handler 訊息處理.png

###主執行緒的訊息迴圈 在主執行緒的入口方法中國,呼叫 Looper.prepareMainLooper() 來建立主執行緒的 Looper 以及 MessageQueue,並通過呼叫 Looper.loop() 來開啟迴圈。

 public static void main(String[] args) {
     ……
        Process.setArgV0("<pre-initialized>");
         Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        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");
    }
複製程式碼

主執行緒訊息迴圈開始之後,ActivityThread 還需要 Handler 來和訊息佇列進行互動,這個 Handler 就是 AcitivityThread.H。 ActivityThread 通過 Application Thread 和 AMS 進行程式間通訊,AMS以程式間通訊的方式完成 ActivityThread 的請求後會回撥 ApplicationThread 中的 Binder 方法,然後 ApplicationThread 會向 H 發訊息,H 收到訊息後將 ApplicationThread 中的邏輯切換到 ActivityThread 中執行,即切換到主執行緒中去執行。

相關文章