Android基礎進階 - 消息機制

目錄

  1. Android消息機制流程
  2. Handler
  3. Message
  4. MessageQueue
  5. Looper
  6. HandleThread
  7. 資料
  8. 收穫

篇外話

在“音視頻開發之旅系列”之外,想把自己比較薄弱的Java&Android基礎也抽時間進行學習加強些,這也更符合自己的內心追求和自我期待。並行的開始另外一段學習旅程,從Handler消息機制開啓,結合消息機制的流程以及源碼進行學習分析。

一、Android消息機制流程

我們先通過下面兩張圖來對Android消息機制流程以及關鍵類之間的關係有個瞭解,後面我們再結合源碼一一進行分析。

消息機制的流程

Handler、Message、MessageQueue、Looper之間的關係

圖片來源-Android消息機制1-Handler(Java層)

二、Handler

Handler有兩個主要的用途:

  1. 調度消息在某個時間點執行;
  2. 不同線程之間通信

2.1 全局變量

 final Looper mLooper;     //有Looper的引用
 final MessageQueue mQueue;//有MessageQueue的引用
 final Callback mCallback;
 final boolean mAsynchronous;
 IMessenger mMessenger;

2.2 構造方法

    public Handler() {
        this(null, false);
    }
    public Handler(@NonNull Looper looper) {
        this(looper, null, false);
    }

 public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

2.3 獲取Message

//從Message複用池中獲取一個Message   
 public final Message obtainMessage()
    {
        return Message.obtain(this);
    }

//和上面的方法基本一致,差異在於從複用池中獲取到Message後給what賦值
    public final Message obtainMessage(int what)
    {
        return Message.obtain(this, what);
    }
//...其他obtainMessage類似

2.4 發送消息

圖片來源-Android消息機制1-Handler(Java層)

下面我們挑幾個發送方法來看下

** sendMessage: 發送一個Message,when爲當前的時間**
MessageQueue根據when進行匹配插入位置

    public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }

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

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        ......
        return enqueueMessage(queue, msg, uptimeMillis);
    }

** post:從消息複用池中獲取Message,設置Message的Callback**

public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }
    
    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

** postAtFrontOfQueue(): 將消息插入到隊列頭部**
通過調用sendMessageAtFrontOfQueue 加入一個when爲0的message到隊列,即插入到隊列的頭部,需要注意的是 MessageQueue#enqueueMessage的插入到鏈表中時是根據when比較的(when < p.when),如果之前已經有多個when等於0的消息在隊列中,這個新的會加入到前面when也爲0的後面。

    public final boolean postAtFrontOfQueue(Runnable r)
    {
        return sendMessageAtFrontOfQueue(getPostMessage(r));
    }

    public final boolean sendMessageAtFrontOfQueue(Message msg) {
        MessageQueue queue = mQueue;
        ......
        //第三個參數爲0,即Message的when爲0,插入到隊列的頭部,注意到MessageQueue#enqueueMessage的插入到鏈表中時是根據when比較的(when < p.when),如果之前已經有多個when等於0的消息在隊列中,這個新的會加入到前面when也爲0的後面。
        return enqueueMessage(queue, msg, 0);
    }

2.5 派發消息 dispatchMessage

優先級如下:
Message的回調方法callback.run() >
Handler的回調方法mCallback.handleMessage(msg) > Handler的默認方法handleMessage(msg)

public void dispatchMessage(@NonNull Message msg) {
      //Message的回調方法,優先級最高  
    if (msg.callback != null) {
            handleCallback(msg);
        } else {
            //Handler的mCallBack優先級次之
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //Handler的handleMessage方法優先級最低(大部分都是在該方法中實現Message的處理)
            handleMessage(msg);
        }
    }

三、Message

全局變量

//一些重要的變量

    public int arg1;
    public int arg2;
    public Object obj;
    public long when;
    Bundle data;
    Handler target;  //Message中有個Handler的引用
    Runnable callback;
    
    //Message有next指針,可以組成單向鏈表
    Message next;


    public static final Object sPoolSync = new Object();
    
    //複用池中的第一個Message
    private static Message sPool;
    
    //複用池的大小,默認最大50個(如果短時間內有超過複用池最大數量的Message會怎樣,重新new)
    private static int sPoolSize = 0;
    private static final int MAX_POOL_SIZE = 50;

構造方法
查看下是否有可以複用的message,如果有,複用池的中可複用的Message個數減一,返回該Message;如果沒有重新new一個。注意複用池默認最大數量爲50。

   public static Message obtain() {
       synchronized (sPoolSync) {
           //查看下是否有可以複用的message
           if (sPool != null) {
               //取出第一個Message
               Message m = sPool;
               sPool = m.next;
               m.next = null;
               m.flags = 0; // clear in-use flag
               //複用池的中可複用的Message個數減一
               sPoolSize--;
               return m;
           }
       }
       //如果複用池中沒有Message了重新new
       return new Message();
   }

** recycleUnchecked**
//標記一個Message時異步消息,正常的情況都是同步的Message,當遇到同步屏障的時候,優先執行第一個異步消息。關於同步屏障,我們在MessageQueue中在結合next等方法再介紹。

public void setAsynchronous(boolean async) {
        if (async) {
            flags |= FLAG_ASYNCHRONOUS;
        } else {
            flags &= ~FLAG_ASYNCHRONOUS;
        }
    }


void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            //可以複用的message爲50個,如果超過了就不會再複用
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

//toString和dumpDebug可以Dump出message信息,遇到一些問題時可以幫助分析
android.os.Message#toString(long)

android.os.Message#dumpDebug

四、MessageQueue

MessageQueue是一個單鏈表優先隊列
Message不能直接添加到MessageQueue中,要通過Handler以及相對應的Looper進行添加。

變量

//MessageQueue鏈表中的第一個Message
Message mMessages;

** next:從消息隊列中取出下一條要執行的消息**
如果是同步屏障消息,找到第一個隊列中中第一個異步消息
如果第一個Message的執行時間比當前時間見還要晚,記錄還要多久開始執行;否則就找到下一條要執行的Message。
後面的Looper的loop方法會從過queue.next調用該方法,獲取需要執行的下一個Message,其中會調用到阻塞的native方法nativePollOnce,該方法用於“等待”, 直到下一條消息可用爲止. 如果在此調用期間花費的時間很長, 表明對應線程沒有實際工作要做,不會因此會出現ANR,ANR和這個沒有半毛錢關係。

關鍵代碼如下:

Message next() {

        //native層MessageQueue的指針
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

     ......
        for (;;) {
           
            //阻塞操作,當等待nextPollTimeoutMillis時長,或者消息隊列被喚醒
            //nativePollOnce用於“等待”, 直到下一條消息可用爲止. 如果在此調用期間花費的時間很長, 表明對應線程沒有實際工作要做,不會因此會出現ANR,ANR和這個沒有半毛錢關係。
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                
                //創建一個新的Message指向 當前消息隊列的頭
                Message msg = mMessages;
                
                //如果是同步屏障消息,找到第一個隊列中中第一個異步消息
                if (msg != null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                
                if (msg != null) {
                    //如果第一個Message的執行時間比當前時間見還要晚,記錄還要多久開始執行
                    if (now < msg.when) {                    
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        //否則從鏈表中取出當前的Message ,並且把鏈表中next指向指向下一個Message
                        
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        //取出當前的Message的值,next置爲空
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } 
                .....

                //android.os.MessageQueue#quit時mQuitting爲true
                //如果需要退出,立即執行並返回一個null的Message,android.os.Looper.loop收到一個null的message後退出Looper循環
                if (mQuitting) {
                    dispose();
                    return null;
                }
        ......
            if (pendingIdleHandlerCount <= 0) {
                                // 注意這裏,如果沒有消息需要執行,mBlocked標記爲true,在enqueueMessage會根據該標記判斷是否調用nativeWake喚醒
                                mBlocked = true;
                                continue;
                            }
        ......
    }
    ......
}

enqueueMessage:向消息隊列中插入一條Message

如果消息鏈表爲空,或者插入的Message比消息鏈表第一個消息要執行的更早,直接插入到頭部
否則在鏈表中找到合適位置插入,通常情況下不需要喚醒事件隊列,以下兩個情況除外:

  1. 消息鏈表中只有剛插入的這一個Message,並且mBlocked爲true即,正在阻塞狀態,收到一個消息後也進入喚醒
  2. 鏈表的頭是一個同步屏障,並且該條消息是第一條異步消息

喚醒誰?MessageQueue.next中被阻塞的nativePollOnce

具體實現如下,

關於如何找到合適的位置?這涉及到鏈表的插入算法:引入一個prev變量,該變量指向p也message(如果是for循環的內部第一次執行),然後把p進行向next移動,和需要插入的Message進行比較when

關鍵代碼如下:

 boolean enqueueMessage(Message msg, long when) {
        ......

        synchronized (this) {
          
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            
            //如果消息鏈表爲空,或者插入的Message比消息鏈表第一個消息要執行的更早,直接插入到頭部
            if (p == null || when == 0 || when < p.when) {
                
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
               
                //否則在鏈表中找到合適位置插入
                //通常情況下不需要喚醒事件隊列,除非鏈表的頭是一個同步屏障,並且該條消息是第一條異步消息
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                //具體實現如下,這個畫張圖來說明
                //鏈表引入一個prev變量,該變量指向p也message(如果是for循環的內部第一次執行),然後把p進行向next移動,和需要插入的Message進行比較when
                Message prev;
                
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; 
                prev.next = msg;
            }

            //如果插入的是異步消息,並且消息鏈表第一條消息是同步屏障消息。
    //或者消息鏈表中只有剛插入的這一個Message,並且mBlocked爲true即,正在阻塞狀態,收到一個消息後也進入喚醒
喚醒誰?MessageQueue.next中被阻塞的nativePollOnce
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

簡單着看下native的epoll (這塊還沒有深入分析,後面篇章補上吧)

nativePollOnce 和 nativeWake 利用 epoll 系統調用, 該系統調用可以監視文件描述符中的 IO 事件. nativePollOnce 在某個文件描述符上調用 epoll_wait, 而 nativeWake 寫入一個 IO 操作到描述符
epoll屬於IO複用模式調用,調用epoll_wait等待. 然後 內核從等待狀態中取出 epoll 等待線程, 並且該線程繼續處理新消息

** removeMessages: 移除消息鏈表中對應的消息**
需要注意的是,在該函數的實現中分爲了頭部meg的移除,和非頭部的msg的移除。
移除消息鏈表中頭部的和需要移除相同的msg
eg:msg1.what=0;msg2.what=0;msg3.what=0; msg4.what=1; 需要移除what爲0的msg,即移除前三個

移除消息鏈表中非頭部的對應的消息,eg:msg1.what=1;msg2.what=0;msg3.what=0; 需要移除what爲0的消息,即移除後續的消息,處處體現鏈表的查詢和移除算法

關鍵代碼如下:

void removeMessages(Handler h, int what, Object object) {
  ......
        synchronized (this) {
            Message p = mMessages;

           
            //移除消息鏈表中頭部的和需要移除相同的msg eg:msg1.what=0;msg2.what=0;msg3.what=0; msg4.what=1; 需要移除what爲0的msg,即移除前三個
            while (p != null && p.target == h && p.what == what
                   && (object == null || p.obj == object)) {
                Message n = p.next;
                mMessages = n;
                p.recycleUnchecked();
                p = n;
            }

         
            //移除消息鏈表中非頭部的對應的消息,eg:msg1.what=1;msg2.what=0;msg3.what=0; 需要移除what爲0的消息,即移除後續的消息,處處體現鏈表的查詢和移除算法
            while (p != null) {
                Message n = p.next;
                if (n != null) {
                    if (n.target == h && n.what == what
                        && (object == null || n.obj == object)) {
                        Message nn = n.next;
                        n.recycleUnchecked();
                        p.next = nn;
                        continue;
                    }
                }
                p = n;
            }
        }
    }

** postSyncBarrier:發送同步屏障消息 **

同步屏障也是一個message,只不過這個Message的target爲null,. 通過ViewRootImpl#scheduleTraversals()發送同步屏障消息
同步屏障消息的插入位置並不是都是消息鏈表的頭部,而是根據when等信息而定:如果when不爲0,消息鏈表也不空,在消息鏈表中找到同步屏障要插入入的位置;如果prev爲空,該條同步消息插入到隊列的頭部。

關鍵代碼如下:

 /**
     * android.view.ViewRootImpl#scheduleTraversals()發送同步屏障消息
     * @param when
     * @return
     */
    private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;

            //同步屏障也是一個message,只不過這個Message的target爲null

            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                //如果when不爲0,消息鏈表也不空,在消息鏈表中找到同步屏障要插入入的位置
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                //如果prev爲空,該條同步消息插入到隊列的頭部
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

dump: MessageQueue信息
有時候我們需要dump出當前looper的Message信息來分析一些問題,比不,是否Queue中有很多消息,如果太多就影響隊列中後面的Message的執行,可能造成邏輯處理比較慢,甚至可能導致ANR等情況,MessageQueue的默認複用池是50個,如果太多排隊的Message也會影響性能。通過dump Message信息可以幫助分析。mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);

 void dump(Printer pw, String prefix, Handler h) {
        synchronized (this) {
            long now = SystemClock.uptimeMillis();
            int n = 0;
            for (Message msg = mMessages; msg != null; msg = msg.next) {
                if (h == null || h == msg.target) {
                    pw.println(prefix + "Message " + n + ": " + msg.toString(now));
                }
                n++;
            }
            pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked()
                    + ", quitting=" + mQuitting + ")");
        }
    }

五、Looper

Looper主要涉及到構造、prepare和loop幾個重要的方法,在保證一個線程有且只有一個Looper的設計上,採用了ThreadLocal以及代碼邏輯的控制。

變量

//一些重要的變量  
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

    final MessageQueue mQueue;

    final Thread mThread;

構造方法
在構造Looper的時候 創建和Looper一一對應的MessageQueue

    private Looper(boolean quitAllowed) {
        //在構造Looper的時候 new一一對應的MessageQueue
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

prepare
我們這裏可以看到消息機制是 如何保證一個線程只有一個Looper。

//quitAllowed參數是否允許quit,UI線程的Looper不允許退出,其他的允許退出
private static void prepare(boolean quitAllowed) {
    //保證一個線程只能有一個Looper,這裏的sThreadLocal
    if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

loop
我們在MessageQueue的next方法已經分析過nativePollOnce這個方法可能會阻塞,直到拿到message。
如果next返回一個null的Message退出Looper循環,否則進行msg的派發。
取出的msg執行完之後,會加入到回收池中等待複用。recycleUnchecked我們在Message中也已經分析過了。不清楚的可以再回看。

public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
    ……
    for (;;) {
            //next方法是一個會阻塞的方法,MessageQueue的next方法前面我們已經分析過nativePollOnce這個方法會可能阻塞,直到拿到message。
            Message msg = queue.next(); 
            //收到爲空的msg,Loop循環退出。那麼何時會收到爲空的msg吶? quit
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            //msg的派發,msg.target就是Handler,即調用Handler的dispatchMessage派發消息
            msg.target.dispatchMessage(msg);

            ……
            //msg回收
            msg.recycleUnchecked();
        }

六、HandleThread

HandlerThread是一個帶有Looper的Thread。

全局變量

public class HandlerThread extends Thread {
    int mPriority;//線程優先級
    int mTid = -1;//線程id
    Looper mLooper;
    private Handler mHandler;
    ......
}

構造方法

    public HandlerThread(String name) {
        super(name);
        //用於run時設置線程的優先級Process.setThreadPriority(mPriority);

        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }

    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }

run方法
進行Looper的prepare和loop的調用,配置好Looper環境

    @Override
    public void run() {
        //線程id
        mTid = Process.myTid();
        //調用Looper的prepare方法,把當前該線程關聯的唯一的Looper加入到sThreadLocal中
        Looper.prepare();
        
        synchronized (this) {
            //從sThreadLocal中獲取Looper
            mLooper = Looper.myLooper();
            
            notifyAll();
        }
        //設置線程的優先級,默認THREAD_PRIORITY_DEFAULT,如果是後臺業務可以配置爲THREAD_PRIORITY_BACKGROUND,根據具體場景進行設置
        Process.setThreadPriority(mPriority);
        //可以做一些預設置的操作
        onLooperPrepared();
        //開始looper循環
        Looper.loop();
        mTid = -1;
    }

使用HandlerThread的一般流程如下

// Step 1: 創建並啓動HandlerThread線程,內部包含Looper
HandlerThread handlerThread = new HandlerThread("xxx");
handlerThread.start();

// Step 2: 創建Handler
Handler handler = new Handler(handlerThread.getLooper());

handler.sendMessage(msg);

這樣有一個弊端,就是每次使用Handler都要new HandlerThread,而Thread又是比較佔用內存,
能不能減少Thread的創建,或者說是Thread的複用.
並且實現Message能夠得到及時執行,不被隊列中前面的Message阻塞;
這的確是一個有很有意思很有挑戰的事情。

七、資料

  1. Android源碼
  2. ThreadLocal原理分析與使用場景
  3. Android消息機制1-Handler(Java層)
  4. Android全面解析之Handler機制(終篇):常見問題彙總
  5. Handler真的懂了嗎?
  6. Android 同步屏障?阻塞喚醒? Handler 中隱藏的祕密
  7. IO多路複用之epoll總結
  8. 關於Handler 的這 15 個問題,你都清楚嗎?
  9. 「細品源碼」 Android 系統的血液:Handler
  10. 自信,這是最好的ThreadLocal分析
  11. Android Handler:手把手帶你深入分析 Handler機制源碼
  12. Android Handler.removeMessage移除所有postDelayed的問題
  13. Android 中 MessageQueue 的 nativePollOnce

八、收穫

通過對Android消息機制的源碼分析梳理,搞清楚了

  1. Android消息機制的流程;
  2. 如何保證一個線程對應一個Looper
  3. Message的單鏈表的數據結構設計以及Message的複用池機制
  4. MessageQueue再加入Message和取出Message的實現,以及涉及到阻塞喚醒以及同步屏障。

關於ThreadLocal以及native層的分析還沒有理解透徹,後面兩篇我們再來分析學習下

感謝你的閱讀
下一篇我們ThreadLocal及其Android消息機制中的運用,歡迎關注公衆號“音視頻開發之旅”,一起學習成長。
歡迎交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章