在我們去討論Handler,Looper,MessageQueue的關係之前,我們需要先問兩個問題:
-
這一套東西搞出來是爲了解決什麼問題呢?
-
如果讓我們來解決這個問題該怎麼做?
以上者兩個問題,是我最近總結出來的,在我們學習瞭解一個新的技術之前,最好是先能回答這兩個問題,這樣你才能對你正在學習的東西有更深刻的認識。
第一個問題:google的程序員們搞出這一套東西是爲了解決什麼問題的?這個問題很顯而易見,爲了解決線程間通信的問題。我們都知道,Android的UI/View這一套系統是運行在主線程的,並且這個主線程是死循環的,來看看具體的證據吧。
public final class ActivityThread {
public static void main(String[] args) {
//...
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
}
如上面的代碼示例所示,ActivityThread.main()
方法作爲Android程序的入口,裏面我省略了一些初始化的操作,然後就執行了一句Looper.loop()
方法,就沒了,再下一行就拋異常了。
loop()
方法裏面實際上就是一個死循環,一直在執行着,不斷的從一個MQ(MessageQueue,後面我都縮寫成MQ了)去取消息,如果有的話,那麼就執行它或者讓它的發送者去處理它。
一般來說,主線程循環中都是執行着一些快速的UI操作,當你有手touch屏幕的時候,系統會產生事件,UI會處理這些事件,這些事件都會在主線程中執行,並快速的響應着UI的變化。如果主線程上發生一些比較耗時的操作,那麼它後面的方法就無法得到執行了,那麼就會出現卡頓,不流暢。
因此,Android並不希望你在主線程去做一些耗時的操作,這裏對“耗時”二字進行樸素的理解就行了,就是執行起來需要消耗的時間比較多的操作。比如讀寫文件,小的文件也許很快,但你無法預料文件的大小,再比如訪問網絡,再比如你需要做一些複雜的計算等等。
爲了不阻礙主線程流暢的執行,我們就必須在需要的時候把耗時的操作放到其他線程上去,當其他線程完成了工作,再給一個通知(或許還帶着數據)給到主線程,讓主線程去更新UI什麼的,當然了,如果你要的耗時操作只是默默無聞的完成就行了,並不需要通知UI,那麼你完全不需要給通知給到UI線程。這就是線程間的通信,其他線程做耗時操作,完成了告訴UI線程,讓它進行更新。爲了解決這個問題,Android系統給我們提供了這樣一套方案來解決。
第二個問題:如果讓我們來想一套方案來解決這個線程間通信的問題,該怎麼做呢?
先看看我們現在已經有的東西,我們有一個一直在循環的主線程,它實現起來大概是這個樣子:
public class OurSystem {
public static void main(String [] args) {
for (;;) {
//do something...
}
}
}
爲什麼主線程要一直死循環的執行呢?
關於這一點,我個人並沒有特別透徹的認知,但我猜測,對於有GUI的系統/程序,應該都有一個不斷循環的主線程,因爲這個GUI程序肯定是要跟人進行交互的,也就是說,需要等待用戶的輸入,比如觸碰屏幕,動動鼠標,敲敲鍵盤什麼的,這些事件肯定是硬件層先獲得一個響應/信號,然後會不斷的向上封裝傳遞。
如果說我們一碰屏幕,一碰鼠標,就開啓一個新線程去處理UI上的變化,首先,這當然是可以的!UI在什麼線程上更新其實都是可以的嘛,並不是說一定要在主線程上更新,這是系統給我設的一個套子。然後,問題也會複雜的多,如果我們快速的點擊2下鼠標,那麼一瞬間就開啓了兩個新線程去執行,那麼這兩個線程的執行順序呢?兩個獨立的線程,我們是無法保證說先啓動的先執行。
所以第一個問題就是執行順序的問題。
第二個問題就是同步,幾個相互獨立的線程如果要處理同一個資源,那麼造成的結果都是令人困惑,不受控制的。另一方面強行給所有的操作加上同步鎖,在效率上也會有問題。
爲了解決順序執行的問題,非常容易就想到的一種方案是事件隊列,各種各樣的事件先進入到一個隊列中,然後有個東西會不斷的從隊列中獲取,這樣第一個事件一定在第二個事件之前被執行,這樣就保證了順序,如果我們把這個“取事件”的步驟放在一個線程中去做,那麼也順便解決了資源同步的問題。
因此,對於GUI程序會有一個一直循環的(主)線程,可能就是這樣來的吧。
這是一個非常純淨的死循環,我們想要做一些事情的話,就得讓它從一個隊列裏面獲取一些事情來做,就像打印機一樣。因此我們再編寫一個消息隊列類,來存放消息。消息隊列看起來應該是這樣:
public class OurMessageQueue() {
private LinkedList<Message> mQueue = new LinkedList<Message>();
// 放進去一條消息
public void enQueue() {
//...
}
// 取出一條消息
public Message deQueue() {
//...
}
// 判斷是否爲空隊列
public boolean isEmpty() {
//...
}
}
接下來我們的循環就需要改造成能從消息隊列裏獲取消息,並能夠根據消息來做些事情了:
public class OurSystem {
public static void main(String [] args) {
// 初始化消息隊列
OurMessageQueue mq = ...
for (;;) {
if (!mq.isEmpty()) {
Message msg = mq.deQueue();
//do something...
}
}
}
}
現在我們假象一下,我們需要點擊一下按鈕,然後去下載一個超級大的文件,下載完成後,我們再讓主線程顯示文件的大小。
首先,按一下按鈕,這個事件應該會被觸發到主線程來(具體怎麼來的我還尚不清楚,但應該是先從硬件開始,然後插入到消息隊列中,主線程的循環就能獲取到了),然後主線程開啓一個新的異步線程來進行下載,下載完成後再通知主線程來更新,代碼看上去是這樣的:
// 腦補的硬件設備……
public class OurDevice {
// 硬件設備可能有一個回調
public void onClick() {
// 先拿到同一個消息隊列,並把我們要做的事情插入隊列中
OurMessageQueue mq = ...
Message msg = Message.newInstance("download a big file");
mq.enQueue(msg);
}
}
然後,我們的主線程循環獲取到了消息:
public class OurSystem {
public static void main(String [] args) {
// 初始化消息隊列
OurMessageQueue mq = ...
for (;;) {
if (!mq.isEmpty()) {
Message msg = mq.deQueue();
// 是一條通知我們下載文件的消息
if (msg.isDownloadBigFile()) {
// 開啓新線程去下載文件
new Thread(new Runnable() {
void run() {
// download a big file, may cast 1 min...
// ...
// ok, we finished download task.
// 獲取到同一個消息隊列
OurMessageQueue mq = ...
// 消息入隊
mq.enQueue(Message.newInstance("finished download"));
}
}).start();
}
// 是一條通知我們下載完成的消息
if (msg.isFilishedDownload()) {
// update UI!
}
}
}
}
}
注意,主線程循環獲取到消息的時候,顯示對消息進行的判斷分類,不同的消息應該有不同的處理。在我們獲取到一個下載文件的消息時,開啓了一個新的線程去執行,耗時操作與主線程就被隔離到不同的執行流中,當完成後,新線程中用同一個消息隊列發送了一個通知下載完成的消息,主線程循環獲取到後,裏面就可以更新UI。
這樣就是一個我隨意腦補的,簡單的跨線程通信的方案。
有如下幾點是值得注意的:
-
主線程是死循環的從消息隊列中獲取消息。
-
我們要將消息發送到主線程的消息隊列,我們需要通過某種方法能獲取到主線程的消息隊列對象
-
消息(Message)的結構應該如何設計呢?
Android 中的線程間通信方案
Looper
Android中有一個Looper
對象,顧名思義,直譯過來就是循環的意思,Looper
也確實幹了維持循環的事情。
Looper的代碼是非常簡單的,去掉註釋也就300多行。在官方文檔的註釋中,它推薦我們這樣來使用它:
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
先來看看prepare方法幹了什麼。
Looper.prepare()
public static void prepare() {
prepare(true);
}
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));
}
注意prepare(boolean)
方法中,有一個sThreadLocal
變量,這個變量有點像一個哈希表,它的key是當前的線程,也就是說,它可以存儲一些數據/引用,這些數據/引用是與當前線程是一一對應的,在這裏的作用是,它判斷一下當前線程是否有Looper
這個對象,如果有,那麼就報錯了,"Only
one Looper may be created per thread",一個線程只允許創建一個Looper
,如果沒有,就new一個新的塞進這個哈希表中。然後它調用了Looper
的構造方法。
Looper 的構造方法
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
在Looper
的構造方法中,很關鍵的一句,它new了一個MessageQueue
對象,並自己維持了這個MQ的引用。
此時prepare()
方法的工作就結束了,接下來需要調用靜態方法loop()
來啓動循環。
Looper.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;
}
msg.target.dispatchMessage(msg);
//...
}
}
loop()方法,我做了省略,省去了一些不關心的部分。剩下的部分非常的清楚了,首先調用了靜態方法myLooper()獲取一個Looper對象。
public static Looper myLooper() {
return sThreadLocal.get();
}
myLooper()
同樣是靜態方法,它是直接從這個ThreadLocal
中去獲取,這個剛剛說過了,它就類似於一個哈希表,key是當前線程,因爲剛剛prepare()
的時候,已經往裏面set了一個Looper
,那麼此時應該是可以get到的。拿到當前線程的Looper
後,接下來,final
MessageQueue queue = me.mQueue;
拿到與這個Looper
對應的MQ,拿到了MQ後,就開啓了死循環,對消息隊列進行不停的獲取,當獲取到一個消息後,它調用了Message.target.dispatchMessage()
方法來對消息進行處理。
Looper的代碼看完了,我們得到了幾個信息:
-
Looper
調用靜態方法prepare()
來進行初始化,一個線程只能創建一個與之對應的Looper
,Looper
初始化的時候會創建一個MQ,因此,有了這樣的對應關係,一個線程對應一個Looper
,一個Looper
對應一個MQ。可以說,它們三個是在一條線上的。 -
Looper
調用靜態方法loop()
開始無限循環的取消息,MQ調用next()
方法來獲取消息
MessageQueue
對於MQ的源碼,簡單的看一下,構造函數與next()
方法就好了。
MQ的構造方法
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
MQ的構造方法簡單的調用了nativeInit()來進行初始化,這是一個jni方法,也就是說,可能是在JNI層維持了它這個消息隊列的對象。
MessageQueue.next()
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
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 (false) Log.v("MessageQueue", "Returning message: " + msg);
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
}
}
}
next()
方法的代碼有些長,我作了一些省略,請注意到,這個方法也有一個死循環,這樣做的效果就是,在Looper
的死循環中,調用了next()
,而next()
這裏也在死循環,表面上看起來,方法就阻塞在Looper
的死循環中的那一行了,知道next()
方法能返回一個Message
對象出來。
簡單瀏覽MQ的代碼,我們得到了這些信息:
-
MQ的初始化是交給JNI去做的
-
MQ的
next()
方法是個死循環,在不停的訪問MQ,從中獲取消息出來返回給Looper
去處理。
Message
Message
對象是MQ中隊列的element,也是Handler
發送,接收處理的一個對象。對於它,我們需要了解它的幾個成員屬性即可。
Message
的成員變量可以分爲三個部分:
-
數據部分:它包括
what(int)
,arg1(int)
,arg2(int)
,obj(Object)
,data(Bundle)
等,一般用這些來傳遞數據。 -
發送者(target):它有一個成員變量叫
target
,它的類型是Handler
的,這個成員變量很重要,它標記了這個Message
對象本身是誰發送的,最終也會交給誰去處理。 -
callback:它有一個成員變量叫
callback
,它的類型是Runnable
,可以理解爲一個可以被執行的代碼片段。
Handler
Handler
對象是在API層面供給開發者使用最多的一個類,我們主要通過這個類來進行發送消息與處理消息。
Handler的構造方法(初始化)
通常我們調用沒有參數的構造方法來進行初始化,使用起來大概是這樣的:
Handler mHandler = new Handler() {
handleMessage(Message msg) {
//...
}
}
沒有參數的構造方法最終調用了一個兩個參數的構造方法,它的部分源碼如下:
public Handler(Callback callback, boolean async) {
//...
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
注意到,它對mLooper
成員變量進行了賦值,通過Looper.myLooper()
方法獲取到當前線程對應的Looper
對象。上面已經提到過,如果Looper
調用過prepare()
方法,那麼這個線程對應了一個Looper
實例,這個Looper
實例也對應了一個MQ,它們三者之間是一一對應的關係。
然後它通過mLooper
對象,獲取了一個MQ,存在自己的mQueue
成員變量中。
Handler
的初始化代碼說明了一點,Handler
所初始化的地方(所在的線程),就是從將這個線程對應的Looper
的引用賦值給Handler
,讓Handler
也持有
對於主線程來說,我們在主線程的執行流中,new一個Handler
對象,Handler對象都是持有主線程的Looper
(也就是Main
Looper
)對象的。
同樣的,如果我們在一個新線程,不調用Looper.prepare()
方法去啓動一個Looper
,直接new一個Handler
對象,那麼它就會報錯。像這樣
new Thread(new Runnable() {
@Override
public void run() {
//Looper.prepare();
//因爲Looper沒有初始化,所以Looper.myLooper()不能獲取到一個Looper對象
Handler h = new Handler();
h.sendEmptyMessage(112);
}
}).start();
以上代碼運行後會報錯:
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
小結:Handler
的初始化會獲取到當前線程的Looper
對象,並通過Looper
拿到對應的MQ對象,如果當前線程的執行流並沒有執行過Looper.prepare()
,則無法創建Handler對象
Handler.sendMessage()
sendMessage
消息有各種各樣的形式或重載,最終會調用到這個方法:
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);
}
它又調用了enqueueMessage
方法:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
注意到它對Message
的target
屬性進行了賦值,這樣這條消息就知道自己是被誰發送的了。然後將消息加入到隊列中。
Handler.dispatchMessage()
Message
對象進入了MQ後,很快的會被MQ的next()
方法獲取到,這樣Looper
的死循環中就能得到一個Message
對象,回顧一下,接下來,就調用了Message.target.dispatchMessage()
方法對這條消息進行了處理。
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
private static void handleCallback(Message message) {
message.callback.run();
}
public void handleMessage(Message msg) {
//這個方法是空實現,讓客戶端程序員去覆寫實現自己的邏輯
}
dispatchMessage
方法有兩個分支,如果callback
(Runnable
)不是null
,則直接執行callback.run()
方法,如果callback
是null
,則將msg
作爲參數傳給handleMessage()
方法去處理,這樣就是我們常見的處理方法了。
Message.target與Handler
特別需要注意Message
中的target
成員變量,它是指向自己的發送者,這一點意味着什麼呢?
意味着:一個有Looper
的線程可以有很多個Handler
,這些Handler
都是不同的對象,但是它們都可以將Message
對象發送到同一個MQ中,Looper
不斷的從MQ中獲取這些消息,並將消息交給它們的發送者去處理。一個MQ是可以對應多個Handler
的(多個Handler
都可以往同一個MQ中消息入隊)
下圖可以簡要的概括下它們之間的關係。
總結
-
Looper
調用prepare()
進行初始化,創建了一個與當前線程對應的Looper
對象(通過ThreadLocal
實現),並且初始化了一個與當前Looper
對應的MessageQueue
對象。 -
Looper
調用靜態方法loop()
開始消息循環,通過MessageQueue.next()
方法獲取Message
對象。 -
當獲取到一個
Message
對象時,讓Message
的發送者(target
)去處理它。 -
Message
對象包括數據,發送者(Handler
),可執行代碼段(Runnable
)三個部分組成。 -
Handler
可以在一個已經Looper.prepare()
的線程中初始化,如果線程沒有初始化Looper
,創建Handler
對象會失敗 -
一個線程的執行流中可以構造多個
Handler
對象,它們都往同一個MQ中發消息,消息也只會分發給對應的Handler
處理。 -
Handler
將消息發送到MQ中,Message
的target
域會引用自己的發送者,Looper
從MQ中取出來後,再交給發送這個Message
的Handler
去處理。 -
Message
可以直接添加一個Runnable
對象,當這條消息被處理的時候,直接執行Runnable.run()
方法。