Android程式框架:執行緒通訊的橋樑Handler

蘇策發表於2017-12-27

關於作者

郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。

文章目錄

  • 一 訊息佇列的建立
    • 1.1 建立訊息佇列
    • 1.2 開啟訊息迴圈
  • 二 訊息的新增
  • 三 訊息的分發和處理
    • 3.1 訊息分發
    • 3.2 訊息處理

第一次閱覽本系列文章,請參見導讀,更多文章請參見文章目錄

Android是一個訊息驅動型的系統,訊息機制在Android系統中扮演者重要的角色,與之相關的Handler也是我日常中常用的工具。今天我們就來聊一聊這個。

Android訊息迴圈流程圖如下所示:

Android程式框架:執行緒通訊的橋樑Handler

主要涉及的角色如下所示:

  • Message:訊息,分為硬體產生的訊息(例如:按鈕、觸控)和軟體產生的訊息。
  • MessageQueue:訊息佇列,主要用來向訊息池新增訊息和取走訊息。
  • Looper:訊息迴圈器,主要用來把訊息分發給相應的處理者。
  • Handler:訊息處理器,主要向訊息佇列傳送各種訊息以及處理各種訊息。

整個訊息的迴圈流程還是比較清晰的,具體說來:

  1. Handler通過sendMessage()傳送訊息Message到訊息佇列MessageQueue。
  2. Looper通過loop()不斷提取觸發條件的Message,並將Message交給對應的target handler來處理。
  3. target handler呼叫自身的handleMessage()方法來處理Message。

事實上,在整個訊息迴圈的流程中,並不只有Java層參與,很多重要的工作都是在C++層來完成的。我們來看下這些類的呼叫關係。

Android程式框架:執行緒通訊的橋樑Handler

注:虛線表示關聯關係,實線表示呼叫關係。

在這些類中MessageQueue是Java層與C++層維繫的橋樑,MessageQueue與Looper相關功能都通過MessageQueue的Native方法來完成,而其他虛線連線的類只有關聯關係,並沒有 直接呼叫的關係,它們發生關聯的橋樑是MessageQueue。

有了上面這些分析,相信我們對Android的訊息機制有了大致的理解,對於這套機制,我們很自然會去思考三個方面的問題:

  • 訊息佇列是如何建立的,它們如何實現訊息迴圈的,訊息迴圈為什麼不會導致執行緒卡死??
  • 訊息是如何新增到佇列中的,它們在佇列裡是如何排序的??
  • 訊息是如何被分發的,分發以後又是如何被處理的??

我們一一來看一下。

一 訊息佇列的建立

1.1 建立訊息佇列

訊息佇列是由MessageQueue類來描述的,MessageQueue是Android訊息機制Java層和C++層的紐帶,其中很多核心方法都交由native方法實現。

既然提到物件構建,我們先來看看它的建構函式。

public final class MessageQueue {
    
    private long mPtr; // used by native code
    
    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
    }
}
複製程式碼

可以看到它呼叫的是native方法來完成初始化,這個方法定義在了一個android_os_MessageQueue的C++類類。

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    //構建NativeMessageQueue物件
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    if (!nativeMessageQueue) {
        jniThrowRuntimeException(env, "Unable to allocate native queue");
        return 0;
    }

    nativeMessageQueue->incStrong(env);
    //將nativeMessageQueue物件的地址值轉成long型返回該Java層
    return reinterpret_cast<jlong>(nativeMessageQueue);
}
複製程式碼

可以看到該方法構建了一個NativeMessageQueue物件,並將NativeMessageQueue物件的地址值轉成long型返回給Java層,這裡我們知道實際上是mPtr持有了這個 地址值。

NativeMessageQueue繼承域MessageQueue.cpp類,我們來看看NativeMessageQueue的構造方法。

NativeMessageQueue::NativeMessageQueue() :
        mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
    
    //先檢查是否已經為當前執行緒建立過一個Looper物件
    mLooper = Looper::getForThread();
    if (mLooper == NULL) {
        //建立Looper物件
        mLooper = new Looper(false);
        //為當前執行緒設定Looper物件
        Looper::setForThread(mLooper);
    }
}
複製程式碼

可以看到NativeMessageQueue構造方法先檢查是否已經為當前執行緒建立過一個Looper物件,如果沒有,則建立Looper物件併為當前執行緒設定Looper物件。

我們再來看看Looper的構造方法。

Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
        mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
    int wakeFds[2];
    //建立管道
    int result = pipe(wakeFds);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not create wake pipe.  errno=%d", errno);
    //讀端檔案描述符
    mWakeReadPipeFd = wakeFds[0];
    //寫端檔案描述符
    mWakeWritePipeFd = wakeFds[1];
    result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake read pipe non-blocking.  errno=%d",
            errno);
    result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake write pipe non-blocking.  errno=%d",
            errno);
    //建立一個epoll例項,並將它的檔案描述符儲存在變數mEpollFd中
    mEpollFd = epoll_create(EPOLL_SIZE_HINT);
    LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance.  errno=%d", errno);
    struct epoll_event eventItem;
    memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
    eventItem.events = EPOLLIN;
    eventItem.data.fd = mWakeReadPipeFd;
    //將前面建立的管道讀端描述符新增到這個epoll例項中,以便它可以對管道的寫操作進行監聽
    result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake read pipe to epoll instance.  errno=%d",
            errno);
}
複製程式碼

這裡面提到兩個概念:管道與epoll機制。

關於管道

管道在本質上也是檔案,但它不是普通的檔案,它不屬於任何檔案型別,而且它只存在與記憶體之中且有固定大小的快取區,一般為1頁即4kb。它分為讀端和寫端,讀端負責從 管道讀取資料,當資料為空時則阻塞,寫端負責向管道寫資料,當管道快取區滿時則阻塞。那管道線上程通訊中主要用來通知另一個執行緒。例如:執行緒A準備好了Message放入 了訊息佇列,這個時候需要通知執行緒B去處理,這個時候執行緒A就像管道的寫端寫入資料,管道有了資料之後就回去喚醒執行緒B區處理訊息。也正是基於管道來進行執行緒的休眠與 喚醒,才保住了執行緒中的loop迴圈不會讓執行緒卡死。

關於epoll機制

epoll機制用來監聽多個檔案描述符的IO讀寫事件,在Android的訊息機制用來監聽管道的讀寫IO事件。

關於epool機制,這裡有個簡單易懂的解釋

epoll一共有三個操作方法:

  • epoll_create():建立一個epoll的控制程式碼,size是指監聽的描述符個數
  • epoll_ctl():對需要監聽的檔案描述符(fd)執行操作,比如將fd加入到epoll控制程式碼。
  • epoll_wait():返回需要處理的事件數目,如返回0表示已超時。

上面Looper的構造方法裡,我們已經看到了利用epoll_create()建立一個epoll的例項,並利用epoll_ctl()將管道的讀端描述符操作符新增到epoll例項中,以便可以對管道的 寫操作進行監聽,下面我們還可以看到epoll_wait()的用法。

講到這裡整個訊息佇列便建立完成了,下面我們接著來看看訊息迴圈和如何開啟的。

1.2 開啟訊息迴圈

訊息迴圈是建立在Looper之上的,Looper可以為執行緒新增一個訊息迴圈的功能,具體說來,為了給執行緒新增一個訊息迴圈,我們通常會這麼做:

public class LooperThread extends Thread {

    public Handler mHandler;

    @Override
    public void run() {
        Looper.prepare();
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                // process incoming messages here
            }
        };
        Looper.loop();
    }
}
複製程式碼

可以看到先呼叫Looper.prepare()初始化一個Looper,然後呼叫Looper.loop()開啟迴圈。

關於Looper,有兩個方法來初始化prepare()/prepareMainLooper(),它們建立的Looper物件都是一樣,只是prepareMainLooper() 建立的Looper是給Android主執行緒用的,它還是個靜態物件,以便其他執行緒都可以獲取到它,從而可以向主執行緒傳送訊息。

public final class Looper {
    
   // sThreadLocal.get() will return null unless you've called prepare().
   static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
   private static Looper sMainLooper;  // guarded by Looper.class

    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));
     }
  
      //建立主執行緒的Looper,應用啟動的時候會右系統呼叫,我們一般不需要呼叫這個方法。
      public static void prepareMainLooper() {
          prepare(false);
          synchronized (Looper.class) {
              if (sMainLooper != null) {
                  throw new IllegalStateException("The main Looper has already been prepared.");
              }
              sMainLooper = myLooper();
          }
      }
      
      //返回和當前執行緒相關的Looper
      public static @Nullable Looper myLooper() {
          return sThreadLocal.get();
      }
}

複製程式碼

指的一提的是這裡使用的是ThreadLocal來儲存新建立的Looper物件。

ThreadLocal描述的是執行緒本地儲存區,不同的執行緒不能訪問對方的執行緒本地儲存區,當前執行緒可以對自己的執行緒本地儲存區進行獨立的修改和讀取。

之所以會採用ThreadLocal來儲存Looper,是因為每個具備訊息迴圈能力的執行緒都有自己獨立的Looper,它們彼此獨立,所以需要用執行緒本地儲存區來儲存Looper。

我們在接著來看看Looper的建構函式,如下所示:

public final class Looper {
    
  private Looper(boolean quitAllowed) {
      //建立訊息佇列
      mQueue = new MessageQueue(quitAllowed);
      //指向當前執行緒
      mThread = Thread.currentThread();
   }  
}
複製程式碼

Looper的建構函式也很簡單,構造了一個訊息佇列MessageQueue,並將成員變數mThread指向當前執行緒,這裡構建了一個MessageQueue物件,在MessageQueue構建 的過程中會在C++層構建Looper物件,這個我們上面已經說過。

Looper物件建立完成後就可以開啟訊息迴圈了,這是由loop()方法來完成的。

public final class Looper {
    
     public static void loop() {
        //獲取當前執行緒的Looper
        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;

        //確保當前執行緒處於本地程式中,Handler僅限於處於同一程式間的不同執行緒的通訊。
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        //進入loop主迴圈方法
        for (;;) {
            //不斷的獲取下一條訊息,這個方法可能會被阻塞
            Message msg = queue.next();
            if (msg == null) {
                //如果沒有訊息需要處理,則退出當前迴圈。
                return;
            }

            // 預設為null,可通過setMessageLogging來指定輸出,用於debug
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            //處理訊息
            try {
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            //把message回收到訊息池,以便重複利用。
            msg.recycleUnchecked();
        }
     }
}
複製程式碼

可以看到,這個方法不斷重複著以下三件事:

  1. 呼叫MessageQueue的next()方法讀取MessageQueue的下一條Message。
  2. 把Message分發給相應的target。
  3. 再把分發的Message回收到訊息池,以便重複利用。

如此訊息迴圈便建立起來了。

二 訊息的新增

在如此開始中,我們通常會呼叫handler的sendXXX()或者postXXX()將一個Message或者Runnable,這些方法實際上呼叫的MessageQueue的enqueueMessage()方法,該方法 會給目標執行緒的訊息佇列插入一條訊息。

注:如何理解這個"目標執行緒的訊息佇列",首先要明白Handler、Looper與MessageQueue這三兄弟是全家桶,綁在一起的,你用哪個Handler,訊息就被插入到了這個Handler所線上程 的訊息佇列裡。

public final class MessageQueue {
    
      boolean enqueueMessage(Message msg, long when) {
            //每個訊息都必須有個target handler
            if (msg.target == null) {
                throw new IllegalArgumentException("Message must have a target.");
            }
            
            //每個訊息必須沒有被使用
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }
    
            synchronized (this) {
                //正在退出時,回收Message,加入訊息池。
                if (mQuitting) {
                    IllegalStateException e = new IllegalStateException(
                            msg.target + " sending message to a Handler on a dead thread");
                    Log.w(TAG, e.getMessage(), e);
                    msg.recycle();
                    return false;
                }
    
                msg.markInUse();
                msg.when = when;
                //mMessages表示當前需要處理的訊息,也就是訊息佇列頭的訊息
                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;
                    needWake = mBlocked;
                } else {
                    needWake = mBlocked && p.target == null && msg.isAsynchronous();
                    //將訊息按照時間順序插入到訊息佇列中
                    Message prev;
                    for (;;) {
                        prev = p;
                        p = p.next;
                        if (p == null || when < p.when) {
                            break;
                        }
                        if (needWake && p.isAsynchronous()) {
                            needWake = false;
                        }
                    }
                    msg.next = p; // invariant: p == prev.next
                    prev.next = msg;
                }
    
                // 喚醒訊息佇列
                if (needWake) {
                    nativeWake(mPtr);
                }
            }
            return true;
        }
}
複製程式碼

enqueueMessage()以時間為序將訊息插入到訊息佇列中去,以下三種情況下需要插入到佇列頭部:

  • 訊息佇列為空
  • 要插入的訊息的執行時間為0
  • 要插入的訊息的執行時間小於訊息佇列頭的訊息的執行時間

上面三種情況很容易想到,其他情況以時間為序插入到佇列中間。當有新的訊息插入到訊息佇列頭時,當前執行緒就需要去喚醒目標執行緒(如果它已經睡眠(mBlocked = true)就執行喚醒操作,否則不需要),以便 它可以來處理新插入訊息頭的訊息。

通過這裡的分析,你可以發現,訊息佇列事實上是基於單向連結串列來實現的,雖然我們總稱呼它為"佇列",但它並不是一個佇列(不滿足先進先出)。

同樣利用單向連結串列這種思路的還有物件池,讀者應該有印象,很多文件都提倡通過Message.obtain()方法獲取一個Message物件,這是因為Message物件會被快取在訊息池中,它主要利用 Message的recycle()/obtain()方法進行快取和獲取。

具體說來:

recycle()

public final class Message implements Parcelable {
    
        private static final Object sPoolSync = new Object();
        private static Message sPool;
        
        public void recycle() {
            //判斷訊息是否正在使用
            if (isInUse()) {
                if (gCheckRecycle) {
                    throw new IllegalStateException("This message cannot be recycled because it "
                            + "is still in use.");
                }
                return;
            }
            //對於不再使用的訊息加入訊息池
            recycleUnchecked();
        }
    
        void recycleUnchecked() {
            //將訊息標記為FLAG_IN_USE並清空關於它的其他資訊
            flags = FLAG_IN_USE;
            what = 0;
            arg1 = 0;
            arg2 = 0;
            obj = null;
            replyTo = null;
            sendingUid = -1;
            when = 0;
            target = null;
            callback = null;
            data = null;
    
            synchronized (sPoolSync) {
                //當訊息池沒有滿時,將訊息加入訊息池
                if (sPoolSize < MAX_POOL_SIZE) {
                    //將sPool存放在next變數中
                    next = sPool;
                    //sPool引用當前物件
                    sPool = this;
                    //訊息池數量自增1
                    sPoolSize++;
                }
            }
        }
}
複製程式碼

obtain()

public final class Message implements Parcelable {
    
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                
                //sPool當前持有的訊息物件將作為結果返回
                Message m = sPool;
                //將m的後繼重新賦值給sPool,這其實一個連結串列的刪除操作
                sPool = m.next;
                //m的後繼置為空
                m.next = null;
                //清除 in-use 標誌位
                m.flags = 0;
                //訊息池大小自減1
                sPoolSize--;
                return m;
            }
        }
        //當物件池為空時,直接建立新的Message()物件。
        return new Message();
    }
}
複製程式碼

這裡面有個巧妙的設計,這也給我們如何設計一個物件池提供了一個很好的思路,它是以單向連結串列具體說來:

  1. 在類中定義一個該類的靜態物件sPool以及它的後繼物件next。
  2. 當物件加入物件池時,將該物件加入到連結串列中。
  3. 當物件從物件池中取出時,返回sPool當前持有的物件即可,並將sPool從當前連結串列中移除。

好了。訊息池就聊這麼多,我們接著來看訊息的分發和處理。

三 訊息的分發與處理

3.1 訊息分發

訊息的分發是建立在訊息迴圈之上的,在不斷的迴圈中拉取佇列裡的訊息,訊息迴圈的建立流程我們上面已經分析過,通過分析得知,loop()方法 不斷呼叫MessageQueue的next()讀取訊息佇列裡的訊息,從而進行訊息的分發。

我們來看看next()方法的實現。

public final class MessageQueue {
    
    Message next() {
           final long ptr = mPtr;
           //當前訊息迴圈已退出,直接返回。
           if (ptr == 0) {
               return null;
           }
   
           //pendingIdleHandlerCount儲存的是註冊到訊息佇列中空閒Handler個個數
           int pendingIdleHandlerCount = -1; 
           //nextPollTimeoutMillisb表示當前無訊息到來時,當前執行緒需要進入睡眠狀態的
           //時間,0表示不進入睡眠狀態,-1表示進入無限等待的睡眠狀態,直到有人將它喚醒
           int nextPollTimeoutMillis = 0;
           for (;;) {
               if (nextPollTimeoutMillis != 0) {
                   Binder.flushPendingCommands();
               }
   
               //nativePollOnce是阻塞操作,用來檢測當前執行緒的訊息佇列中是否有訊息需要處理
               nativePollOnce(ptr, nextPollTimeoutMillis);
   
               //查詢下一條需要執行的訊息
               synchronized (this) {
                   final long now = SystemClock.uptimeMillis();
                   Message prevMsg = null;
                   //mMessages代表了當前執行緒需要處理的訊息
                   Message msg = mMessages;
                    
                   //查詢第一個可以處理的訊息(msg.target == null表示沒有處理Handler,無法進行處理,忽略掉)
                   if (msg != null && msg.target == null) {
                       do {
                           prevMsg = msg;
                           msg = msg.next;
                       } while (msg != null && !msg.isAsynchronous());
                   }
                   if (msg != null) {
                       //如果訊息的執行時間大於當前時間,則計算執行緒需要睡眠等待的時間
                       if (now < msg.when) {
                           //當非同步訊息的觸發時間大於當前時間,則使者下一次輪詢的超時時長
                           nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                       } 
                       //如果訊息的執行時間小於當前時間,則說明該訊息需要立即執行,則將該訊息返回,並從訊息佇列中
                       //將該訊息移除
                       else {
                           mBlocked = false;
                           if (prevMsg != null) {
                               prevMsg.next = msg.next;
                           } else {
                               mMessages = msg.next;
                           }
                           msg.next = null;
                           if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                           //設定訊息的使用狀態,即flags |= FLAG_IN_USE。
                           msg.markInUse();
                           return msg;
                       }
                   } else {
                       // 如果沒有更多訊息需要處理,則將nextPollTimeoutMillis置為-1,讓當前執行緒進入無限睡眠狀態,直到
                       //被其他執行緒喚醒。
                       nextPollTimeoutMillis = -1;
                   }
   
                   //所有訊息都已經被處理,準備退出。
                   if (mQuitting) {
                       dispose();
                       return null;
                   }
                   
                   //pendingIdleHandlerCount指的是等待執行的Handler的數量,mIdleHandlers是一個空閒Handler列表
                   if (pendingIdleHandlerCount < 0
                           && (mMessages == null || now < mMessages.when)) {
                       pendingIdleHandlerCount = mIdleHandlers.size();
                   }
                   if (pendingIdleHandlerCount <= 0) {
                       //當沒有空閒的Handler需要執行時進入阻塞狀態,mBlocked設定為true
                       mBlocked = true;
                       continue;
                   }
   
                   //mPendingIdleHandler是一個IdleHandler陣列
                   if (mPendingIdleHandlers == null) {
                       mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                   }
                   mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
               }
   
               //只有在第一次迴圈時,才會去執行idle handlers,執行完成後重置pendingIdleHandlerCount為0
               for (int i = 0; i < pendingIdleHandlerCount; i++) {
                   final IdleHandler idler = mPendingIdleHandlers[i];
                   //釋放Handler的引用
                   mPendingIdleHandlers[i] = null;
   
                   boolean keep = false;
                   try {
                       keep = idler.queueIdle();
                   } catch (Throwable t) {
                       Log.wtf(TAG, "IdleHandler threw exception", t);
                   }
   
                   if (!keep) {
                       synchronized (this) {
                           mIdleHandlers.remove(idler);
                       }
                   }
               }
   
               //執行完成後,重置pendingIdleHandlerCount為0,以保證不會再次重複執行。
               pendingIdleHandlerCount = 0;
               
               //當呼叫一個空閒Handler時,新的訊息可以立即被分發,因此無需再設定超時時間。
               nextPollTimeoutMillis = 0;
           }
       } 
}
複製程式碼

首先要明確一個概念,MessageQueue是利用物件間的後繼關聯(每個物件都知道自己的直接後繼)實現的連結串列,其中它的成員變數mMessages變數指的是當前需要被處理訊息。

next()方法主要用來從訊息佇列裡迴圈獲取訊息,這分為兩步:

  1. 呼叫nativePollOnce(ptr, nextPollTimeoutMillis)方法檢測當前執行緒的訊息佇列中是否有訊息需要處理(注意不是在這裡取訊息)。它是一個阻塞操作,可能會引起 執行緒睡眠,下面我們會說。
  2. 查詢當前需要處理的訊息,返回並將其從訊息佇列中移除。

這個查詢當前需要處理的訊息可以分為三步:

  1. 找到當前的訊息佇列頭mMessages,如果它為空就說明整個訊息佇列為空,就將nextPollTimeoutMillis置為-1,當前執行緒進入無限睡眠等待,知道別的執行緒將其喚醒。如果 它不為空,則進入步驟2.
  2. 如果訊息佇列頭的執行之間大於當前時間,則說明執行緒需要等待該訊息的執行,執行緒進入睡眠。否則進入步驟3.
  3. 查詢到了當前需要被處理的訊息,將該訊息從訊息佇列裡移除,並返回該訊息。

可以看到這裡呼叫的是native方法nativePollOnce()來檢查當前執行緒是否有訊息需要處理,呼叫該方法時,執行緒有可能進入睡眠狀態,具體由nextPollTimeoutMillis引數決定。0表示不進入睡眠狀態,-1表示 進入無限等待的睡眠狀態,直到有人將它喚醒。

我們接著來看看nativePollOnce()方法的實現。

nativePollOnce()方法是個native方法,它按照呼叫鏈:android_os_MessageQueue#nativePollOnce() -> NativeMessageQueue#pollOnce() -> Looper#pollOnce() -> Looper#pollInner() 最終完成了訊息的拉取。可見實現功能的還是在Looper.cpp裡。

我們來看一下實現。

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        ...
        //內部不斷呼叫pollInner()方法檢查是否有新訊息需要處理
        result = pollInner(timeoutMillis);
    }
}

int Looper::pollInner(int timeoutMillis) {
    ...
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    
    //呼叫epoll_wait()函式監聽前面註冊在mEpollFd例項裡的管道檔案描述符中的讀寫事件。如果這些管道
    //檔案描述符沒有發生讀寫事件,則當前執行緒會在epoll_wait()方法裡進入睡眠,睡眠事件由timeoutMillis決定
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    ...
    
    //epoll_wait返回後,檢查哪一個管道檔案描述符發生了讀寫事件
    for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        //如果fd是當前執行緒關聯的管道讀端描述符且讀寫事件型別是EPOLLIN
        //就說明當前執行緒關聯的一個管道寫入了新的資料,這個時候就會呼叫
        //awoken()去喚醒執行緒
        if (fd == mWakeReadPipeFd) {
            if (epollEvents & EPOLLIN) {
                //此時已經喚醒執行緒,讀取清空管道資料
                awoken();
            } else {
                ALOGW("Ignoring unexpected epoll events 0x%x on wake read pipe.", epollEvents);
            }
        } 
        ...
     }
     ...
    return result;
}
複製程式碼

可以看到這個方法做了兩件事情:

  1. 呼叫epoll_wait()函式監聽前面註冊在mEpollFd例項裡的管道檔案描述符中的讀寫事件。如果這些管道檔案描述符沒有發生讀寫事件,則當前執行緒 會在epoll_wait()方法裡進入睡眠,睡眠事件由timeoutMillis決定。
  2. 如果fd是當前執行緒關聯的管道讀端描述符且讀寫事件型別是EPOLLIN就說明當前執行緒關聯的一個管道寫入了新的資料,這個時候就會呼叫awoken()去喚醒執行緒。

至此,訊息完成了分發。從上面的loop()方法我們可以知道,訊息完成分發後會接著呼叫Handler的dispatchMessage()方法來處理訊息。

我們接著來聊一聊Handler。

3.1 訊息處理

Handler主要用來傳送和處理訊息,它會和自己的Thread以及MessageQueue相關聯,當建立一個Hanlder時,它就會被繫結到建立它的執行緒上,它會向 這個執行緒的訊息佇列分發Message和Runnable。

一般說來,Handler主要有兩個用途:

  • 排程Message和Runnable,延時執行任務。
  • 進行執行緒的切換,請求別的執行緒完成相關操作。

我們先來看看Handler的建構函式。

public class Handler {
    
    //無參構造方法,最常用。
    public Handler() {
        this(null, false);
    }

    public Handler(Callback callback) {
        this(callback, false);
    }

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

    public Handler(Looper looper, Callback callback) {
        this(looper, callback, false);
    }
    
    public Handler(Callback callback, boolean async) {
        //匿名類、本地類都必須宣告為static,否則會警告可能出現記憶體洩漏,這個提示我們應該很熟悉了。
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
    
        //獲取當前執行緒的Looper
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        //獲取當前執行緒的訊息佇列
        mQueue = mLooper.mQueue;
        //回撥方法,這個Callback裡面其實只有個handleMessage()方法,我們實現這個
        //介面,就不用去用匿名內部類那樣的方式來建立Handler了。
        mCallback = callback;
        //設定訊息是否為非同步處理方式
        mAsynchronous = async;
    }   
}
複製程式碼

對於構造方法而言,我們最常用的是無參構造方法,它沒有Callback回撥,且訊息處理方式為同步處理,從這裡我們也可以看出你在哪個執行緒裡建立了Handler,就預設使用當前執行緒的Looper。

從上面的loop()方法中,我們知道Looper會呼叫MessageQueue的dispatchMessage()方法進行訊息的處理,我們來看看這個方法的實現。

public class Handler {
        public void dispatchMessage(Message msg) {
            //當Message存在回撥方法時,優先呼叫Message的回撥方法message.callback.run()
            if (msg.callback != null) {
                //實際呼叫的就是message.callback.run();
                handleCallback(msg);
            } else {
                //如果我們設定了Callback回撥,優先呼叫Callback回撥。
                if (mCallback != null) {
                    if (mCallback.handleMessage(msg)) {
                        return;
                    }
                }
                //如果我們沒有設定了Callback回撥,則回撥自身的Callback方法。
                handleMessage(msg);
            }
        }
}
複製程式碼

可以看到整個訊息分發流程如下所示:

  1. 當Message存在回撥方法時,優先呼叫Message的回撥方法message.callback.run()。
  2. 果我們設定了Callback回撥,優先呼叫Callback回撥。
  3. 如果我們沒有設定了Callback回撥,則回撥自身的Callback方法。

由此我們也可以得知方法呼叫的優先順序,從高到低依次為:

  • message.callback.run()
  • Handler.mCallback.handleMessage()
  • Handler.handleMessage()

大部分程式碼都是以匿名內部類的形式實現了Handler,所以一般會走到第三個流程。

可以看到所以傳送訊息的方法最終都是呼叫MessageQueue的enqueueMessage()方法來實現,這個我們上面在分析MessageQueue的時候已經說過,這裡就不再贅述。

理解了上面的內容,相信讀者已經對Android的訊息機制有了大致的瞭解,我們趁熱打鐵來聊一聊實際業務開發中遇到的一些問題。

在日常的開發中,我們通常在子執行緒中執行耗時任務,主執行緒更新UI,更新的手段也多種多樣,如Activity#runOnUIThread()、View#post()等等,它們之間有何區別呢?如果我的程式碼了 既沒有Activity也沒有View,我該如何將程式碼切換回主執行緒呢??

我們一一來分析。

首先,Activity裡的Handler直接呼叫的就是預設的無參構造方法。可以看到在上面的構造方法裡呼叫Looper.myLooper()去獲取當前執行緒的Looper,對於Activity而言當前執行緒就是主執行緒(UI執行緒),那主執行緒 的Looper是什麼時候建立的呢??

03Android元件框架:Android檢視容器Activity一文 裡我們就分析過,ActivityThread的main()函式作為應用的入口,會去初始化Looper,並開啟訊息迴圈。

public final class ActivityThread {
      public static void main(String[] args) {
          ...
          Looper.prepareMainLooper();
          ...
          if (sMainThreadHandler == null) {
              sMainThreadHandler = thread.getHandler();
          }
          ...
          Looper.loop();
          throw new RuntimeException("Main thread loop unexpectedly exited");
      }  
}
複製程式碼

主執行緒的Looper已經準備就緒了,我們再呼叫Handler的建構函式去構建Handler物件時就會預設使用這個Handler,如下所示:

public class Activity {
    
   final Handler mHandler = new Handler();

   public final void runOnUiThread(Runnable action) {
          if (Thread.currentThread() != mUiThread) {
              mHandler.post(action);
          } else {
              action.run();
          }
      }  
}
複製程式碼
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    
    public boolean post(Runnable action) {
        
        //當View被新增到window時會新增一些附加資訊,這裡面就包括Handler
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }

        //Handler等相關資訊還沒有被關聯到Activity,先建立一個排隊佇列。
        //這其實就相當於你去銀行辦事,銀行沒開門,你們在門口排隊等著一樣。
        getRunQueue().post(action);
        return true;
    }
}
複製程式碼

這裡面也是利用attachInfo.mHandler來處理訊息,它事實上是一個Handler的子類ViewRootHandler,同樣的它也是使用Looper.prepareMainLooper()構建出來的Looper。

所以你可以看出Activity#runOnUIThread()、View#post()這兩種方式並沒有本質上的區別,最終還都是通過Handler來傳送訊息。那麼對於那些既不在Activity裡、也不在View裡的程式碼 當我們想向主執行緒傳送訊息或者將某段程式碼(通常都是介面的回撥方法,在這些方法裡需要更新UI)post到主執行緒中執行,就可以按照以下方式進行:

Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
    @Override
    public void run() {
        //TODO refresh ui
    }
})
複製程式碼

好了,到這裡整個Android的訊息機制我們都已經分析完了,如果對底層的管道這些東西感覺比較模糊,可以先理解Java層的實現。

相關文章