(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章之後我也才明白
- Android中的UI控制元件不是執行緒安全的,如果在子執行緒中也能修改UI元素,那多執行緒的時,共同訪問同一個UI元素,就會導致這個UI元素處於我們不可預知的狀態,這個執行緒讓它往左一點,那個執行緒讓它往右一點,UI該聽誰的,好tm亂。。 乾脆我就只聽主執行緒的把。
問題 : 那為什麼不通過對訪問UI控制元件的子執行緒加上鎖機制呢 ?
這個很簡單了,如果為不同的執行緒訪問同一UI元素加上鎖機制,那我們程式設計師寫相關程式碼的時候會變得超級麻煩。。。 改個UI還得考慮它是不是已經被別的執行緒佔用了,被佔用了,還得讓那個執行緒釋放鎖。。。執行緒再多一點的話,大大地加大了程式設計師地工作量.
而且加上鎖機制無疑會由於執行緒堵塞地原因降低訪問UI的效率,幀率降低,體驗也會不友好。
讓UI元素只能再主執行緒訪問就會省下很多事,建立一個Handler
就行了。
下面從整體概述一下 訊息機制的整個工作過程 :
-
Handler
建立時會採用當前執行緒的Looper
來構建內部的訊息迴圈系統,如果Handler在子執行緒,則一開始是沒有Looper
物件的(解決方法稍後介紹),主執行緒ActivityThread
預設有一個Looper。 -
Handler
建立完畢,通過post
方法傳入Runnable
物件,或者通過sendMessage(Message msg)
傳送訊息。post()
方法裡也是通過呼叫send()
實現的 -
send()
方法被呼叫後,呼叫 MessageQueue的enqueueMessage()方法將訊息傳送到訊息佇列中,等待被處理。 -
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
傳送過來的訊息,再來一張圖加深印象
主要包含兩個操作:
- 通過
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 迴圈裡 :
- 通過queue.next()一直讀取新的訊息,如果沒有訊息 則退出迴圈。
- 接下來,
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(); } 複製程式碼
如果沒有通過
post
傳Runnable
,就會看建立Handler時的構造方法中有沒有傳Runnable
引數,傳了的話由mCallback
儲存。這個
mCallback
是Handler內部的一個介面public interface Callback { public boolean handleMessage(Message msg); } 複製程式碼
如果構造Handler時也沒有傳
Runnable
物件,最終會執行handleMessage(msg)
,這個 方法就是我們建立handler時重寫的handleMessage()方法.
參考資料: Android藝術開發探索
(完~)