Android Handler 通信 - 徹底瞭解 Handler 的通信過程

相關文章鏈接:

1. Android Handler 通信 - 源碼分析與手寫 Handler 框架
2. Android Handler 通信 - 徹底瞭解 Handler 的通信過程

相關源碼文件:

/frameworks/base/core/java/android/os/Handler.java
/frameworks/base/core/java/android/os/MessageQueue.java
/frameworks/base/core/java/android/os/Looper.java

framework/base/core/jni/android_os_MessageQueue.cpp
system/core/libutils/Looper.cpp
system/core/include/utils/Looper.h

在 Android 應用開發過程中,跨進程通信一般是 binder 驅動 ,關於 binder 驅動的源碼分析,大家感興趣可以看看我之前的一些文章。這裏我們來聊聊線程間的通信 Handler,關於 handler 的通信原理我想大家應該都是倒背如流。但有一些比較細節的東西大家可能就未必瞭解了:

  • 基於 Handler 可以做性能檢測
  • 基於 Handler 可以做性能優化
  • 基於 Handler 竟然也可以做跨進程通信?

關於 Handler 的基礎原理篇,大家有不瞭解的可以看看 《Android Handler 通信 - 源碼分析與手寫 Handler 框架》 ,本文這裏我們先來分析一下 Handler 是怎麼處理消息延遲的,這是我在頭條面試碰到的一個題目。首先我們先自己思考一下:如果我們要延遲 2s 再去處理這個消息,自己實現會怎樣去處理?(思考五分鐘)我們一起來看看源碼:

// MessageQueue.java 中的 next 方法源碼
Message next() {
        // 判斷 native 層的 MessageQueue 對象有沒有正常創建
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
        // 消息執行需要等待的時間
        int nextPollTimeoutMillis = 0;
        for (;;) {
            // 執行 native 層的消息延遲等待,調 next 方法第一次不會進來
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                // 獲取當前系統的時間
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                ...
                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;
                        // 標記爲已在使用狀態
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // 如果隊列裏面沒有消息,等待時間是 -1
                    nextPollTimeoutMillis = -1;
                }
                // 有沒有空閒的 IdleHandler 需要執行,一般我們沒關注這個功能
                // 後面內容有專門解釋,這裏目前分析是 == 0 ,跳出
                if (pendingIdleHandlerCount <= 0) {
                    mBlocked = true;
                    continue;
                }
                ...
            }
            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0;
        }
    }

通過源碼分析我們發現消息的處理過程,是通過當前消息的執行時間與當前系統時間做比較,如果小於等於當前系統時間則立即返回執行該消息,如果大於當前系統時間則調用 nativePollOnce 方法去延遲等待被喚醒,當消息隊列裏面爲空時則設置等待的時間爲 -1。關於 IdleHandler 、異步消息和消息屏障的源碼已被我忽略,大家待會可以看文章後面的分析。我們跟進到 Native 層的 android_os_MessageQueue_nativePollOnce 方法。

static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jlong ptr, jint timeoutMillis) {
    // 通地址轉換成 native 層的 MessageQueue 對象
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    // 調用 native 層 Looper 對象的 pollOnce 方法
    mLooper->pollOnce(timeoutMillis);
}

inline int pollOnce(int timeoutMillis) {
    return pollOnce(timeoutMillis, NULL, NULL, NULL); 【5】
}

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        ...
        if (result != 0) {
            ...
            return result;
        }
        // 再處理內部輪詢
        result = pollInner(timeoutMillis); 【6】
    }
}

int Looper::pollInner(int timeoutMillis) {
    ...
    int result = POLL_WAKE;
    mResponses.clear();
    mResponseIndex = 0;
    mPolling = true; //即將處於idle狀態
    // fd最大個數爲16
    struct epoll_event eventItems[EPOLL_MAX_EVENTS]; 
    // 等待事件發生或者超時,在 nativeWake() 方法,向管道寫端寫入字符,則該方法會返回;
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    ...
    return result;
}

通過源碼分析我們能發現 Looper.loop() 方法並不是一個死循環那麼簡單,如果真是一個簡單的死循環那得多耗性能,其實 native 層是調用 epoll_wait 進入等待的,timeoutMillis 這裏有三種情況分別爲:0 ,-1 和 >0 。如果是 -1 那麼該方法會一直進入等待,如果是 0 那麼該方法會立即返回,如果是 >0 該方法到等待時間就會立即返回,關於 epoll_wait 的使用和原理介紹,大家可以看看之前的內容。接下來我們看看喚醒方法:

boolean enqueueMessage(Message msg, long when) {
  ...
  synchronized (this) {
    if (mQuitting) {
      ...
      // We can assume mPtr != 0 because mQuitting is false.
      // 如果需要喚醒,調用 nativeWake 方法喚醒
      if (needWake) {
        nativeWake(mPtr);
      }
    }
    return true;
}

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

void Looper::wake() {
    uint64_t inc = 1;
    // 向管道 mWakeEventFd 寫入字符1 , 寫入失敗仍然不斷執行
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
    if (nWrite != sizeof(uint64_t)) {
        if (errno != EAGAIN) {
            ALOGW("Could not write wake signal, errno=%d", errno);
        }
    }
}

我們來總結一下延遲消息的處理過程:首先我們在 native 層其實也有 Handler.cpp 、MessageQueue.cpp 和 Looper.cpp 對象,但他們並不是與 Java 層一一對應的,只有 MessageQueue.java 和 MessageQueue.cpp 有關聯。當我們上層計算好延遲時間後調用 native 層 nativePollOnce 方法,其內部實現採用 epoll 來處理延遲等待返回(6.0版本)。

當有新的消息插入時會調用 native 層的 nativeWake 方法,這個方法很簡單就是像文件描述符中寫入一個最簡單的 int 數據 -1,目的是爲了喚醒之前的 epoll_wait 方法,其實也就是喚醒 nativePollOnce 的等待。關於 Handler.cpp 、MessageQueue.cpp 和 Looper.cpp 對象,如果大家感興趣可以看看之前的文章。

由此可見大公司的一個簡單面試題就能過濾到很多人,我以前也經常聽到同學抱怨,你爲什麼要問我這些問題,開發中又用不上。那麼接下帶大家來看看開發中能用上的,但我們可能並不熟悉的一些源碼細節。

  • IdleHandler

我們在實際的開發過程中爲了不阻塞主線程的其它任務執行,可能要等主線程空閒下來再去執行某個特定的方法,比如我們寫了一個性能監測的工具,觸發了某些條件要不定時的去收集某些信息,這個肯定是不能去影響主線程的,否則我們發現加了某些性能監測工具,反而會引起整個應用性能更差甚至引起卡頓。那如何才能知道主線程是否空閒了?

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
  @Override
  public boolean queueIdle() {
    // 開始執行一些其它操作,可能不耗時也可能稍微耗時
    // 比如跨進程訪問,比如查詢數據庫,比如收集某些信息,比如寫日誌等等
    return false;
  }
});

這個代碼很簡單,只是我們平時可能比較少接觸,我們來看看源碼:

public void addIdleHandler(@NonNull IdleHandler handler) {
  if (handler == null) {
    throw new NullPointerException("Can't add a null IdleHandler");
  }
  synchronized (this) {
    mIdleHandlers.add(handler);
  }
}

Message next() {
        int pendingIdleHandlerCount = -1; // -1 only during first iteration

        for (;;) {
            synchronized (this) {
                //  目前這裏分析有內容
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // 循環遍歷所有的 IdleHandler 回調
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
                // 回調執行 queueIdle 方法
                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;
            nextPollTimeoutMillis = 0;
        }
    }
  • 主線程方法耗時

在實際的應用開發過程中,我們可能會發現界面會有卡頓的問題,尤其是在一些複雜的場景下,比如直播間各種禮物動畫效果或者複雜列表滑動等等,當然引起卡頓的原因會有很多,比如機型的不同,在我們的手機上絲絲般順滑,在測試的手機上卻像沒喫飯一樣,當然還與 cpu 使用率、主線程執行耗時操作和磁盤 I/O 操作等等都有關。實際過程中要排查卡頓其實是比較複雜的,這裏我們主要來監聽排查是不是主線程有方法耗時:

    @Override
    public void onCreate() {
        super.onCreate();
        Looper.getMainLooper().setMessageLogging(new Printer() {
            long currentTime = -1;

            @Override
            public void println(String x) {
                if (x.contains(">>>>> Dispatching to ")) {
                    currentTime = System.currentTimeMillis();
                } else if (x.contains("<<<<< Finished to ")) {
                    long executeTime = System.currentTimeMillis() - currentTime;
                    // 原則上是 16 毫秒,一般沒辦法做到這麼嚴格
                    if (executeTime > 60) {
                        Log.e("TAG", "主線程出現方法耗時");
                    }
                }
            }
        });
    }

當然這麼做只能監測到主線程有方法耗時而引起卡頓,可能目前這些代碼無法跟蹤到是哪個方法耗時引起的卡頓,但基於這些代碼去實現是哪個方法引起的耗時應該是 soeasy ,這裏我們先不做過多的延伸主要來看下原理:

    /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        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;

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            // 在執行方法之前做打印
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            // 執行消息的分發
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            // 執行方法之後再做打印
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
            ...
        }
    }

原理其實是非常簡單的,關於異步消息、消息屏障、跨進程通信以及監聽具體的某個方法耗時,在後面的源碼分析中會陸陸續續的提到,大家感興趣可以持續關注。國慶快樂~

視頻地址:https://pan.baidu.com/s/1j_wgzITcgABVbThvO0VBPA
視頻密碼:jj4b

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