Android訊息機制全面解析(Handler,MessageQueue,Looper,Threadlocal)

拜仁fans發表於2019-04-28

(1),Android訊息機制概述

Android中的訊息機制主要指 Handler的執行機制 以及 MessageQueue,Looper的工作過程 ,三者相互協作,保證著訊息的接收,傳送,處理,執行。

​ 圖片來自郭神的《第一行程式碼》

先簡單的介紹一下 Android 中 訊息機制大家庭的主要成員 :

  • Handler : 是Android訊息機制的上層介面,最為大家常用,相當於Android訊息機制的入口,我們通過使用Handler傳送訊息來引起訊息機制的迴圈。通常用於:在子執行緒執行完耗時任務完後,更新UI。

  • MessageQueue : 儲存 訊息(Message) 物件的訊息佇列,實則是單連結串列結構.

  • Looper : 用於無限的從 MessageQueue中取出訊息,相當於訊息的永動機,如果有新的訊息,則處理執行,若沒有,則就一直等待,堵塞。Looper** 所在的執行緒是 建立 Handler 時所在的執行緒。

    主執行緒建立Handler時,會自動建立一個Looper,但是子執行緒並不會自動建立Looper

  • ThreadLocal : 在每個執行緒互不干擾的儲存,提供資料,以此來獲取當前執行緒的Looper

  • ActivityThread : Android 的主執行緒,也叫UI執行緒,主執行緒被建立時 自動初始化主執行緒的 Looper物件。

問題 : 大家都知道只有在UI執行緒才能對UI元素進行操作,在子執行緒更改UI就會報錯,為什麼?

看完《Android藝術開發探索》 這本書的第10章之後我也才明白

  1. Android中的UI控制元件不是執行緒安全的,如果在子執行緒中也能修改UI元素,那多執行緒的時,共同訪問同一個UI元素,就會導致這個UI元素處於我們不可預知的狀態,這個執行緒讓它往左一點,那個執行緒讓它往右一點,UI該聽誰的,好tm亂。。 乾脆我就只聽主執行緒的把。

問題 : 那為什麼不通過對訪問UI控制元件的子執行緒加上鎖機制呢 ?

這個很簡單了,如果為不同的執行緒訪問同一UI元素加上鎖機制,那我們程式設計師寫相關程式碼的時候會變得超級麻煩。。。 改個UI還得考慮它是不是已經被別的執行緒佔用了,被佔用了,還得讓那個執行緒釋放鎖。。。執行緒再多一點的話,大大地加大了程式設計師地工作量.

而且加上鎖機制無疑會由於執行緒堵塞地原因降低訪問UI的效率,幀率降低,體驗也會不友好。

讓UI元素只能再主執行緒訪問就會省下很多事,建立一個Handler就行了。

下面從整體概述一下 訊息機制的整個工作過程 :

  1. Handler建立時會採用當前執行緒的 Looper來構建內部的訊息迴圈系統,如果Handler在子執行緒,則一開始是沒有Looper物件的(解決方法稍後介紹),主執行緒ActivityThread預設有一個Looper。

  2. Handler建立完畢,通過 post方法傳入Runnable物件,或者通過sendMessage(Message msg)傳送訊息。

    post()方法裡也是通過呼叫send()實現的

  3. send()方法被呼叫後,呼叫 MessageQueue的enqueueMessage()方法將訊息傳送到訊息佇列中,等待被處理。

  4. Looper物件執行在Handler所在的執行緒,從MessageQueue訊息佇列中不斷地取出訊息,處理,所以業務邏輯(通常是更新UI)就執行在Looper的執行緒中。

接下來從區域性來分析訊息機制的每個成員。

(2),ThreadLocal 工作原理

1, 什麼是ThreadLocal?

ThreadLocal是一個執行緒內部的資料儲存類,通過它可以在指定的執行緒中獲得儲存資料,獲得資料,執行緒之間的ThreadLocal相互獨立,且無法獲得另一個執行緒的TheadLocal.

  • 相對整個程式來說,每個執行緒的ThreadLocal是區域性變數。

  • 相對一個執行緒來說,執行緒內的ThreadLocal是執行緒的全域性變數

ThreadLocal是一個泛型類,可以儲存任意型別的物件。

示例:

public class ThreadLocalTest {

	public static void main(String[] args) {
		
		ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>();
		mThreadLocal.set(true);
		System.out.println("#Main Thread : ThreadLocal " + mThreadLocal.get());
		
		
		new Thread( new Runnable() {
			
			@Override
			public void run() {
				mThreadLocal.set(false);
				System.out.println("#1 Thread : ThreadLocal " + mThreadLocal.get());
			}
		}).start();
		
		new Thread( new Runnable() {
			
			@Override
			public void run() {
				System.out.println("#2 Thread : ThreadLocal " + mThreadLocal.get());
			}
		}).start();

	}

}
複製程式碼

我們在主執行緒建立一個 泛型為Boolean的ThreadLocal,並.set(True),然後在第一個子執行緒中.set(False),在第二個子執行緒中不做修改,直接列印。 可以看到,在不同的執行緒中獲得的值也不同。

輸出 :

#Main Thread : ThreadLocal true
#1 Thread : ThreadLocal false
#2 Thread : ThreadLocal null
複製程式碼

2,ThreadLocal的實現原理

首先每個執行緒內部都維護著一個ThreadLocalMap物件

Thread.Java

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
複製程式碼

這個ThraedLocalMap 與Map類似,一個執行緒內可以有多個ThreadLocal型別變數,所以通過ThreadLocalMap <ThreadLocal<?> key, Object value>.儲存著多個<ThreadLocal , 任意型別物件>鍵值對。

看一下ThreadLocal的set()方法實現 :

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
複製程式碼

先是獲得當前執行緒的ThreadLocalMap物件,map.set(this,value) 設定了我這個ThreadLocal儲存的值.

get()方法實現 :

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();//獲得當前執行緒
        ThreadLocalMap map = getMap(t);//根據根據獲得它的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//獲得<k,v>鍵值對
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;//通過<k,v>獲得值
                return result;
            }
        }
        return setInitialValue();
    }
複製程式碼

3,ThreadLocal的使用場景

一般,當某些資料是以執行緒為作用域,並且不同的執行緒具有不同的資料副本時,可以考慮用ThreadLocal

場景1:

對於Handler,它想要獲得當前執行緒的Looper,並且Looper的作用域就是當前的執行緒,不同的執行緒具有不同的Looper物件,這時可以使用ThreadLocal。

場景2:

複雜邏輯下的物件的傳遞,如果想要一個物件貫穿著整個執行緒的執行過程,可採用Threadlocal讓此物件作為該執行緒的全域性物件。

(3),MessageQueue的工作原理

單連結串列的形式,儲存著Handler傳送過來的訊息,再來一張圖加深印象

Android訊息機制全面解析(Handler,MessageQueue,Looper,Threadlocal)

主要包含兩個操作:

  • 通過enqueueMessage(Message msg,long when),像佇列插入一個訊息,這裡為了節省篇幅,就不上原始碼,貼上原始碼連線,MessageQueue.enqueueMessage()
  • 通過next()從無限迴圈佇列中取出訊息,並從訊息佇列中刪除。MessageQueue.next()

雖然它叫做訊息佇列,但內部其實是以單連結串列的結構儲存,有利於插入,刪除的操作。

(4),Looper的工作原理

它的主要作用就是 不停地從訊息佇列中 檢視是否有新的訊息,如果有新的訊息就會立刻處理,沒有訊息就會堵塞。

持有MessageQueue的引用,並且會在構造方法中初始化

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

問題: 如何在子執行緒建立它的Looper物件 ?

前面說到主執行緒自己會建立一個Looper物件,所以我們在主執行緒使用Handler的時候直接建立就可以了。

但是在子執行緒使用Handler的話,就需要我們手動建立Looper了,

示例:

new Thread() {
    @Override
    public void run() {
        Looper.prepare();
        Handler handler = new Handler();
        Looper.loop();
    }
}.start();
複製程式碼

prepare()原始碼如下:

 /** Initialize the current thread as a looper.
77      * This gives you a chance to create handlers that then reference
78      * this looper, before actually starting the loop. Be sure to call
79      * {@link #loop()} after calling this method, and end it by calling
80      * {@link #quit()}.
81      */
82    public static void prepare() {
83        prepare(true);
84    }
85
86    private static void prepare(boolean quitAllowed) {
87        if (sThreadLocal.get() != null) {
88            throw new RuntimeException("Only one Looper may be created per thread");
89        }
90        sThreadLocal.set(new Looper(quitAllowed));
91    }
複製程式碼

可以看到最終是呼叫了 此 Looper 所線上程的 **ThreadLocal.set()**方法,存了一個Looper物件進去。

除了prepare(),還有一些其他方法,我們也需要知道

  • loop() : 啟動訊息迴圈,,只有當Looper呼叫了loop()之後,整個訊息迴圈才活了起來

  • prepareMainLooper() : 給主執行緒建立Looper物件

  • getMainLooper() : 獲得主執行緒的Looper物件

  • quit() : 通知訊息佇列,直接退出訊息迴圈,不等待當前正在處理的訊息執行完,quit之後,再向訊息佇列中傳送新的訊息就會失敗( Handler的send()方法就會返回false )

     public void quit() {
         mQueue.quit(false);
     }
    複製程式碼
  • quitSafety() : 通過訊息佇列,不再接收新的訊息,等當前的訊息佇列中的訊息處理完就退出。

     public void quitSafely() {
            mQueue.quit(true);
     }    
    複製程式碼

下面分析loop()的實現:

/**
119     * Run the message queue in this thread. Be sure to call
120     * {@link #quit()} to end the loop.
121     */
122    public static void loop() {
123        final Looper me = myLooper();
124        if (me == null) {
125            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on                                            this thread.");
126        }
127        final MessageQueue queue = me.mQueue;
128
129        ...//省略部分程式碼
133		  //從這裡開啟無限迴圈,直到 沒有訊息
134        for (;;) {
135            Message msg = queue.next(); // might block
136            if (msg == null) {
137                // No message indicates that the message queue is quitting.
138                return;
139            }
140
141            // This must be in a local variable, in case a UI event sets the logger
142            Printer logging = me.mLogging;
143            if (logging != null) {
144                logging.println(">>>>> Dispatching to " + msg.target + " " +
145                        msg.callback + ": " + msg.what);
146            }
147
148            msg.target.dispatchMessage(msg);
149			   ...//省略部分程式碼
166        }
167    }
複製程式碼

在 for 迴圈裡 :

  1. 通過queue.next()一直讀取新的訊息,如果沒有訊息 則退出迴圈。
  2. 接下來,msg.target.dispatchMessage(msg);,target是傳送此訊息的 Hander對像,通知Handler呼叫dispatchMessage()來接收訊息。

(5),Handler的工作原理

Handler的主要工作就是 傳送訊息,接收訊息。

傳送訊息的方式有post(),send(),不過post()方法最後還是呼叫的send()方法

  • 傳送訊息的過程:

    send型別的傳送訊息方法有很多,並且是巢狀的

    sendMessage()

    public final boolean sendMessage(Message msg) {
            return sendMessageDelayed(msg, 0);
    }
    複製程式碼

    sendMessageDelayed()

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

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

    三種send的傳送訊息方式,最後都會通過enqueueMessage()來通知訊息佇列 插入這條新的訊息。

    Handler.enqueueMessage

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
          msg.target = this;
          if (mAsynchronous) {
               msg.setAsynchronous(true);
          }
          return queue.enqueueMessage(msg, uptimeMillis);//呼叫訊息佇列的enqueueMessage()
    }
    複製程式碼
  • 接收訊息的過程

    接收訊息由dispatchMessage(Message msg)為入口

    dispatchMessage()

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

    這裡的callback是我們呼叫 post(Runnable runnalbe) 時傳入的Runnable物件,如果我們傳入了Runnable物件

    就會執行Runnable的run方法:

    private static void handleCallback(Message message) {
            message.callback.run();
    }
    複製程式碼

    如果沒有通過postRunnable,就會看建立Handler時的構造方法中有沒有傳Runnable引數,傳了的話由mCallback儲存。

    這個mCallback是Handler內部的一個介面

    public interface Callback {
            public boolean handleMessage(Message msg);
    }
    複製程式碼

    如果構造Handler時也沒有傳Runnable物件,最終會執行handleMessage(msg),這個 方法就是我們建立handler時重寫的handleMessage()方法.

    Android訊息機制全面解析(Handler,MessageQueue,Looper,Threadlocal)


參考資料: Android藝術開發探索

www.cnblogs.com/luxiaoxun/p…

(完~)

Android訊息機制全面解析(Handler,MessageQueue,Looper,Threadlocal)

相關文章