Android Handler與Looper原理簡析

看書的小蝸牛發表於2019-03-04

本文分析下Android的訊息處理機制,主要是針對Handler、Looper、MessageQueue組成的非同步訊息處理模型,先主觀想一下這個模型需要的材料:

  • 訊息佇列:通過Handler傳送的訊息並是即刻執行的,因此需要一個佇列來維護
  • 工作執行緒:需要一個執行緒不斷摘取訊息,並執行回撥,這種執行緒就是Looper執行緒
  • 互斥機制,會有不同的執行緒向同一個訊息佇列插入訊息,這個時候就需要同步機制進行保證
  • 空訊息佇列時候的同步機制,生產者消費者模型

上面的三個部分可以簡單的歸結為如下圖:

Android Handler與Looper原理簡析
Looper執行模型.jpg

APP端UI執行緒都是Looper執行緒,每個Looper執行緒中維護一個訊息佇列,其他執行緒比如Binder執行緒或者自定義執行緒,都能通過Handler物件向Handler所依附訊息佇列執行緒傳送訊息,比如點選事件,都是通過InputManagerService處理後,通過binder通訊,傳送到App端Binder執行緒,再由Binder執行緒向UI執行緒傳送送Message,其實就是通過Handler向UI的MessageQueue插入訊息,與此同時,其他執行緒也能通過Handler向UI執行緒傳送訊息,顯然這裡就需要同步,以上就是Android訊息處理模型的簡單描述,之後跟蹤原始碼,淺析一下具體的實現,以及裡面的一些小手段,首先,從Handler的常見用法入手,分析其實現原理,

Handler的一種基本用法--訊息Message的插入

      <關鍵點1>
    Handler hanlder=new Handler();
    <關鍵點2>
    hanlder.post(new Runnable() {
        @Override
        public void run() {
            //TODO 
        }
    });複製程式碼

這裡有兩個點需要注意,先看關鍵點1,Handler物件的建立,直觀來看可能感覺不到有什麼注意的地方,但是如果你在普通執行緒建立Handler,就會遇到異常,因為普通執行緒是不能建立Handler物件的,必須是Looper執行緒才能建立,才有意義,可以看下其建構函式:

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;
}複製程式碼

從上面的程式碼可以看出,Looper.myLooper()必須非空,否則就會丟擲 RuntimeException異常,Looper.myLooper()什麼時候才會非空?

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

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.prepare的執行緒,才會生成一個執行緒單利的Looper物件,Looper.prepare只能呼叫一次,再次呼叫會丟擲異常。其實prepare的作用就是新建一個Looper物件,而在new Looper物件的時候,會建立關鍵的訊息佇列物件:

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}複製程式碼

之後,一個執行緒就有了MessageQueue,雖然還沒有呼叫Loop.loop()將執行緒變成loop執行緒,但是new Handler已經沒問題。接著看hanlder.post函式,它將會建立一個Message(如果需要),並將Message插入到MessageQueue,供loop執行緒摘取並執行。

   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;
}

// 靜態方法,同步
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}複製程式碼

上面的Message新建流程,其實主要是涉及了一個Message執行緒池,預設執行緒池大小50,當然,不採用執行緒池,全部新建Message也是可以的,採用執行緒池主要是為了提高效率,避免重複建立物件,因為Handler與Message的時候實在是太頻繁了,Message執行緒池訊息池常用的方法有兩個:obtain()和recycle(),前者是用於從執行緒池取出一個乾淨的Message,而後者是用於將使用完的Message清理乾淨,並放回執行緒池,當然以上方法都是需要同步的。之後,通過Looper物件將Message插入到MessageQueue,Handler發訊息最終都會呼叫sendMessageAtTime函式

 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);
}   

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}複製程式碼

mAsynchronous可以先不關心,我們使用的一般是mAsynchronous=false的,可以看到,Handler最後通過MessageQueue的enqueueMessage函式來進行插入,

boolean enqueueMessage(Message msg, long when)  {
        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) {
        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
        <!--關鍵點1-->
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
        <!--關鍵點2-->
            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;
        }
        <!--關鍵點3-->
        if (needWake) {
            nativeWake(mPtr);
        } }
    return true; }複製程式碼

很明顯enqueueMessage需要同步,因為存在多個執行緒往一個Loop執行緒的MessageQueue中插入訊息的場景。 這裡其實是將Message根據延時插入到特定的地方,先看下關鍵點1,mMessages其實代表訊息佇列的頭部,如果mMessages為空,說明還沒有訊息,如果當前插入的訊息不需要延時,或者說延時比mMessages頭訊息的延時要小,那麼當前要插入的訊息就需要放在頭部,至於是否需要喚醒佇列,則需要根據當前的Loop執行緒的狀態來判斷,後面講Loop執行緒的時候再回過頭說;再來看下關鍵點2,這個時候需要將訊息插入到佇列中間,其實就是找到第一個Delay事件小於當前Message的非空Message,並插入到它的前面,往佇列中插入訊息時,如果Loop執行緒在睡眠,是不應該喚醒的,非同步訊息的處理會更加特殊一些,先不討論。最後看關鍵點3,如果需要喚醒Loop執行緒,通過nativeWake喚醒,以上,普通訊息的插入算結束了,接下來看一下訊息的執行。

MessageQueue中Message訊息的執行

在訊息的傳送部分已經訊息模型的兩個必要條件:訊息隊裡+互斥機制,接下來看一下其他兩個條件,Loop執行緒+消費者模型的同步機制。MessageQueue只有同Loop執行緒(死迴圈執行緒)配合起來才有意義,普通執行緒必須可以通過Looper的loop函式變成Loop執行緒,loop函式除了是個死迴圈,還包含了從MessageQueue摘取訊息並執行的邏輯。看一下這個函式:

public static void loop() {
  `<!--關鍵點1 確保MessageQueue準備好-->
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    ...
    <!--關鍵點2-->
    for (;;) {
    <!--關鍵點3 獲取一個訊息,如果佇列為空,阻塞等待-->
        Message msg = queue.next(); // might block
        if (msg == null) {
          // No message indicates that the message queue is quitting.
            return;
        }
    <!--關鍵點4 執行訊息回撥-->
        msg.target.dispatchMessage(msg);
    ...
    <!--關鍵點5 清理,回收到快取池-->
        msg.recycleUnchecked();
    }
}複製程式碼

先看下關鍵點1,它要確保當前執行緒已經呼叫過Looper.prepare函式,並且準備好了MessageQueue訊息佇列;再看關鍵點2,其實就是將執行緒化身成Looper執行緒,變成死迴圈,不斷的讀取執行訊息;關鍵點3,就是從MessageQueue摘取訊息的函式,如果當前訊息佇列上沒有訊息,Loop執行緒就會進入阻塞,直到其他執行緒插入訊息,喚醒當前執行緒。如果訊息讀取成功,就走到關鍵點4,執行target物件的回撥函式,執行完畢,進入關鍵點5,回收清理Message物件,放入Message快取池。直接看關鍵點3,訊息的摘取與阻塞:

   Message next() {

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
           <!--關鍵點1 是否需要阻塞等待,第一次一定不阻塞-->
            nativePollOnce(ptr, nextPollTimeoutMillis);
           <!--關鍵點2 同步互斥-->
            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
          <!--關鍵點3 是否存在barier-->
                if (msg != null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
           <!--關鍵點4 第一個訊息是否需要阻塞等待,並計算出阻塞等待時間-->
                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;
                        msg.markInUse();
                        return msg;
                    }
                } else {
           <!--關鍵點5 需要無限等待-->
                    nextPollTimeoutMillis = -1;
                }         
          <!--關鍵點6 沒有可以即刻執行的Message,檢視是否存在需要處理的IdleHandler,如果不存在,則返回,阻塞等待,如果存在則執行IdleHandler-->
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }
            <!--關鍵點7處理IdleHandler-->
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
           <!--處理完IdleHandler ,需要重新判斷Message佇列 nextPollTimeoutMillis賦值為0-->
            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0;
        }
    }複製程式碼

先看下關鍵點1 nativePollOnce,這是個native函式,其主要作用是設定一個定時的睡眠,其引數timeoutMillis,不同的值意義不同

  • timeoutMillis =0 :無需睡眠,直接返回
  • timeoutMillis >0 :睡眠如果超過timeoutMillis,就返回
  • timeoutMillis =-1:一直睡眠,知道其他執行緒喚醒它

next函式中,nextPollTimeoutMillis初始值=0 ,所以for迴圈第一次是一定不會阻塞的,如果能找到一個Delay倒數計時結束的訊息,就返回該訊息,否則,執行第二次迴圈,睡眠等待,直到頭部第一個訊息Delay時間結束,所以next函式一定會返回一個Message物件。再看MessageQueue的nativePollOnce函式之前,先走通整個流程,接著看關鍵點2,這裡其實是牽扯到一個互斥的問題,防止多個執行緒同時從訊息佇列取訊息,關鍵點3主要是看看是否需要處理非同步訊息,關鍵點4,是常用的入口,看取到的訊息是不是需要立即執行,需要立即執行的就返回當前訊息,如果需要等待,計算出等待時間。最後,如果需要等待,還要檢視,IdleHandler列表是否為空,不為空的話,需要處理IdleHandler列表,最後,重新計算一遍。

接著分析nativePollOnce函式,該函式可以看做睡眠阻塞的入口,該函式是一個native函式,牽扯到native層的Looper與MessageQueue,因為java層的MessageQueue只是一個簡單的類,沒有處理睡眠與喚醒的機制,首先看一下Java層MessageQueue建構函式,這裡牽扯到後面的執行緒阻塞原理:

MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();
}複製程式碼

MessageQueue的nativeInit函式在Native層建立了NativeMessageQueue與Looper,不過對於Java層來說,Native層的NativeMessageQueue只用來處理執行緒的睡眠與喚醒,Java層傳送的訊息還是在Java層被處理

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

    nativeMessageQueue->incStrong(env);
    return reinterpret_cast<jlong>(nativeMessageQueue);
}

NativeMessageQueue::NativeMessageQueue() :
        mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
    mLooper = Looper::getForThread();
    if (mLooper == NULL) {
        mLooper = new Looper(false);
        Looper::setForThread(mLooper);
    }
}

Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
        mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
        mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
    <!--關鍵點1-->
    <!-- eventfd 這個函式會建立一個 事件物件 老版本用管道來實現-->
    mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);

    AutoMutex _l(mLock);
    rebuildEpollLocked();
}

void Looper::rebuildEpollLocked() {
if (mEpollFd >= 0) {
    close(mEpollFd);
}
mEpollFd = epoll_create(EPOLL_SIZE_HINT);

struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd;
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);

for (size_t i = 0; i < mRequests.size(); i++) {
    const Request& request = mRequests.valueAt(i);
    struct epoll_event eventItem;
    request.initEventItem(&eventItem);
    int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem);
    if (epollResult < 0) {
        ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s",
              request.fd, strerror(errno));
    }
}複製程式碼

}

看一下關鍵點1,這裡其實是採用了Linux的新API,這裡用的是7.0的原始碼,eventfd函式會建立一個eventfd,這是一個計數器相關的fd,計數器不為零是有可讀事件發生,read以後計數器清零,write遞增計數器;返回的fd可以進行如下操作:read、write、select(poll、epoll)、close,現在我們知道了,Native層有也有一套MessageQueue與Looper,簡單看一下Java層如何使用Native層物件的,接著走nativePollOnce

static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jlong ptr, jint timeoutMillis) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    mPollEnv = env;
    mPollObj = pollObj;
    mLooper->pollOnce(timeoutMillis);
    mPollObj = NULL;
    mPollEnv = NULL;

}複製程式碼

所以最終呼叫Looper::pollOnce,Java層有自己的訊息佇列,pollOnce也沒有更新Java層物件,那麼Native層的訊息隊裡對於Java層有什麼用呢,其實只有睡眠與喚醒的作用,比如2.3之前的版本,Native層的MessageQueue都不具備傳送訊息的能力。不過後來Native新增了傳送訊息的功能,但是日常開發我們用不到,不過如果native層如果有訊息,一定會優先執行native層的訊息

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
            ...
        result = pollInner(timeoutMillis);
    }
}複製程式碼

pollInner 函式比較長,主要是通過利用epoll_wait監聽上面的管道或者eventfd,等待超時或者其他執行緒的喚醒,不過多分析

     int Looper::pollInner(int timeoutMillis) {

       mPolling = true;
        <!--關鍵點1-->
        struct epoll_event eventItems[EPOLL_MAX_EVENTS];
        int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
         <!--關鍵點2-->
        mPolling = false;
        mLock.lock();
           <!--關鍵點3 檢視那個fd上又寫入操作-->                for (int i = 0; i < eventCount; i++) {
            int fd = eventItems[i].data.fd;
            uint32_t epollEvents = eventItems[i].events;
            <!--關鍵點5 喚醒fd 上有寫入操作 返回Java層繼續執行-->
            if (fd == mWakeEventFd) {
                if (epollEvents & EPOLLIN) {
                    awoken();
                } else { } } 
                else {
              <!--關鍵點6 本地MessageQueue有訊息,執行本地訊息-->    
                } }複製程式碼

以上牽扯到Linux中的epoll機制:epoll_create、epoll_ctl、epoll_wait、close等用一句話概括:執行緒阻塞監聽多個fd控制程式碼,其中一個fd有寫入操作,當前執行緒就被喚醒。這裡不用太過於糾結,只要理解,這是執行緒間通訊的一種方式,為了處理多執行緒間生產者與消費者通訊模型用的,看下7.0原始碼中native層實現的同步邏輯:

Android Handler與Looper原理簡析
Looper Java層與native層關係7.0.jpg

在更早的Android版本中,同步邏輯是利用管道通訊實現的,不過思想是一致的,看一下4.3的程式碼

Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
        mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
    int wakeFds[2];
    int result = pipe(wakeFds);
    mWakeReadPipeFd = wakeFds[0];
    mWakeWritePipeFd = wakeFds[1];
    result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
    result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);
    // Allocate the epoll instance and register the wake pipe.
    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;
    result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);
}複製程式碼

Android Handler與Looper原理簡析
Looper Java層與native層關係4.3.jpg

小結

  • loop執行緒睡眠的原理 :在MessageQueue中找到下一個需要執行的訊息,沒有訊息的話,需要無限睡眠等待其他執行緒插入訊息喚醒,如果有訊息,計算出執行下一個訊息需要等待的時間,阻塞等待,直到超時。
  • Java層與Native層兩份訊息佇列:Java層的主要是為了業務邏輯,native層,主要為了睡眠與喚醒
  • 睡眠與喚醒的實現手段:早期版本通過管道,後來如6.0、7.0的版本,是通過eventfd來實現,思想一致。

相關文章