重學Android-我對Handler有了新認識

 

Handler,我們Android工程師每天都在用的東西,它主要可以用來做線程間通信,這一塊知識其實我們很熟悉了,但是最近我又翻了一遍Handler源碼,發現我對Handler又有了新的認識,所以總結了這篇文章記錄一下, 本文的源代碼基於8.0。

在文章開始之前,友情提示,我會默認大家有了解Handler,Looper,MessageQueue的基礎知識,不清楚的同學可以自行查閱別的資料哈。

由於Handler在Framework層的代碼量也是比較大的,一篇文章不可能面面俱到,所以我打算從Handler的使用入手,只對關鍵節點進行深入,這樣既不會太陷入細節,也能把握Handler背後的整個流程。

使用-消息的發送

先看一下Handler的使用,我們先定義一個MyHandler靜態內部類如下:

    private static class MyHandler extends Handler {

        WeakReference<MainActivity> mMainActWeakRef;

        public MyHandler(MainActivity activity) {
            this.mMainActWeakRef = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }

然後我們new一個MyHandler對象,就可以通過向MyHandler發消息來實現我們的邏輯了。

    // MainActivity,onCreate
    mMyHandler = new MyHandler(this);
    mMyHandler.sendMessage(new Message());

我們調用了sendMessage方法,最終會觸發mMyHandler的handleMessage方法被回調,參數就是我們發送的Message。我們看下sendMessage的實現

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

可以看到,先調用Handler的sendMessageDelayed方法,由於delay爲0,所以消息的發送時間是SystemClock.uptimeMillis()獲得的當前時間,接着調用enqueueMessage方法,內部調用了MessageQueue的enqueueMessage方法,具體發送到哪個MessageQueue由Handler初始化時綁定的Looper決定的,一般我們在主線程new的Handler默認使用MainLooper,MainLooper的初始化在ActivityThread的main方法中。

接下來我們看下MessageQueue的enqueueMessage方法的實現,關鍵方法多已經添加註釋了。

    boolean enqueueMessage(Message msg, long when) {
    // ....
        synchronized (this) {
            // 設置消息inUse標識
            msg.markInUse();
            // 消息何時被處理
            msg.when = when;
            // mMessages是消息隊列頭,注意數據結構是單鏈表
            Message p = mMessages;
            boolean needWake;
            // 如果隊列是空,或者when的時間爲0,或者當前插入的時間小於表頭消息時間
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                // 將消息插入表頭,並檢查當前是否被阻塞,如果被阻塞,設置needWake標識
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                // 這一坨邏輯就是將新的msg插入到一個合適的位置,按照when字段從小到大排序
                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;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            // 如果需要喚醒,喚醒Looper
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

所以插入消息到消息隊列是立即插入的,按照消息的when字段來排序,如果是delay的消息,它的when就是當前時間+delay時間,最後有一個很關鍵的方法調用 nativeWake(mPtr),它的作用是喚醒Looper,具體實現這裏先放一放,後面會做詳細分析。

消息的監聽

先來複習一下Looper的知識,我們知道Handler需要綁定一個Looper,主線程創建的Handler默認綁定的就是主線程的Looper,當然在子線程也可以用Looper

// 子線程中使用Looper
Looper.prepare();
Looper.loop();

prepare方法就是新建一個Looper,並設置給sThreadLocal對象。 我們重點看loop方法的實現

    public static void loop() {
        // ...省略檢查代碼...
        for (;;) {
            // 從MessageQueue中獲取下一條消息
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // ...省略代碼...
            try {
                // 消息分發
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            // ...省略代碼...
        }
    }

我們看到先是一個死循環,然後調用了queue.next()這個方法,從MessageQueue獲取下一條消息,而且可能會阻塞,我們繼續跟進next方法的實現

    // MessageQueue.java
    Message next() {
        // 獲取ptr,這是NativeMessageQueue的指針
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            // pollOnce,會阻塞
            nativePollOnce(ptr, nextPollTimeoutMillis);
            
            // 從消息隊列中拿消息,注意這裏加鎖了
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                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;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
            // ...省略了一坨代碼...    
            
            // 執行idle Handler
            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            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);
                    }
                }
            }
        }
    }

方法比較長,不重要的代碼我已經省略了,我們先列下重點流程,然後逐個分析。

  • 獲取mPtr,long類型,看名字它應該是一個指針,它其實是Native層的MessageQueue對象指針
  • 調用nativePollOnce,參數是mPtr和Poll超時時間,這個方法會阻塞
  • 加鎖,然後從消息隊列中取出一個Message,這裏有一坨邏輯,我們後面會細說
  • 如果消息隊列已經爲空了,會執行idle handlers,

mPtr的初始化

mPtr的初始化是在MessageQueue的構造方法中初始化的

    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
    }

nativeInit是一個Jni方法,繼續跟進

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

可以看到new了一個NativeMessageQueue對象(它繼承與RefBase,是一個智能指針對象),然後將它的引用計數+1,這樣就不會自動回收,最後將它的對象指針地址返回。所以我們就清楚了,應用層MessageQueue中的mPtr就是Native層NativeMessageQueue的指針。

nativePollOnce

nativePollOnce也是一個Jni方法,我們看下實現

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

先將參數的mPtr轉化爲NativeMessageQueue指針,然後調用了pollOnce方法,繼續跟進

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

    if (mExceptionObj) {
        env->Throw(mExceptionObj);
        env->DeleteLocalRef(mExceptionObj);
        mExceptionObj = NULL;
    }
}

調用了mLooper的pollOnce,這個mLooper是native層的Looper對象,我們繼續跟進

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
        // 省略了一坨代碼
        result = pollInner(timeoutMillis);
    }
}

調用了自身的pollInnner方法

int Looper::pollInner(int timeoutMillis) {
    // 省略了一坨代碼
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
  // 省略了一坨代碼
}

這裏只貼出關鍵代碼,我們看到調用了epoll_wait方法,這個方法可能會阻塞,正因爲這個方法,所以我們前面介紹的Looper中的死循環不會導致主線程一直在空轉(面試常見考點)。這個方法返回會有幾種情況。

  1. 發生錯誤
  2. 超過了我們傳入的timeoutMills時間
  3. 等待的mEpollFd被喚醒,具體的喚醒過程後面會介紹

當nativePollOnce方法返回後,進入下一步

加鎖,取下一條消息

我們重新回到MessageQueue.java中,這裏再貼一次代碼,我直接在代碼中添加註釋了

        final long now = SystemClock.uptimeMillis();
        Message prevMsg = null;
        // 獲取消息隊列的頭
        Message msg = mMessages;
        // 如果msg.target==null,說明這是一個消息屏障,如果是消息屏障,會遍歷整個消息隊列,
        // 去搜索是否有異步消息,如果有異步消息,會將第一個異步消息取出
        if (msg != null && msg.target == null) {
            // Stalled by a barrier.  Find the next asynchronous message in the queue.
            do {
                prevMsg = msg;
                msg = msg.next;
            } while (msg != null && !msg.isAsynchronous());
        }
        if (msg != null) {
            // 如果消息還沒到時間,Looper繼續休眠
            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;
                if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                msg.markInUse();
                // 從消息隊列移除消息,並返回
                return msg;
            }
        } else {
            // No more messages.
            // 如果消息隊列是空,繼續休眠,而且休眠時間是永久,直到被喚醒
            nextPollTimeoutMillis = -1;
        }

這裏涉及到二個概念,需要解釋一下

  • 消息屏障,它也是一個消息,只不過它的target是null,我們可以通過MessageQueue的postSyncBarrier方法來發送一個消息屏障插入到消息隊列,排在它後面的普通消息會一直等待,直到該消息屏障被移除纔會有機會被處理,當然需要清除就調用removeSyncBarrier。
  • 異步消息,異步消息通過Message的setAsynchronous方法來設置,異步消息不受消息屏障的影響,所以如果希望消息能立刻被執行,可以發送異步消息,並將其插入到消息隊列的頭部。
    Message message = Message.obtain();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
        message.setAsynchronous(true);
    }
    mMyHandler.sendMessageAtFrontOfQueue(message);

執行idle handlers

當消息隊列中沒有消息或者消息隊列現在被消息屏障阻塞着的時候,會執行idle handlers,所以它的意義在於,當消息隊列中沒有任務在執行時,給我們一個回調,讓我們有機會去執行一些非緊急任務,而不用擔心搶佔主線程緊急任務的CPU執行時間片,因爲此刻消息隊列是空閒狀態,Idle Handler的一個典型應用就是:我們經常在App啓動的時候會用到Idle Handler來執行一些非緊急的初始化任務。可以通過下面的代碼向MessageQueue中添加IdleHandler

    mMessageQueue.addIdleHandler(new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            return true;
        }
    });

這裏queueIdle的返回值是有意義的,如果返回false,IdleHandler被回調後就會被移除,也就是說只會回調一次,如果返回true,會多次調用。當然IdleHandler也是執行在主線程,如果做過重的任務,仍然有可能會阻塞消息隊列,導致後面的消息處理不及時。

技術交流qun:185873940

Looper喚醒-從消息隊列中取消息

前面已經說過了,Looper調用MessageQueue的next方法會阻塞,它的底層實現是epoll_wait,那麼在阻塞的時候,如果有新消息過來,肯定是要喚醒Looper來處理消息。喚醒方法我們 前面提到過,我們再回到MessageQueue的enquque方法

    if (needWake) {
        nativeWake(mPtr);
    }

它也是一個native方法,調用是NativeMessageQueue的wake方法,它最終調用的是native層Looper的wake方法。

static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->wake();
}

void NativeMessageQueue::wake() {
    mLooper->wake();
}

void Looper::wake() {
    // 省略判斷代碼
    uint64_t inc = 1;
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
    // 省略判斷代碼
}

在Looper的wake方法中,調用了write方法,它的第一個參數是mWakeEventFd,其實就是往mWakeEventFd這個描述符對應的文件中寫入了一個1,很顯然就是這個寫入將把Looper從epoll_wait的等待 中喚醒,那mWakeEventFd跟我們的epoll_wait有什麼關係呢?

我們查看下Looper的構造函數

Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
        mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
        mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
    mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    LOG_ALWAYS_FATAL_IF(mWakeEventFd < 0, "Could not make wake event fd: %s",
                        strerror(errno));
    AutoMutex _l(mLock);
    rebuildEpollLocked();
}

可以看到在構造器內,mWakeEventFd被初始化,然後調用了rebuildEpollLocked()方法,看名字應該跟構建Epoll有關,跟進去看看

void Looper::rebuildEpollLocked() {
    // 省略一坨代碼
    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);
    // 省略一坨代碼
}

先創建了mEpollFd,接着調用epoll_ctl方法,這個方法就是將mEpollFd和mWakeEventFd關聯起來,當向mWakeEventFd中寫入數據時,mEpollFd將會被喚醒,epoll_wait方法就會返回。到這裏其實就很清楚了,我們上面的wake方法,就是向mWakeEventFd中寫入了一個1,來喚醒mEpollFd。

消息的分發

消息的分發在Lopper.java類中,我們看下實現

    try {
        msg.target.dispatchMessage(msg);
        dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
    } finally {
        if (traceTag != 0) {
            Trace.traceEnd(traceTag);
        }
    }

最終調用了msg的target字段來分發,我們前面說過這個target就是Handler對象

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

這段邏輯很簡單,這裏簡單提一下,先判斷msg是否有callback,如果沒有再判斷Handler是否有Callback,如果沒有,才最終回調Handler的handleMessage方法來處理。

總結

到這裏,Android中Handler的整個原理就講完了,我們從Message的整個生命週期入手,從消息被創建,到加入消息隊列,到被消費處理然後被回收,我們都深入到了native層,瞭解了底層的技術原理。

總結一下

本文主要分析了以下幾點

  1. 消息是如何創建然後插入到消息隊列的
  2. Looper在死循環中等待消息用到了 epoll_wait機制,從而不會卡死主線程
  3. Looper的喚醒機制,通過向mWakeEventFd中寫入數據來喚醒Looper
  4. 消息的分發機制,而且有介紹消息屏障和異步消息
  5. idle Handler 的回調時機分析

瞭解這些原理,可以指導我們寫出更優的應用代碼,希望本文對你有幫助!

後我想貼一段代碼,來演示epoll的使用,算是彩蛋吧!

爲什麼會有這個想法呢?因爲我相信大部分同學包括我自己,對Linux都算不上熟悉,看源碼內的很多系統調用很容易一臉懵逼,這樣理解源碼起來就很困難。所以我就一直想能有個機會真實操作一把Linux編程,剛好這次分析的Handler底層利用的epoll不是很難,我就擼了一個demo來感受一下Linux編程,直接上代碼(環境是ubuntu 14, 編譯器是clion)。

需求非常簡單:主進程fork一個子進程出來,然後主進程epoll_wait等待,子進程先sleep 2s,然後喚醒主進程,程序結束

#include <sys/eventfd.h>
#include <sys/epoll.h>

static const int EPOLL_SIZE_HINT = 8;
int main(void) {
    // 創建時間描述符
    int mEventFd = eventfd(0, 0);
    if (mEventFd == -1) {
        printf("create efd error!");
        exit(0);
    }
    // 創建epoll描述符
    int mEpollFd = epoll_create(EPOLL_SIZE_HINT);
    struct epoll_event eventItem;
    eventItem.events = EPOLLIN;
    eventItem.data.fd = mEventFd;
    // 綁定事件描述符到epoll
    epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mEventFd, &eventItem);
    
    // 創建一個子進程,注意fork出來的進程繼承主進程的內存佈局,而vfork不會
    _pid_t pid = fork();
    // 注意fork方法會返回二次,==0 是在子進程,>0在父進程
    if (pid == 0) {
        // 子進程
        printf("child process sleep 2s\n");
        // 先sleep 2s
        sleep(2);
        uint64_t event = 1;
        // 寫入1到mEventFd, 喚醒父進程的epoll
        write(mEventFd, &event, sizeof(uint64_t);
        printf(child process write event finish!\n);
    } else {
        // 父進程
        struct epoll_event eventItems[EPOLL_SIZE_HINT * 100];
        printf("parent process start wating!\n");
        // 父進程epoll阻塞等待,最後一個參數是-1,表示永遠等待
        int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_SIZE_HINT*100, -1);
        // 被喚醒 epoll_wait返回,
        printf("get event : %d\n", eventCount);
    }
    exit(0);
}

控制檯輸出結果如下:

 

代碼不難,Linux環境可以直接跑的,大家可以自己動手敲一遍,加深理解! 以上!!

文末

好啦,如此文章到這裏就結束了,希望這篇文章能夠幫到正在看的你們解決小夥伴們的問題~

如果你覺得文章寫得不錯就給個唄?如果你覺得文章非常不錯的話那就轉發一個唄,讓更多小夥伴看到;如果你覺得那裏值得改進的,請給我留言。一定會認真查詢,修正不足,謝謝~

再推薦一篇文章:“寒冬未過”,阿里P9架構分享Android必備技術點,讓你offer拿到手軟!

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