Android訊息機制,從Java層到Native層剖析

Cheelok發表於2019-02-22

由Handler、MessageQueue、Looper構成的執行緒訊息通訊機制在Android開發中非常常用,不過大部分人都只粗淺地看了Java層的實現,對其中的細節不甚了了,這篇博文將研究Android訊息機制從Java層到Native層的實現。

訊息機制由於更貼近抽象設計,所以整個結構更簡單,只包含了訊息的產生、分發,不像Input子系統那樣還有歸類、過濾等環節。整體的結構如下圖:

Android Java層訊息機制

訊息的產生

在Java層中訊息的產生都來源於使用者建立的Message物件,經過封裝的Runnable物件,或呼叫obtainMessage從Message Pool中獲得,Message Pool指的是Message類內的Message迴圈佇列,隊頭是靜態的Message物件sPool,該佇列最大容納MAX_POOL_SIZE(50)個Message:

MessagePool對Message的複用節省了不斷建立Message帶來的開銷,如果當前50個Message都已經被用過,由於MessagePool是迴圈佇列,則會回到隊頭並請空該Message,向下複用。

BlockingRunnable

看Java層Handler的原始碼的時候發現了一個奇怪的東西:BlockingRunnable,基本上沒有用過的東西,也沒看別人講過,於是我就來鑽研一下吧:

private static final class BlockingRunnable implements Runnable {
    private final Runnable mTask;
    private boolean mDone;

    public BlockingRunnable(Runnable task) {
        mTask = task;
    }

    @Override
    public void run() {
        try {
            mTask.run();
        } finally {
            synchronized (this) {
                mDone = true;
                notifyAll();
            }
        }
    }

    public boolean postAndWait(Handler handler, long timeout) {
        if (!handler.post(this)) {
            return false;
        }

        synchronized (this) {
            if (timeout > 0) {
                final long expirationTime = SystemClock.uptimeMillis() + timeout;
                while (!mDone) {
                    long delay = expirationTime - SystemClock.uptimeMillis();
                    if (delay <= 0) {
                        return false; // timeout
                    }
                    try {
                        wait(delay);
                    } catch (InterruptedException ex) {
                    }
                }
            } else {
                while (!mDone) {
                    try {
                        wait();
                    } catch (InterruptedException ex) {
                    }
                }
            }
        }
        return true;
    }
}複製程式碼

我們可以看到,BlockingRunnable是一個“包裹”構造方法中傳入的Runnable的Runnable,呼叫BlockingRunnable的postAndWait會做以下事情:

  1. 如果投遞BlockingRunnable失敗,返回false
  2. 鎖住投遞BlockingRunnable的執行緒
  3. 如果timeout大於0,計算引數Runnable的到期時間,只要引數Runnable還沒處理完,則一直輪詢還剩多少時間,並呼叫wait(delay)讓投遞BlockingRunnable的執行緒繼續等待,直引數Runnable處理完(mDone為true)這個過程才結束
  4. 如果timeout小於等於0,而且引數Runnable還沒處理完,則一直等待直到引數Runnable處理完(mDone為true)

這個東西的說明書和使用風險可以在runWithScissors方法的註釋裡看到,我在這裡就不當翻譯工了。

訊息的投遞和處理

得到Message後,就會通過Handler的sendMessageAtTime呼叫MessageQueue的enqueueMessage將Message投遞到MessageQueue中,在往下學習之前必須先了解Handler的建立,因為後面的知識和它有關聯。

Handler的建立和初始化

其實Handler的初始化沒什麼好看的,就是儲存Callback、mLooper的MessageQueue的引用,以及宣告Handler是否非同步投遞所有Message。但是裡面有一個記憶體洩露的檢查,可以學習一下,就是如果開啟了FIND_POTENTIAL_LEAKS,就會進行記憶體洩露的檢查,它會做以下事情:

  1. 獲取當前Handler類
  2. 如果Handler是匿名內部類,或成員類,或區域性類,且Handler的修飾符不是static
  3. 那麼就會打出log提示可能會發生記憶體洩露
public Handler(Callback callback, boolean async) {
    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());
        }
    }

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

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}複製程式碼

既然Handler的建立這麼簡單,為什麼說後面要學習的內容和它相關呢?原因就出在Looper中,我們可以看到Looper是通過sThreadLocal返回的,這個ThreadLocal是什麼呢?

ThreadLocal – 維持執行緒內物件的唯一性

ThreadLocal是一個關於建立執行緒區域性變數的類。

通常情況下,我們建立的變數是可以被任何一個執行緒訪問並修改的。而使用ThreadLocal建立的變數只能被當前執行緒訪問,其他執行緒則無法訪問和修改。它的實現原理如下:

如圖所示,ThreadLocalRef其實是同一個ThreadLocal物件的引用,為了不讓線看起來很亂我分別用了兩個方塊表示ThreadLocal物件,但其實是同一個物件。ThreadLocal同時是ThreadA、ThreadB甚至ThreadN內ThreadLocalMap的Key,但取出來的物件時不一樣的,因為Map不一樣對應的鍵值對也不一樣嘛。

ThreadLocalMap

ThreadLocalMap是僅用於維護ThreadLocal值的自定義HashMap,只在Thread類內使用。為了避免ThreadLocalMap的Key->ThreadLocal在GC時無法被回收,裡邊的元素都是用WeakReference封裝的。ThreadLocalMap除了這點以外,沒有什麼特別的,就不細講了。

需要注意的一點是:ThreadLocalMap是可能帶來記憶體洩露的,但root cause不是ThreadLocalMap本身,而是程式碼質量不夠高。首先,由於作為Map的Key的ThreadLocal是弱引用,那麼GC時ThreadLocal會被回收,此時Map記憶體在一對Key為null的鍵值對,而Value仍然被執行緒強引用著,那麼如果用完ThreadLocal後不主動移除,就會記憶體洩露了。但事實上,ThreadLocal用完後主動調remove就能規避這個問題,本來也該這樣做。

Entry

Entry作為ThreadLocalMap的元素,表示的是一對鍵值對:ThreadLocal的弱引用為鍵,將要用ThreadLocal儲存的物件為值。

static class Entry extends WeakReference<ThreadLocal> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}複製程式碼

ThreadLocal總結

換句話說,所謂的不可被其他執行緒修改的區域性變數,表示的是:每個執行緒中都會維護一個ThreadLocalMap,裡邊以ThreadLocal為鍵,對應的區域性變數為值,通過鍵值對來控制訪問和資料的一致性,而不是通過鎖來控制。

Looper

既然一個執行緒只有一個Looper,那麼Looper裡面有什麼呢?從原始碼可以看到,Looper的構造方法是私有的,也就意味著獲得Looper物件基本都是單例,這一點和執行緒<->Looper的一對一對映關係切合。

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

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的成員變數我們可以知道Looper包含了以下東西:

  • sMainLooper:應用主執行緒的Looper,建立其他執行緒的Looper時為null
  • mQueue:Looper關聯的MessageQueue
  • mThread:Looper關聯的執行緒
  • sThreadLocal:執行緒區域性變數的Key

從這可以知道,一個執行緒對應一個Looper,一個Looper對應一個MessageQueue

—————-分割線,接下來回到訊息的投遞結束的地方—————-

得到Message後,就會通過Handler的sendMessageAtTime呼叫MessageQueue的enqueueMessage將Message投遞到MessageQueue中,在往下學習之前必須先了解Handler的建立,因為後面的知識和它有關聯。

現在我們知道Message將要投遞到哪裡的MessageQueue裡了,那麼投遞過去之後,訊息是怎麼被處理的呢?這程式碼很長,而且就是個進入佇列的過程,我就不貼了,做了以下事情:

  1. 合法性檢查
  2. 標記Message正在使用
  3. 入列
  4. 喚醒native的MessageQueue

在這裡有個有意思的概念必須提一下,就是Barrier Message,它表示的是一種柵欄的概念,將它加入MessageQueue可以攔住所有執行時間在它之後的同步Message,非同步Message則不受影響,遍歷到就會處理,這種狀況會持續到把Barrier Message移除。

提示:圖裡綠色代表Message可以被取出執行,紅色表示無法被取出執行

它和Message的根本差別是,他沒有target,即:沒有處理該Message的Handler,但我們自己將Message的Handler設為null是沒法加入MessageQueue的,必須呼叫postSyncBarrier方法:

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    ……
}

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++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (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 {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}複製程式碼

訊息的分發

前面已經知道Message投遞後就會到達MessageQueue,接下來就看訊息是怎麼被遍歷處理的。首先要知道的一點是,Looper在呼叫prepare建立後,是必須調loop()方法的,很多人會問,我平常用的時候沒用loop()方法也沒問題啊。那是因為你是在主執行緒用的,主執行緒在建立Looper的時候已經呼叫過loop()方法了。

我們建立了其他執行緒的Looper後,調loop()方法會做以下事情:

  1. 迴圈獲取MessageQueue中的Message
  2. 將Message通過Handler的dispatchMessage方法分發到對應的Handler中
  3. 將Message的資訊清空,回收到Message Pool中等待下一次使用
public static void loop() {
    ……

    for (;;) {
        Message msg = queue.next(); // might block

        ……

        try {
            msg.target.dispatchMessage(msg);
        } finally {
            ……
        }

        ……

        msg.recycleUnchecked();
    }
}複製程式碼

在Handler的dispatchMessage中,對Message的處理其實是有優先順序這個說法的:

  1. 如果Message設定了callback,則將Message交給Message的callback處理
  2. 如果Handler設定了callback,則將Message先交給Handler的callback處理
  3. 否則的話,將Message交給Handler的handleMessage處理
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}複製程式碼

對於MessageQueue,它實際表示了Java層和Native層的MessageQueue,Java層的MessageQueue就是mMessages表示的迴圈佇列,Native層的MessageQueue就是mPtr。它的next()方法裡做的事情如下:

  1. 呼叫nativePollOnce讓native層的MessageQueue先處理Native層的Message,再處理Java層的Message,這個過程可能阻塞
  2. 如果在按時序遍歷MessageQueue的過程中發現了Barrier Message,即handler為空的Message,則跳過它後面的所有同步Message,只處理非同步Message
  3. 如果訊息是延時訊息,計算當前時間和目標時間的差值,休眠這個時間差後再去取這個Message
  4. 如果訊息不是延時訊息,在Message Pool裡標記該Message正在使用,並返回它

Java層Android訊息機制的整個過程可以用下圖概括:

有鑽研過Java層程式碼的朋友肯定知道,Handler裡面還有個用於跨程式Message通訊的MessengerImpl,這個東西我就不在這裡說了,因為它就是個簡單的跨程式通訊,和整個Handler、Looper、MessageQueue其實關係不大。

Android Native層訊息機制

Android訊息機制在Native層其實和Java層很相似,保留了Handler、Looper、MessageQueue的結構。但是Native層Message、Handler、MessageQueue的概念被弱化得很厲害,基本上只是個“空殼”,核心邏輯都在Looper裡邊了。

其他區別都不大了,只是在實現上有一點不一樣,具體的差別就在原始碼中找答案吧。整體結構圖如下:

訊息的產生

在Native層中,訊息由MessageEnvelope和封裝fd(Java層Handler可以新增fd的監聽、Native當然也可以)相關資訊後得到的epoll_event組成。

fd

對於要被監聽的fd的訊息,Looper做了以下事情:

  1. 合法性檢查
  2. 將相關資訊封裝到Request中,並初始化為epoll_event
  3. 將該fd以及要監聽的epoll_event事件(步驟2轉換Request得到)註冊到當前Looper的epollFd中
  4. 如果出錯,進行出錯處理
  5. 更新mRequests
int Looper::addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data) {
    ……

    { // acquire lock
        AutoMutex _l(mLock);

        ……

        struct epoll_event eventItem;
        request.initEventItem(&eventItem);

        ssize_t requestIndex = mRequests.indexOfKey(fd);
        if (requestIndex < 0) {
            int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, fd, & eventItem);
            ……
            mRequests.add(fd, request);
        } else {
            int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_MOD, fd, & eventItem);
            ……
            mRequests.replaceValueAt(requestIndex, request);
        }
    } // release lock
    return 1;
}複製程式碼

MessageEnvelope

MessageEnvelope相對於fd就簡單多了,在呼叫Native層Looper的sendMessage相關函式時會將uptime、MessageHandler、Native層Message封裝到MessageEnvelope中,然後插入mMessageEnvelopes中。

void Looper::sendMessageAtTime(nsecs_t uptime, const sp<MessageHandler>& handler, const Message& message) {
    ……
    size_t i = 0;
    { // acquire lock
        AutoMutex _l(mLock);

        size_t messageCount = mMessageEnvelopes.size();
        while (i < messageCount && uptime >= mMessageEnvelopes.itemAt(i).uptime) {
            i += 1;
        }

        MessageEnvelope messageEnvelope(uptime, handler, message);
        mMessageEnvelopes.insertAt(messageEnvelope, i, 1);

        ……
    }
    ……
}複製程式碼

訊息的投遞和處理

前面已經提到了,Java層的MessageQueue處理訊息時,會先呼叫Native層MessageQueue的nativePollOnce(),它實際呼叫的是native層MessageQueue的pollOnce(),而native的pollOnce呼叫的是Native層的Looper的pollOnce:

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

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    ……
    mLooper->pollOnce(timeoutMillis);
    ……
}複製程式碼

在看Native層Looper的pollOnce方法之前,先看看Native層的Looper和Java層的Looper會不會有一些不一樣吧。

Looper Native

和Java層Looper的使用一樣,Native層Looper也需要prepare,也是一個通過執行緒區域性變數儲存的物件,一個執行緒只有一個。那麼在Native層是怎麼實現執行緒區域性變數的呢?

Linux TSD(Thread-specific Data)池

Native層執行緒區域性變數的思想和Java層很類似,Native層會維護一個全域性的pthread_keys陣列,用於存放執行緒區域性變數的鍵。其中seq用於標記是否”in_use”,destr則是一個函式指標,可用作解構函式,線上程退出時釋放該鍵對應於執行緒中的執行緒區域性變數。

static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] ={{0,NULL}};

int pthread_key_create(pthread_key_t *key, void (*destr_function) (void*));

struct pthread_key_struct
{
  /* Sequence numbers.  Even numbers indicated vacant entries.  Note
     that zero is even.  We use uintptr_t to not require padding on
     32- and 64-bit machines.  On 64-bit machines it helps to avoid
     wrapping, too.  */
  uintptr_t seq;

  /* Destructor for the data.  */
  void (*destr) (void *);
};複製程式碼

pthread在建立執行緒時會維護一個指標陣列,陣列元素指向執行緒區域性變數的資料塊。整體解構如下圖:

建立Looper

建立Looper時,會做以下事情:

  1. 通過eventfd建立mWakeEventFd用於執行緒間通訊去喚醒Looper的,當需要喚醒Looper時,就往裡面寫1
  2. 建立用於監聽epoll_event的mEpollFd,並初始化mEpollFd要監聽的epoll_event型別
  3. 通過epoll_ctl將mWakeEventFd註冊到mEpollFd中,當mWakeEventFd有事件可讀則喚醒Looper
  4. 如果mRequests不為空的話,說明前面註冊了有要監聽的fd,則遍歷mRequests中的Request,將它初始化為epoll_event並通過epoll_ctl註冊到mEpollFd中,當有可讀事件同樣喚醒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);
    ……
    rebuildEpollLocked();
}

void Looper::rebuildEpollLocked() {
    ……

    // Allocate the new epoll instance and register the wake pipe.
    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);
        ……
    }
}複製程式碼

pollOnce

對於Native層Looper的pollOnce,找它函式定義稍微有點隱祕,它在Looper.h中宣告,inline到pollOnce(int timeoutMillis, int outFd, int outEvents, void** outData)函式裡了,它做了以下事情:

  1. 優先處理mResponses裡的Response,即來自fd的事件
  2. 如果沒有需處理的Response,再呼叫pollInner
inline int pollOnce(int timeoutMillis) {
    return pollOnce(timeoutMillis, NULL, NULL, NULL);
}

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        while (mResponseIndex < mResponses.size()) {
            const Response& response = mResponses.itemAt(mResponseIndex++);
            int ident = response.request.ident;
            if (ident >= 0) {
                ……
                return ident;
            }
        }

        if (result != 0) {
#if DEBUG_POLL_AND_WAKE
            ALOGD("%p ~ pollOnce - returning result %d", this, result);
#endif
            ……
            return result;
        }

        result = pollInner(timeoutMillis);
    }
}複製程式碼

pollInner這個函式比較長,它做了以下事情:

  1. 基於下一個Message調整獲取Message的時間間隔timeoutMillis
  2. 清空mResponses
  3. 獲取epoll事件,即將要處理的Message
  4. 更新mPolling,防止進入idle
  5. 執行合法性檢查
  6. 如果epoll_event的fd為mWakeFd,說明是Looper的喚醒事件,則喚醒Looper
  7. 否則先將epoll_event封裝為Request,更新epoll_event的事件型別,再封裝為Response裝入mResponses
  8. 迴圈取出mMessageEnvelopes隊頭的MessageEnvelope,並將MessageEnvelope中的Message交給對應的Native層的Handler處理
  9. 迴圈呼叫mResponses中所有Response的callback

至此對Android訊息機制的學習就結束啦。

題外話

如果你覺得我的分享有幫助到你的話,請我吃個零食/喝杯咖啡唄~

相關文章