如需轉載請評論或簡信,并注明出處,未經允許不得轉載
目錄
前言
之前寫過一篇文章Android消息機制(Handler、Looper、MessageQueue),這篇文章主要是從Java層入手,分析了和我們實際開發最貼近的消息機制原理。自認為對java層的消息機制流程已經非常熟悉了,但是面試是被問到“Handler是如何實現延遲消息的?”,“為什么Looper.loop()中的死循環不會導致ANR?”這些問題的時候,還是不能完全說出個所以然來,所以就決定再去看看native層到底做了哪些事情。如果你對java層的消息機制還不是特別熟悉的話,建議先看上一篇文章哦
消息隊列的創建
從上一章中,我們知道MessageQueue
是在Looper
的構造方法中創建的,但是我們沒有對MessageQueue
做更深入的分析
Looper.java
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
跟進一下MessageQueue
的構造方法,看看里面做了什么
MessageQueue.java
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
這里有一個nativeInit()
方法,這是一個native層的方法
core/jni/android_os_MessageQueue.cpp
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
...
//增加引用計數
nativeMessageQueue->incStrong(env);
//使用強制類型轉換符reinterpret_cast把NativeMessageQueue指針強轉成long類型并返回到java層
return reinterpret_cast<jlong>(nativeMessageQueue);
}
NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
//從當前線程的本地緩存(相當于ThreadLocal)拿到looper
mLooper = Looper::getForThread();
if (mLooper == NULL) {
//如果looper == null,創建一個looper加入到線程本地緩存中
mLooper = new Looper(false);
Looper::setForThread(mLooper);
}
}
我們發現,native層中也會創建一個Looper
和MessageQueue
,且native層的Looper
創建和獲取方式和java層非常的相似。native層的Looper
與Java層的Looper
沒有必然的關系,只是在native層重實現了一套類似功能的邏輯
system/core/libutils/Looper.cpp
再來看看Looper
的構造方法做了什么
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
//創建fd(文件操作符)
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();
}
這個eventfd實際上就是一個文件描述符,那什么是文件描述符呢?
文件描述符:簡稱fd,它就是一個int值,又叫做句柄,在Linux中,打開或新建一個文件,它會返回一個文件描述符,讀寫文件需要使用文件描述符來指定待讀寫的文件,所以文件描述符就是指代被打開的文件,所有對這個文件的IO操作都要通過文件描述符
void Looper::rebuildEpollLocked() {
//關閉舊的管道
if (mEpollFd >= 0) {
close(mEpollFd);
}
//創建一個新的epoll文件描述符,并注冊wake管道
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event));
//設置mWakeEventFd的可讀事件(EPOLLIN)監聽
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd;
//將喚醒事件fd(mWakeEventFd)添加到epoll文件描述符(mEpollFd),進行監聽
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
//將各種事件,如鍵盤、鼠標等事件的fd添加到epoll文件描述符(mEpollFd),進行監聽
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));
}
}
}
}
rebuildEpollLocked()
方法的主要作用其實就是通過epoll機制設置了mWakeEventFd的可讀事件和其他各種事件的監聽
epoll機制是Linux最高效的I/O復用機制,使用一個文件描述符管理多個描述符
I/O復用機制:多人聊天室就是一種非常適合I/O多路復用的例子,可能會同時有很多個用戶登錄,但是不會同時在同一個時刻發言。如果用普通I/O模型,則需要開很多的線程,大部分線程是空閑的,而且在處理多個客戶的消息的時候需要切換線程,對系統來講也是比較重的。而使用I/O多路復用則可以重復使用一條線程,減少線程空閑和切換的情況
epoll模型主要就是三個部分
epoll_create:創建epoll文件描述符
epoll_ctl:添加文件描述符監聽(如監聽mWakeEventFd的EPOLLIN事件)
epoll_wait:阻塞監聽add進來的描述符,只要其中任意一個或多個描述符可用或者超時就會返回(這個我們稍后會介紹)
在進一步分析消息機制的native層原理之前,我們可以先畫一個基本的系統架構圖,捋一捋他們之間的關系
發送消息
我們應用層調用Handler
的sendmessage()
、sendEmptyMessageDelayed()
、postDelayed()
、post()
最終都會調用到MessageQueue#enqueueMessage()
MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
....
//將消息延遲時間賦值給msg的成員變量
msg.when = when;
Message p = mMessages;
boolean needWake;
//① p == null,代表消息隊列為空
//② when == 0,當我們調用sendMessageAtFrontOfQueue的時候
//③ when < p.when,新來的消息比消息隊列中第一個消息還早
if (p == null || when == 0 || when < p.when) {
//插到頭結點
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
//如果不能插到頭結點,按時間將消息進行排序
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;
prev.next = msg;
}
if (needWake) {
//發送一個eventFd可讀事件
nativeWake(mPtr);
}
}
return true;
}
從這個函數中我們了解到,所謂的延遲消息,并不是延遲去發送消息,而是延遲去處理消息。延遲消息和普通消息一樣,都會在第一時間發送到消息隊列中
core/jni/android_os_MessageQueue.cpp
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();
}
system/core/libutils/Looper.cpp
void Looper::wake() {
uint64_t inc = 1;
//使用write函數往mWakeEventFd寫入字符inc,epoll就會收到可讀事件,被喚醒
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
...
}
還記得這個mWakeEventFd
是什么嗎?它就是我們一開始在native Looper
的構造方法中創建的用于可讀事件通知的文件操作符
總結一下發送消息就是根據消息的時間戳順序,往消息隊列中插入消息,每插入一條消息,就會往mWakeEventFd寫入字符inc,epoll就會收到可讀事件,被喚醒
接下來我們看看epoll收到可讀事件后,會發生什么?
讀取消息
Looper.loop()
內部實際上調用的是MessageQueue#next()
來讀取消息
MessageQueue.java
這里最重要的就是nativePollOnce()
方法,他是一個native層方法,它會一直阻塞,這里面就包含了epoll接收可讀事件的相關邏輯
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
...
//nextPollTimeoutMillis表示nativePollOnce的超時時間
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//這是個native層方法,會一直阻塞,直到下一條可用的消息返回
//ptr就是NativeMessageQueue的指針
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message msg = mMessages;
if (msg != null) {
if (now < msg.when) {
//如果消息觸發時間還沒到,計算還需要等多久
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//如果觸發時間到了,取出消息
mMessages = msg.next;
msg.next = null;
return msg;
}
} else {
//nextPollTimeoutMillis設置成-1代表沒有消息,nativePollOnce就會無限等待
nextPollTimeoutMillis = -1;
}
...
}
}
core/jni/android_os_MessageQueue.cpp
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);
}
system/core/libutils/Looper.cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) {
...
//當result不等于0時,就會跳出循環,返回到java層
if (result != 0) {
return result;
}
//處理內部輪詢
result = pollInner(timeoutMillis);
}
}
該方法內部是一個死循環,核心在于調用了pollInner
方法,pollInner
方法返回一個int值result,代表著本次輪詢是否成功處理了消息,當result不等于0時,就會跳出循環,返回到java層繼續處理java層消息
接下來我們重點看一下pollInner()
做了什么,這是整個消息機制的核心
system/core/libutils/Looper.cpp
int Looper::pollInner(int timeoutMillis) {
//...
//事件集合(eventItems),EPOLL_MAX_EVENTS為最大事件數量,它的值為16
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
//1.epoll_wait有四種情況
//① 當timeoutMillis == -1或timeoutMillis > 0,進入休眠等待
//② 如果有事件發生,且timeoutMillis == 0,就從管道中讀取事件放入事件集合(eventItems)返回事件個數
//③ 如果休眠timeoutMillis時間后還沒有被喚醒,就會返回0
//④ 如果出錯,就會返回-1
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
....
//獲取鎖
mLock.lock();
//2.遍歷事件集合(eventItems)
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
//如果文件描述符為mWakeEventFd
if (fd == mWakeEventFd) {
//并且事件類型為EPOLLIN(可讀事件),這說明當前線程關聯的管道的另外一端寫入了新數據
if (epollEvents & EPOLLIN) {
//調用awoken方法不斷的讀取管道數據
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
//如果是其他文件描述符,就進行它們自己的處理邏輯
...
}
}
//3、下面是處理Native的Message
Done:;
mNextMessageUptime = LLONG_MAX;
//mMessageEnvelopes是一個Vector集合,它代表著native中的消息隊列
while (mMessageEnvelopes.size() != 0) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
//取出MessageEnvelope,MessageEnvelop有收件人Hanlder和消息內容Message
const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
//判斷消息的執行時間
if (messageEnvelope.uptime <= now) {//消息到達執行時間
{
//獲取native層的Handler
sp<MessageHandler> handler = messageEnvelope.handler;
//獲取native層的消息
Message message = messageEnvelope.message;
mMessageEnvelopes.removeAt(0);
mSendingMessage = true;
//釋放鎖
mLock.unlock();
//通過MessageHandler的handleMessage方法處理native層的消息
handler->handleMessage(message);
}
mLock.lock();
mSendingMessage = false;
//result等于POLL_CALLBACK,表示某個監聽事件被觸發
result = POLL_CALLBACK;
} else {//消息還沒到執行時間
mNextMessageUptime = messageEnvelope.uptime;
//跳出循環,進入下一次輪詢
break;
}
}
//釋放鎖
mLock.unlock();
...
return result;
}
總結一下,pollInner
主要做了三件事情:
- 執行epoll_wait方法,等待事件發生或者超時(重要)
1)當timeoutMillis == -1或timeoutMillis > 0,進入休眠等待
2)如果有事件發生,且timeoutMillis == 0,就從管道中讀取事件放入事件集合(eventItems)返回事件個數
3)如果休眠timeoutMillis時間后還沒有被喚醒,就會返回0
4)如果出錯,就會返回-1
- 遍歷事件集合(eventItems),檢測哪一個文件描述符發生了IO事件
遍歷事件集合中,如果是mWakeEventFd
,就調用awoken方法不斷的讀取管道數據,直到清空管道,如果是其他的文件描述符發生了IO事件,讓它們自己處理相應邏輯
- 處理native層的Message
只要epoll_wait
方法返回后,都會進入Done
標記位的代碼段,就開始處理處理native層的Message
,處理完native層消息后,又會返回到java層處理java層的消息
用一個圖總結一下前面的過程
到此為止,native源碼分析的差不多了,對于native層消息機制,我們已經具備了不錯的理論基礎,接下來開始回答文章一開始的問題吧
如何實現延遲消息
我們先來看看應用層是如何調用的,這個大家應該都非常清楚
SystemClock.uptimeMillis()
是從開機到當前的毫秒數,也就是說最終傳給Looper::pollInner(int timeoutMillis)
的時間是以開機時間為基準的一個時間戳
public final boolean sendMessageDelayed(Message msg, long delayMillis){
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
我們上面介紹epoll_wait的時候,說過這個方法的四種情況
1)當timeoutMillis == -1或timeoutMillis > 0,進入休眠等待
2)如果有事件發生,且timeoutMillis == 0,就從管道中讀取事件放入事件集合(eventItems)返回事件個數
3)如果休眠timeoutMillis時間后還沒有被喚醒,就會返回0
4)如果出錯,就會返回-1
所以當epoll收到一條延遲消息的時候,timeoutMillis > 0,所以就符合第一條,這時候就會epoll進入休眠等待,直到休眠了timeoutMillis的時間,就會被喚醒并返回。也就是說此時nativePollOnce不再阻塞,就會從消息隊列中取出消息進行分發。所以,消息延遲就是基于epoll_wait的超時時間來實現的
為什么Looper.loop()中的死循環不會導致ANR
首先,我們得需要知道ANR是怎么產生的,有的人可能會說主線程執行了耗時操作、Activity
的最長執行時間是5秒、BroadcastReceiver
的最長執行時間則是10秒、Service
的最長執行時間是20秒等等這些。但是我們今天要說的,其實是要更深一步
ANR是怎么產生的
ANR的時候,我們會看到系統會彈出一個對話框,我們先找到這個對話框是從哪里彈出來的
ActivityManagerService.java
final void appNotResponding(ProcessRecord app, ActivityRecord activity,
ActivityRecord parent, boolean aboveSystem, final String annotation) {
...
Message msg = Message.obtain();
msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
msg.obj = new AppNotRespondingDialog.Data(app, activity, aboveSystem);
mService.mUiHandler.sendMessage(msg);
}
這里的SHOW_NOT_RESPONDING_UI_MSG
消息最終會執行到AppErrors#handleShowAnrUi()
,從而打開一個AppNotRespondingDialog
AppErrors.java
void handleShowAnrUi(Message msg) {
...
Dialog dialogToShow = new AppNotRespondingDialog(mService, mContext, data);
...
dialogToShow.show()
}
接下來我們來看看這個超時是如何觸發的,這里拿Service舉例
ActiveService.java
這是Service
開始啟動的方法
private final void realStartServiceLocked(ServiceRecord r,
ProcessRecord app, boolean execInFg){
...
bumpServiceExecutingLocked(r, execInFg, "create")
...
app.thread.scheduleCreateService(...)
...
}
private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
...
scheduleServiceTimeoutLocked(r.app);
...
}
static final int SERVICE_TIMEOUT = 20*1000;
// How long we wait for a service to finish executing.
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;
void scheduleServiceTimeoutLocked(ProcessRecord proc) {
...
Message msg = mAm.mHandler.obtainMessage(
ActivityManagerService.SERVICE_TIMEOUT_MSG);
msg.obj = proc;
//發送一個超時的消息,這個超時消息如果最終被接收,將會執行appNotResponding()
mAm.mHandler.sendMessageDelayed(msg,
proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}
通過看源碼我們發現,實際上就是在Service
啟動的時候,會往消息隊列中發送一個20s的延遲消息。如果這個消息在20s之內沒有被remove(),那就會彈出ANR的彈窗!為了不彈出這個ANR的彈窗,如果Android系統交給你們設計,你們會怎么做呢?
我想你們應該很容易想到吧,Andorid系統在Service啟動完成后就會remove消息,同樣的道理,輸入事件,BroadcastReceiver啟動等都會發送一個延遲消息,之后等到成功響應后就會remove這個延遲消息
我們通過兩個圖來對比一下啟動Service正常的情況和發生ANR的情況
looper為什么不會ANR
了解了ANR發生的原理,我們再來回答looper()
為什么不會ANR應該會更準確一些
我認為這里要回答兩點,一點是epoll的原理,還有一點就是ANR的觸發原理
在Linux中,文件、socket、管道(pipe)等可以進行IO操作的對象都可以稱之為流,既然是IO流,那肯定會有兩端:read端和write端,我們可以創建兩個文件描述符wiretFd和readFd,對應read端和write端,當流中沒有數據時,讀線程就會阻塞(休眠)等待,當寫線程通過wiretFd往流的wiret端寫入數據后,readFd對應的read端就會感應到,喚醒讀線程讀取數據,大概就是這樣的一個讀寫過程。讀線程進入阻塞后,并不會消耗CPU時間,這是epoll機制高效的原因之一
Activity
啟動事件時一個Message
,ANR也是一個延遲Message
,當Activity
啟動完成后就移除ANR的Message
,這是多么自然的事情,怎么會導致ANR呢?
其他問題
主線程中進行耗時操作一定會ANR嗎
通過閱讀源碼可以找到四種ANR類型(下面推薦了四篇文章,感興趣的可以跟著代碼看一看)
-
Activity
對輸入事件在5秒內沒有處理完畢(https://blog.csdn.net/chewbee/article/details/72670038) -
BroadcastReceiver
的onReceive
函數時10秒內沒有執行完畢(https://blog.csdn.net/chewbee/article/details/72672095) -
Service
的各個生命周期函數時20秒內沒有執行完畢(https://blog.csdn.net/chewbee/article/details/72671665) -
ContentProvider
執行相關操作的超時時間是20s(https://blog.csdn.net/chewbee/article/details/72670603)
如果觸發了上面這些情景,就會發生ANR,反之即使在主線程做了耗時操作,你也看不到ANR彈窗
可以做下面這個實驗
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
這樣僅僅會讓這個Activtiy延遲了10s才真正啟動,但是由于Activity沒有接收輸入時間,所以不會出現ANR彈窗。但是如果在Service中sleep超過20秒,就會出現ANR彈窗,這里Service和Activity其實是有區別的
總結
本文分析了native層MessageQueue
、Looper
的創建過程,也通過了解epoll的喚醒和阻塞機制,解決了“Handler是如何實現延遲消息的?”,“為什么Looper.loop()中的死循環不會導致ANR?”這兩個問題。看源碼有時候雖然辛苦,但是看完之后感覺還是有不少收獲的。所以大家以后有什么非常不解的問題,不妨研究研究源碼吧