Android Handler訊息機制中的諸多疑問
前言
網上總是有很多闡述Android訊息機制的文章,基本上大同小異,都是講Handle,Message,Looper,MessageQueue這四個類會如何協同工作的。但是動腦筋的童鞋們可能總是會有如下的一些疑問,我翻閱了數多微博,很多年了,也沒有看到相關比較完整的解釋,所以這些天自己深刻閱讀了一下原始碼,並且為自己解答了心中一直存在的疑惑,記錄在此,希望也能幫助有同樣疑問的小夥伴。
Handler訊息機制的諸多疑問
1、主執行緒中的Looper死迴圈取message,為什麼不會卡死主執行緒?
我覺得這個問題困擾著每一個“瞭解”handler的人,我們經常看到一些講解訊息機制的部落格,說了一大堆,ActivityThread啟動之後,就Looper.loop(),之後這個方法裡就通過死迴圈去取MessageQueue的Message,可是真正思考過的小夥伴一定想過,那麼主執行緒豈不是一個死迴圈卡死在那裡,那還怎麼工作?那麼請看如下程式碼:
public static void loop() { //取出本執行緒的looper,沒有looper的話就拋異常,這也解釋了為何在子執行緒使用handler得先去Looper.prepare() final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } //取出本執行緒的MessageQ final MessageQueue queue = me.mQueue; ............. //開始死迴圈 for (;;) { //從MQ裡面next來取訊息,我們從原始碼註釋了也可以看到 這個next()方法可能會阻塞,我們去看看。 Message msg = queue.next(); // might block //如果從MQ取出null來,說明沒有訊息了,就結束返回,這也代表著looper的loop迴圈結束退出。 if (msg == null) { // No message indicates that the message queue is quitting. return; } ....... msg.recycleUnchecked(); } }
接下來我們去看next()方法,我貼的程式碼會盡可能少,只貼重點,每一行都會標明註釋。
Message next() { //這個ptr變數 儲存的是一個記憶體地址,對應本地方法中的指標!,很重要,訊息迴圈要結束,就是將它置為0,當然還要銷燬本地指標。 final long ptr = mPtr; if (ptr == 0) { return null; } //這兩個變數很重要,先注意一下 int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; //死迴圈開始從自己的連結串列裡取訊息 for (;;) { ....... //看這個方法,很重要,從名字上看它就知道是一個native方法,這個方法用的是linux的一種poll機制, 就是你去讀取一個玩意(ptr),然後第二個引數是超時時間,大概就是,如果有資料的話就會返回, 如果沒資料的話,就等待nextPollTimeoutMillis這麼時間,而重要的是它在等待的過程中是不佔用CPU的, 是一種休眠狀態,放棄CPU,讓CPU可以去幹其他的事情。所以我們是不是覺得已經找到了答案?其實還沒有, 因為我們可以看到上面的變數,nextPollTimeoutMillis = 0,所以他沒有設定超時,是一種隨取隨用的狀態,所以 這個方法暫時像普通方法一樣走。 nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // 取訊息 final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; ....... //訊息不為空 if (msg != null) { //如果沒到訊息該執行的時間,就nextPollTimeoutMillis 賦值為還需要等待多久,因為這個變數是給nativePollOnce方法 使用的,所以我們猜想得到,遇到訊息時延時訊息的話,我們會通過這種不佔CPU的方法去睡眠這麼久,醒來剛好 執行延時訊息 if (now < msg.when) { nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { //如果不為空的就返回訊息給Looper. //這個變數很重要,留意一下,如果有訊息並且返回 它是等於false的 mBlocked = false; .......... return msg; } } //訊息為空的情況下,nextPollTimeoutMillis = -1,我們同樣知道它是給nativePollOnce方法用的,傳-1進去,代表著永久 等待,除非有人通過某個方法去喚醒。 else { // No more messages. nextPollTimeoutMillis = -1; } // 如果退出了,就dispose去釋放ptr,ptr = 0,然後返回null,我們知道loop就結束了。 if (mQuitting) { dispose(); return null; } //還記得一開始那兩個變數嗎,一個nextPollTimeoutMillis ,一個pendingIdleHandlerCount = -1, 我們知道Looper剛開始迴圈是沒有訊息的,所以mMessages也等於空,所以如下條件是成立的,然後 pendingIdleHandlerCount = mIdleHandlers.size(),而mIdleHandlers是一個成員變數,一個List,可以通過MQ的 addIdleHander方法來為這個列表新增物件,但是我檢視過,系統沒有去新增的相關程式碼,所以這個size就為0。 if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { pendingIdleHandlerCount = mIdleHandlers.size(); } //如果size為0了,這個條件就成立,那麼mBlocked = true,然後continue就執行下一次迴圈了, 我們知道這個時候nextPollTimeoutMillis 已經等於了 -1,所以迴圈的下一次就會放棄CPU進入無限等待的狀態。 這下我們就知道了,為什麼這個死迴圈不會造成卡死了吧? 那麼問題又來了,無限等待的話,那誰來喚醒他? 請看下一節,嘻嘻。 if (pendingIdleHandlerCount <= 0) { // No idle handlers to run. Loop and wait some more. mBlocked = true; continue; } . ........ 這個中間有很長一段過於IdleHandler的程式碼,它與上面的程式碼邏輯是獨立的 ,不影響上面的執行,所以我把 這裡刪除了,為了避免給大家產生疑惑,覺得看不懂。其實沒關係,看不懂沒關係,我們從程式碼結構和變數上也可以 看的出來,我下面會貼出一片講這段程式碼的部落格。 } ......... //這一步一般執行不到,如果要執行到,mIdleHandlers這個list的size應該是大於0的,而這個列表的相關作業, 不在這個討論範圍。 nextPollTimeoutMillis = 0; } }
雖然我秉承著儘量不貼程式碼的方式,因為我怕大家看到程式碼太多會頭暈眼花,犯困瞌睡,但是如果沒有關鍵程式碼,光靠我說,大家如何信服我說的呢?所以我總結一下這一節的問題:
為什麼死迴圈不會卡死主執行緒? 因為他通過native方法用到了Linux的Poll機制,這是一種放棄CPU進入睡眠狀態的等待機制,還有重要的一點是超時時間設定為負數的話,就代表無限等待。
所以這就是一個Looper開始工作時的狀態,死迴圈了一次,在第二次的時候便進入了不佔CPU無限等待的狀態。那麼它什麼時候被喚醒呢啊?看下節
2、當執行緒的Looper收到訊息的時候,如何喚醒阻塞?
帶著上一節的疑問,我們來到的這一節,能堅持到第二件就說明大家很有毅力了,我現在開始講解誰喚醒了那個無限等待。我們可以先猜想一下,當我們傳送一個訊息的時候,是不是進是它該醒來的時候了?我們去看一下訊息入隊的方法(怎麼從Handler走入到MessageQueue的過程我就不再重複的闡述了,我們直接看MessageQueue的enqueueMessage方法:
boolean enqueueMessage(Message msg, long when) {
.......
synchronized (this) {
//如果發訊息的時候,Looper退出的話,直接回收訊息並且返回
if (mQuitting) {
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;
//從上面一節的分析,我們知道 ,沒訊息的時候,這個mBlocked被賦值為了true.不信的回去看。
needWake = mBlocked;
} else
//如果有訊息的話,needWake的值需要如下三個條件共同成立。為什麼呢?根據原來英文註釋的解釋應該是這樣的:
通常不需要喚醒事件佇列,除非這個是一個最早的非同步訊息,或者在頭部阻塞的情況下。所以在我看來:
enqueue的這個入隊方法,在第一行就對message.target判空了,如果為空的話就丟擲異常,說明這個p.target == null幾乎
在任何時候不成立,而這裡的needWake一般也是false,為什麼要這樣設計呢?我個人感覺是,你看這個if else,如果沒有
訊息的話,它在新增message為佇列頭的時候,就會給needWake = mBlocked = true。所以它本意是隻在佇列頭部喚醒
一次訊息佇列,然後就一直取,直到取完訊息佇列之後,再會進入-1的那種無限等待狀態。
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
.....取合適的佇列訊息
}
}
// We can assume mPtr != 0 because mQuitting is false.
//看這裡,如果是true的話,就通過nativeWake去把ptr這個東西喚醒,而這個東西就是我們在next中nativePollOnce時傳入的。
所以我們可以知道,當訊息入隊時,並且是一個訊息佇列的頭部時,一般會去喚醒阻塞的next()方法。
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
所以我們來總結一下:
當Handler傳送訊息到MessageQueue的時候,在MessageQueue的enqueueMessage入隊方法中,我們會判斷是否需要喚醒阻塞在那邊的next()方法。而喚醒的條件,一般是在佇列的頭部來喚醒,然後一直取,直到取完這個訊息佇列,然後再進入-1的那種無限等待狀態。
可能會有細心的同學發現,如果是那種延時訊息呢?其實我們已經知道,如果是延時訊息的話,它會將nextPollTimeoutMillis 置為需要休眠的時間,然後去休眠這麼久,不佔用CPU,讓CPU繼續去處理下一條訊息,然後等著nextPollTimeoutMillis 時間之後,自動喚醒。
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);
}
3、執行緒中的Looper開啟迴圈之後,會自動退出嗎?何時退出?
這個問題已經不涉及到原始碼了,本來這一節的標題是“子執行緒中的Looper會自動退出嗎?合適退出“,但是當我對原始碼深入的瞭解之後,已經經過自己實驗與查證之後,可以確信。無論是主執行緒還是子執行緒,Looper都不會自動退出。除非你去呼叫它的quit方法。因為在我腦子中,一直相信子執行緒中使用完handler或者looper不用管他,他會自動退出,但是我錯了,根本沒有自動退出的,你需要手動呼叫Looper.quit()方法。所以也給了我們一個警示,在子執行緒中建立了looper之後,記得quit釋放,要不會引發記憶體洩露等各種問題。以下是我的實驗資料:
在activity啟動的時候,開啟一個執行緒建立Looper,開啟迴圈,然後點選按鈕可以傳送訊息:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
handler = new Handler(){
@Override
public void handleMessage(Message msg) {
Log.i("mydata","fuck everyone");
}
};
Looper.loop();
}
}).start();
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.bt_show:
handler.sendEmptyMessage(1);
break;
}
}
11-07 00:06:58.188 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:01.836 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:02.484 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:02.676 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:02.876 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:03.056 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:03.236 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:03.428 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:03.612 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:03.780 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
當然我們也可以用Linux命令來檢視存在多少執行緒,以及可以看到,我們子執行緒並不會自己銷燬,因為Looper一直在loop。
注意:這也為我們提了個醒,這就是我們的主執行緒,為什麼可以一直存在,不會因為沒有訊息就執行結束了。
4、主執行緒的looper與子執行緒的looper有何區別?
他兩主要的區別就在於quit了。因為他們在建立時候有這樣的區別:
public static void prepareMainLooper() { //主執行緒用來建立looper的方法
prepare(false);
........
}
public static void prepare() { //普通執行緒
prepare(true);
}
private static void prepare(boolean quitAllowed) { //都會調到此方法,不同的是變數的值
.......
sThreadLocal.set(new Looper(quitAllowed));
}
所以我們可以知道,主執行緒的looper是quitAllowed = false,子執行緒的是quitAllowed = true。就是主執行緒不允許退出,子執行緒允許退出。其他無恙,所以我們子執行緒使用完Looper一定要記得自己退出,避免產生麻煩。
5、遺留問題,主執行緒Looper會退出嗎?如何退出?
解決完上述問題之後,我又發現和思考了一個有趣的問題,那麼主執行緒的looper是何時退出?它在建立的時候已經表明了quitAllowed = false,不允許退出,我們也可以試著自己手動呼叫Looper.getMainLooper().quit(),可以發現會拋異常。那麼主執行緒的looper到底何時停止,如果不停止那豈不是代表著程式一直會存在嗎?我猜想主線的問題肯定在ActivityThread.java類中可以找到答案。所以我去找了這個類,並且在H這個handler中發現了一個很顯眼的一條訊息:
case EXIT_APPLICATION:
if (mInitialApplication != null) {
mInitialApplication.onTerminate();
}
Looper.myLooper().quit();
break;
我的天啊,我們知道開始一個應用的時候 先發的一個訊息叫BIND_APPLICATION,那麼這個EXIT_APPLICATION是否就標誌著退出應用呢?它是否是退出應用的標誌,我還沒有確定,但可以確定的是,收到這個訊息之後,主執行緒會退出Looper迴圈。但是又有一個奇怪的問題,就是我手動去調這行程式碼,會拋異常,因為主執行緒的looper是不允許退出的,但是在這裡由系統調就不會丟擲異常,我很是鬱悶,我同時也看了層層呼叫,並沒有try catch相關程式碼,這裡算是一個小小的疑問吧,有明白的同學也可以給我解釋一下。
太不容易了,寫這一篇 看了一天原始碼,寫了一天部落格,反覆斟酌,喜歡能夠幫到一些人,有什麼意見可以留下,我們共同討論,共勉。