1. 程式人生 > >Android Handler訊息機制中的諸多疑問

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相關程式碼,這裡算是一個小小的疑問吧,有明白的同學也可以給我解釋一下。

太不容易了,寫這一篇 看了一天原始碼,寫了一天部落格,反覆斟酌,喜歡能夠幫到一些人,有什麼意見可以留下,我們共同討論,共勉。