相關(guān)文章鏈接:
1. Android Framework - 學(xué)習(xí)啟動篇
2. Android Handler 通信 - 源碼分析與手寫 Handler 框架
3. Android Handler 通信 - 徹底了解 Handler 的通信過程
相關(guān)源碼文件:
/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 應(yīng)用開發(fā)過程中,跨進程通信一般是 binder 驅(qū)動 ,關(guān)于 binder 驅(qū)動的源碼分析,大家感興趣可以看看我之前的一些文章。這里我們來聊聊線程間的通信 Handler,關(guān)于 handler 的通信原理我想大家應(yīng)該都是倒背如流。但有一些比較細節(jié)的東西大家可能就未必了解了:
- 基于 Handler 可以做性能檢測
- 基于 Handler 可以做性能優(yōu)化
- 基于 Handler 竟然也可以做跨進程通信?
關(guān)于 Handler 的基礎(chǔ)原理篇,大家有不了解的可以看看 《Android Handler 通信 - 源碼分析與手寫 Handler 框架》 ,本文這里我們先來分析一下 Handler 是怎么處理消息延遲的,這是我在頭條面試碰到的一個題目。首先我們先自己思考一下:如果我們要延遲 2s 再去處理這個消息,自己實現(xiàn)會怎樣去處理?(思考五分鐘)我們一起來看看源碼:
// MessageQueue.java 中的 next 方法源碼
Message next() {
// 判斷 native 層的 MessageQueue 對象有沒有正常創(chuàng)建
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
// 消息執(zhí)行需要等待的時間
int nextPollTimeoutMillis = 0;
for (;;) {
// 執(zhí)行 native 層的消息延遲等待,調(diào) next 方法第一次不會進來
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// 獲取當(dāng)前系統(tǒng)的時間
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 {
// 不需要延遲獲取已經(jīng)過了時間,立馬返回
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
// 標(biāo)記為已在使用狀態(tài)
msg.markInUse();
return msg;
}
} else {
// 如果隊列里面沒有消息,等待時間是 -1
nextPollTimeoutMillis = -1;
}
// 有沒有空閑的 IdleHandler 需要執(zhí)行,一般我們沒關(guān)注這個功能
// 后面內(nèi)容有專門解釋,這里目前分析是 == 0 ,跳出
if (pendingIdleHandlerCount <= 0) {
mBlocked = true;
continue;
}
...
}
pendingIdleHandlerCount = 0;
nextPollTimeoutMillis = 0;
}
}
通過源碼分析我們發(fā)現(xiàn)消息的處理過程,是通過當(dāng)前消息的執(zhí)行時間與當(dāng)前系統(tǒng)時間做比較,如果小于等于當(dāng)前系統(tǒng)時間則立即返回執(zhí)行該消息,如果大于當(dāng)前系統(tǒng)時間則調(diào)用 nativePollOnce 方法去延遲等待被喚醒,當(dāng)消息隊列里面為空時則設(shè)置等待的時間為 -1。關(guān)于 IdleHandler 、異步消息和消息屏障的源碼已被我忽略,大家待會可以看文章后面的分析。我們跟進到 Native 層的 android_os_MessageQueue_nativePollOnce 方法。
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
jlong ptr, jint timeoutMillis) {
// 通地址轉(zhuǎn)換成 native 層的 MessageQueue 對象
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
// 調(diào)用 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;
}
// 再處理內(nèi)部輪詢
result = pollInner(timeoutMillis); 【6】
}
}
int Looper::pollInner(int timeoutMillis) {
...
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;
mPolling = true; //即將處于idle狀態(tài)
// fd最大個數(shù)為16
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
// 等待事件發(fā)生或者超時,在 nativeWake() 方法,向管道寫端寫入字符,則該方法會返回;
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
...
return result;
}
通過源碼分析我們能發(fā)現(xiàn) Looper.loop() 方法并不是一個死循環(huán)那么簡單,如果真是一個簡單的死循環(huán)那得多耗性能,其實 native 層是調(diào)用 epoll_wait 進入等待的,timeoutMillis 這里有三種情況分別為:0 ,-1 和 >0 。如果是 -1 那么該方法會一直進入等待,如果是 0 那么該方法會立即返回,如果是 >0 該方法到等待時間就會立即返回,關(guān)于 epoll_wait 的使用和原理介紹,大家可以看看之前的內(nèi)容。接下來我們看看喚醒方法:
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
if (mQuitting) {
...
// We can assume mPtr != 0 because mQuitting is false.
// 如果需要喚醒,調(diào)用 nativeWake 方法喚醒
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
void NativeMessageQueue::wake() {
mLooper->wake();
}
void Looper::wake() {
uint64_t inc = 1;
// 向管道 mWakeEventFd 寫入字符1 , 寫入失敗仍然不斷執(zhí)行
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);
}
}
}
我們來總結(jié)一下延遲消息的處理過程:首先我們在 native 層其實也有 Handler.cpp 、MessageQueue.cpp 和 Looper.cpp 對象,但他們并不是與 Java 層一一對應(yīng)的,只有 MessageQueue.java 和 MessageQueue.cpp 有關(guān)聯(lián)。當(dāng)我們上層計算好延遲時間后調(diào)用 native 層 nativePollOnce 方法,其內(nèi)部實現(xiàn)采用 epoll 來處理延遲等待返回(6.0版本)。
當(dāng)有新的消息插入時會調(diào)用 native 層的 nativeWake 方法,這個方法很簡單就是向文件描述符中寫入一個最簡單的 int 數(shù)據(jù) -1,目的是為了喚醒之前的 epoll_wait 方法,其實也就是喚醒 nativePollOnce 的等待。關(guān)于 Handler.cpp 、MessageQueue.cpp 和 Looper.cpp 對象,如果大家感興趣可以看看之前的文章。
由此可見大公司的一個簡單面試題就能過濾到很多人,我以前也經(jīng)常聽到同學(xué)抱怨,你為什么要問我這些問題,開發(fā)中又用不上。那么接下帶大家來看看開發(fā)中能用上的,但我們可能并不熟悉的一些源碼細節(jié)。
- IdleHandler
我們在實際的開發(fā)過程中為了不阻塞主線程的其它任務(wù)執(zhí)行,可能要等主線程空閑下來再去執(zhí)行某個特定的方法,比如我們寫了一個性能監(jiān)測的工具,觸發(fā)了某些條件要不定時的去收集某些信息,這個肯定是不能去影響主線程的,否則我們發(fā)現(xiàn)加了某些性能監(jiān)測工具,反而會引起整個應(yīng)用性能更差甚至引起卡頓。那如何才能知道主線程是否空閑了?
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 開始執(zhí)行一些其它操作,可能不耗時也可能稍微耗時
// 比如跨進程訪問,比如查詢數(shù)據(jù)庫,比如收集某些信息,比如寫日志等等
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) {
// 目前這里分析有內(nèi)容
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// 循環(huán)遍歷所有的 IdleHandler 回調(diào)
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
// 回調(diào)執(zhí)行 queueIdle 方法
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
// 執(zhí)行完函數(shù)返回是不是需要空閑時一直回調(diào)
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
pendingIdleHandlerCount = 0;
nextPollTimeoutMillis = 0;
}
}
- 主線程方法耗時
在實際的應(yīng)用開發(fā)過程中,我們可能會發(fā)現(xiàn)界面會有卡頓的問題,尤其是在一些復(fù)雜的場景下,比如直播間各種禮物動畫效果或者復(fù)雜列表滑動等等,當(dāng)然引起卡頓的原因會有很多,比如機型的不同,在我們的手機上絲絲般順滑,在測試的手機上卻像沒吃飯一樣,當(dāng)然還與 cpu 使用率、主線程執(zhí)行耗時操作和磁盤 I/O 操作等等都有關(guān)。實際過程中要排查卡頓其實是比較復(fù)雜的,這里我們主要來監(jiān)聽排查是不是主線程有方法耗時:
@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 毫秒,一般沒辦法做到這么嚴(yán)格
if (executeTime > 60) {
Log.e("TAG", "主線程出現(xiàn)方法耗時");
}
}
}
});
}
當(dāng)然這么做只能監(jiān)測到主線程有方法耗時而引起卡頓,可能目前這些代碼無法跟蹤到是哪個方法耗時引起的卡頓,但基于這些代碼去實現(xiàn)是哪個方法引起的耗時應(yīng)該是 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;
}
// 在執(zhí)行方法之前做打印
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
// 執(zhí)行消息的分發(fā)
try {
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
// 執(zhí)行方法之后再做打印
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}
原理其實是非常簡單的,關(guān)于異步消息、消息屏障、跨進程通信以及監(jiān)聽具體的某個方法耗時,在后面的源碼分析中會陸陸續(xù)續(xù)的提到,大家感興趣可以持續(xù)關(guān)注。國慶快樂~
視頻地址:https://pan.baidu.com/s/1fLq65bUnwswwrEa20RKpsw
視頻密碼:gkpv