參考資料;
https://zh.wikipedia.org/wiki/%E7%AE%A1%E9%81%93_(Unix)
好久沒有寫文章了,最近想要學習的東西很多,opengl es也好久沒更新了,主要是事情太多了,剛好在公司研究了一下Native層的Handler源碼實現,這裏記錄一下學習的內容。
由於Native的Handler設計到c++以及對飲Linux系統接口的調用,文章講述的內容有如下三個方面:
- Linux中的匿名管道
- epoll函數
- Handler的Native層源碼研究
首先弄懂對應的API能夠幫助我們更好的去理解對應的Handler源碼。
Linux中的管道
tips
進程間的通信方式:
- 管道(pipe)和命名管道(FIFO)
- 信號(signal)
- 消息隊列
- 共享內存
- 信號量
- 套接字(socket)
想要了解多點的可以查看我的這篇文章
管道是Linux中進行進程通信或者線程通信的一種手段之一,管道分爲匿名管道(pipe)以及命名管道(named pipe),管道是內核維護的一個緩存, 它提供兩個 fd, 從一個fd寫入數據, 從另一個fd讀出數據. 所以它是半雙工的。
關於爲什麼是半雙工而不是雙工的請看這篇文章:
這裏由於Android的Native源碼中運用的是匿名管道,只針對匿名管道進行說明,關於命名管道(我也不太瞭解)有興趣的請自行查閱資料。
匿名管道
匿名管道通過調用pipe(int[2])函數來進行獲取兩個描述符,分別代表着管道讀端以及管道的寫端,方式如下:
int fds[2];
int result=pipe(fds);
if(result>=0){
...做自己的事情
}
以以上例子爲例,即fds[0]爲管道的讀端,fds[1]爲管道的寫端。管道的兩端是兩個普通的,匿名的文件描述符,這就讓其他進程無法連接該管道,所以稱之爲匿名管道。對於進程而言,通過管道通信需要在進程A關閉讀/寫端,在進程B關閉寫/讀端,數據流向爲單向。對於線程而言,不需要關閉管道任何端,子線程是和創建它的進程共享fd的,任何一方關閉管道的讀或寫都會影響到另一方。
使用匿名管道需要注意如下幾個點:
- 只提供單向通信,也就是說,兩個進程都能訪問這個文件,假設進程1往文件內寫東西,那麼進程2 就只能讀取文件的內容。
- 只能用於具有血緣關係的進程間通信,通常用於父子進程建通信
- 管道是基於字節流來通信的
- 依賴於文件系統,它的生命週期隨進程的結束結束(隨進程)
- 其本身自帶同步互斥效果
首先試下線程間通過匿名管道進行數據交換的過程:
void* run(void* fd){
std::cout<<"run start"<<std::endl;
char str[] = "hello everyone!";
write( *(int*)fd, str,strlen(str) );
}
int main (void)
{
int fd[2];
if(pipe(fd)){
throw out_of_range("error");
}
pthread_t tid=0;
pthread_create(&tid,NULL,run,&fd[1]);
pthread_join(tid, NULL);
char readbuf[1024];
sleep(3);
// read buf from child thread
read( fd[0], readbuf, sizeof(readbuf) );//阻塞操作
printf("%s\n",readbuf);
return (EXIT_SUCCESS);
}
//執行命令g++ main.cpp -o test -lpthread
// ./test
//輸出結果
run start
//等待三秒後
hello everyone!
通過匿名管道,我們在子線程中調用write(...)函數將數據寫入,在主線程中調用read(...)函數獲取對應的數據,從而實現了對應的子線程到主線程的數據的單向流通的操作,那如果要子線程讀取主線程通過匿名管道寫入的數據,改下實現即可:
printMsg (char ch)
{
std::cout << ch << std::endl;
}
void* run(void* fd){
std::cout<<"run start"<<std::endl;
char readbuf[1024];
read(*(int*)fd, readbuf, sizeof(readbuf) );
printf("%s\n",readbuf);
}
int main (void)
{
int fd[2];
if(pipe(fd)){
throw out_of_range("error");
}
pthread_t slef=pthread_self();
std::cout<<"pthread_id="<<slef<<std::endl;
pthread_t tid=0;
pthread_create(&tid,NULL,run,&fd[0]);
// read buf from child thread
char str[] = "hello everyone!";
write(fd[1], str,strlen(str) );
sleep(3);
return (EXIT_SUCCESS);
}
//輸出結果與上面的相同
接下來看下進程間通過匿名管道進行數據交流的過程,主要運行fork()函數進行子進程的初始化過程,首先測試從子進程寫數據,父進程讀數據的情況:
int main (void)
{
int fd[2];
int pid=0;
char str[]="hello everyone";
char readBuffer[1024];
if(pipe(fd)>=0){
if((pid=fork())<0){
printf("%s","fork error");
}else if(pid==0){
//子進程
printf("%s\n","子進程創建成功");
//關閉子進程的讀端
close(fd[0]);
//寫數據
write(fd[1],str,strlen(str));
printf("%s\n","子進程寫入數據完畢");
}else{
//父進程,即當前進程
printf("%s\n","父進程開始作業");
//關閉父進程寫端
close(fd[1]);
sleep(3);
read(fd[0],readBuffer,sizeof(readBuffer));
printf("父進程讀到數據=%s\n",readBuffer);
}
}
return (EXIT_SUCCESS);
}
//運行結果
父進程開始作業
子進程創建成功
子進程寫入數據完畢
父進程讀到數據=hello everyone
測試從父進程寫數據,子進程讀數據的情況:
int main (void)
{
int fd[2];
int pid=0;
char str[]="hello everyone";
char readBuffer[1024];
if(pipe(fd)>=0){
if((pid=fork())<0){
printf("%s","fork error");
}else if(pid==0){
printf("%s\n","子進程開始作業");
//關閉子進程寫端
close(fd[1]);
sleep(3);
read(fd[0],readBuffer,sizeof(readBuffer));
//子進程,即當前進程
printf("子進程讀到數據=%s\n",readBuffer);
}else{
//父進程
printf("%s\n","父進程創建成功");
//關閉父進程的讀端
close(fd[0]);
//寫數據
write(fd[1],str,strlen(str));
printf("%s\n","父進程寫入數據完畢");
}
}
return (EXIT_SUCCESS);
}
//輸出結果
父進程創建成功
父進程寫入數據完畢
子進程開始作業
子進程讀到數據=hello everyone
epoll
epoll是Linux對於select以及poll的增強版,在Linux的2.6內核提出。對於epoll可以直接在bash中用man進行文檔查看,或者查閱官網對應的內容。
對於epoll而言,網上有很多文章講了其實現的功能以及對應與select以及poll的比較,這裏對於我認爲比較好的文章進行總結以及梳理,資料大多來自於網上。
附:學習來源
https://zh.wikipedia.org/wiki/Epoll
http://blog.51cto.com/yaocoder/888374
https://www.zhihu.com/question/28594409
對於select,poll以及epoll的而言,三個都是IO多路複用的機制,可以監視多個描述符的讀/寫等事件,一旦某個描述符就緒(一般是讀或者寫事件發生了),就能夠將發生的事件通知給關心的應用程序去處理該事件。但select,poll,epoll本質上都是同步I/O,因爲他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
關於IO複用機制的說明,可以看下知乎的講解作爲最直觀的理解思路,slect,poll以及epoll的優缺點整理如下:
select優缺點如下:
缺點:
-
每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大;
-
同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大;
-
select支持的文件描述符數量太小了,默認是1024。
優點:
-
select的可移植性更好,在某些Unix系統上不支持poll()。
-
select對於超時值提供了更好的精度:微秒,而poll是毫秒。
poll優缺點如下:
缺點:
-
大量的fd的數組被整體複製於用戶態和內核地址空間之間,而不管這樣的複製是不是有意義;
-
與select一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。
優點:
-
poll() 不要求開發者計算最大文件描述符加一的大小。
-
poll() 在應付大數目的文件描述符的時候速度更快,相比於select。
-
它沒有最大連接數的限制,原因是它是基於鏈表來存儲的。
epoll的優點就是改進了前面所說缺點:
-
支持一個進程打開大數目的socket描述符:相比select,epoll則沒有對FD的限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關係很大。
-
IO效率不隨FD數目增加而線性下降:epoll不存在這個問題,它只會對"活躍"的socket進行操作--- 這是因爲在內核實現中epoll是根據每個fd上面的callback函數實現的。那麼,只有"活躍"的socket纔會主動的去調用 callback函數,其他idle狀態socket則不會,在這點上,epoll實現了一個"僞"AIO,因爲這時候推動力在os內核。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環境,epoll並不比select/poll有什麼效率,相 反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。
-
使用mmap加速內核與用戶空間的消息傳遞:這點實際上涉及到epoll的具體實現了。無論是select,poll還是epoll都需要內核把FD消息通知給用戶空間,如何避免不必要的內存拷貝就 很重要,在這點上,epoll是通過內核於用戶空間mmap同一塊內存實現的。
用法說明
epoll主要提供三個API給開發者進行調用實現自主功能:
- epoll_create(): 創建一個epoll實例並返回相應的文件描述符(epoll_create1() 擴展了epoll_create() 的功能)。
- epoll_ctl(): 註冊相關的文件描述符使用
- epoll_wait(): 可以用於等待IO事件。如果當前沒有可用的事件,這個函數會阻塞調用線程。
邊緣觸發(edge-triggered 簡稱ET)和水平觸發(level-triggered 簡稱LT):
epoll的事件派發接口可以運行在兩種模式下:邊緣觸發(edge-triggered)和水平觸發(level-triggered),兩種模式的區別請看下面,我們先假設下面的情況:
- 一個代表管道讀取的文件描述符已經註冊到epoll實例上了。
- 在管道的寫入端寫入了2kb的數據。
- epoll_wait 返回一個可用的rfd文件描述符。
- 從管道讀取了1kb的數據。
- 調用epoll_wait 完成。
如果rfd被設置了ET,在調用完第五步的epool_wait 後會被掛起,儘管在緩衝區還有可以讀取的數據,同時另外一段的管道還在等待發送完畢的反饋。這是因爲ET模式下只有文件描述符發生改變的時候,纔會派發事件。所以第五步操作,可能會去等待已經存在緩衝區的數據。在上面的例子中,一個事件在第二步被創建,再第三步中被消耗,由於第四步中沒有讀取完緩衝區,第五步中的epoll_wait可能會一直被阻塞下去。
下面情況下推薦使用ET模式:
- 使用非阻塞的IO。
- epoll_wait() 只需要在read或者write返回的時候。
相比之下,當我們使用LT的時候(默認),epoll會比poll更簡單更快速,而且我們可以使用在任何一個地方。
上述講述水平觸發和邊緣觸發翻譯來自epoll的doc中,想要完全理解可以查看這篇文章,講的十分清楚。
int epoll_create(int size);
epoll_create() 可以創建一個epoll實例。在linux 內核版本大於2.6.8 後,這個size 參數就被棄用了,但是傳入的值必須大於0。如果執行成功,返回一個非負數(實際爲文件描述符), 如果執行失敗,會返回-1。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
這個系統調用能夠控制給定的文件描述符epfd指向的epoll實例,op是添加事件的類型,fd是目標文件描述符。
有效的op值有以下幾種:
- EPOLL_CTL_ADD:註冊新的fd到epfd中(epfd爲epoll_create()返回的參數);
- EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
- EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數是需要監聽的fd。第四個參數是告訴內核需要監聽什麼事,代碼結構如下:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
//感興趣的事件和被觸發的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events這個參數是一個字節的掩碼構成的。下面是可以用的事件:
- EPOLLIN - 當關聯的文件可以執行 read ()操作時。
- EPOLLOUT - 當關聯的文件可以執行 write ()操作時。
- EPOLLRDHUP - (從 linux 2.6.17 開始)當socket關閉的時候,或者半關閉寫段的(當使用邊緣觸發的時候,這個標識在寫一些測試代碼去檢測關閉的時候特別好用)
- EPOLLPRI - 當 read ()能夠讀取緊急數據的時候。
- EPOLLERR - 當關聯的文件發生錯誤的時候,epoll_wait() 總是會等待這個事件,並不是需要必須設置的標識。
- EPOLLHUP - 當指定的文件描述符被掛起的時候。epoll_wait() 總是會等待這個事件,並不是需要必須設置的標識。當socket從某一個地方讀取數據的時候(管道或者socket),這個事件只是標識出這個已經讀取到最後了(EOF)。所有的有效數據已經被讀取完畢了,之後任何的讀取都會返回0(EOF)。
- EPOLLET - 設置指定的文件描述符模式爲邊緣觸發,默認的模式是水平觸發。
- EPOLLONESHOT - (從 linux 2.6.17 開始)設置指定文件描述符爲單次模式。這意味着,在設置後只會有一次從epoll_wait() 中捕獲到事件,之後你必須要重新調用 epoll_ctl() 重新設置。
返回值:如果成功,返回0。如果失敗,會返回-1, errno將會被設置。有以下幾種錯誤:
- EBADF - epfd 或者 fd 是無效的文件描述符。
- EEXIST - op是EPOLL_CTL_ADD,同時 fd 在之前,已經被註冊到epoll中了。
- EINVAL - epfd不是一個epoll描述符。或者fd和epfd相同,或者op參數非法。
- ENOENT - op是EPOLL_CTL_MOD或者EPOLL_CTL_DEL,但是fd還沒有被註冊到epoll上。
- ENOMEM - 內存不足。
- EPERM - 目標的fd不支持epoll。
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
epoll_wait 這個系統調用是用來等待epfd中的事件。events指向調用者可以使用的事件的內存區域。maxevents告知內核有多少個events,必須要大於0.
timeout這個參數是用來制定epoll_wait 會阻塞多少毫秒,會一直阻塞到下面幾種情況:
- 一個文件描述符觸發了事件。
- 被一個信號處理函數打斷,或者timeout超時。
當timeout等於-1的時候這個函數會無限期的阻塞下去,當timeout等於0的時候,就算沒有任何事件,也會立刻返回。
下面寫個例子演示一下epoll和pipe一起使用的過程:
static int MAX=256;
struct Data{
int* fd;
int epfd;
struct epoll_event events[];
};
void *runEp(void* data){
printf("線程運行開始\n");
Data r_data=*(Data*)data;
struct epoll_event allEvs[MAX];
int pipeFd=*(r_data.fd);
//struct epoll_event events[MAX]=r_data.events;
int count=epoll_wait(r_data.epfd,allEvs,MAX,5000);
for(int i=0;i<count;i++){
if(allEvs[i].data.fd==pipeFd&&(allEvs[i].events&EPOLLIN)){
printf("接收到管道可以進行讀的信號,開始讀取\n");
char buffer[MAX];
read(pipeFd,buffer,100);
printf("讀取的內容是:%s\n",buffer);
}
}
}
void testEpoll(){
int epollId=epoll_create(MAX);
if(epollId<=0){
throw out_of_range("epoll error");
}
int pipFd[2];
int pirRes;
if((pirRes=pipe(pipFd))<0){
throw out_of_range("pipe error");
}
struct epoll_event event;
event.data.fd=pipFd[0];//監聽管道讀端
event.events=EPOLLIN|EPOLLET;//設置參數,接收可以read()的通知,設置邊緣觸發模式
int epfd=epoll_create(MAX);
struct Data data;
data.epfd=epfd;
data.fd=&pipFd[0];
int res=epoll_ctl(epfd,EPOLL_CTL_ADD,pipFd[0],&event);
if(res!=0){
throw out_of_range("pipe error");
}
pthread_t tid=12;
pthread_create(&tid,NULL,runEp,&data);
sleep(2);
char str[] = "hello everyone!";
write(pipFd[1], str,strlen(str) );
printf("寫入管道數據完畢\n");
sleep(3);
}
//運行testEpoll()輸出結果:
線程運行開始
寫入管道數據完畢
接收到管道可以進行讀的信號,開始讀取
讀取的內容是:hello everyone!
NativeHandler的執行過程
上面瞭解了一下關於管道以及epoll,接下來跟蹤一下Handler的具體源碼來理一下邏輯。首先Looper在初始化的時候會同時初始化一個MessageQueue,在MessageQueue的構造函數如下:
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
對應的native層實現在android_os_MessageQueue.cpp
文件中:
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
//初始化一個本地的MessageQueue
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
if (!nativeMessageQueue) {
jniThrowRuntimeException(env, "Unable to allocate native queue");
return 0;
}
nativeMessageQueue->incStrong(env);//增加引用
return reinterpret_cast<jlong>(nativeMessageQueue);//返回指針地址
}
上述代碼主要相關的爲兩件事情:
- 初始化一個NativeMessageQueue
- 將指針返回到Java層以便下層通過指針地址直接訪問。
在NativeMessageQueue初始化過程如下:
NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread();
if (mLooper == NULL) {
mLooper = new Looper(false);
Looper::setForThread(mLooper);
}
}
這裏在Native層也建立了一個Looper,實際上可以理解爲Looper.java在Native層的映射,看下構造函數:
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
int wakeFds[2];
int result = pipe(wakeFds);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not create wake pipe. errno=%d", errno);
mWakeReadPipeFd = wakeFds[0];
mWakeWritePipeFd = wakeFds[1];
...
mIdling = false;
// Allocate the epoll instance and register the wake pipe.
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance. errno=%d", errno);
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
eventItem.events = EPOLLIN;//監聽管道的read()操作
eventItem.data.fd = mWakeReadPipeFd;//記錄管道讀端的fd
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);
...
}
這裏的一套在學習epoll的時候已經見識過了,在native層的Looper的構造函數中會去監聽管道讀端的read()操作。
總結一下messagequeue.nativeInit()做的事情:
調用Natvie層代碼在Native初始化一個NativeMessageQueue和Looper,在Looper中會開啓一個匿名管道,由epoll來監聽I/O事件的變化,當管道中有數據的時候,通過epoll通知系統讀取數據。最後返回一個NativeMessageQueue的指針交由Java層的MessageQueue方便下次尋址訪問。
ok,這裏初始化完Java層的Looper,之後會調用Looper.loop()方法,在該方法中會一直取MessageQueue裏面的數據:
public static void loop() {
...
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
...
}
MessageQueue.next()方法如下:
Message next() {
//獲取指針地址
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
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;
}
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// 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);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}
這裏可以看到調用了nativePollOnce(...)
方法進入了native層,對應實現爲:
//`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);
}
該方法最終調用native層的Looper.pollOnce(...)
:
//Looper.cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) {
...
result = pollInner(timeoutMillis);
}
}
int Looper::pollInner(int timeoutMillis) {
...
// Poll.
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;
// We are about to idle.
mIdling = true;
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
//阻塞等待可以讀取管道的通知
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// No longer idling.
mIdling = false;
// Acquire lock.
mLock.lock();
...
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeReadPipeFd) {
if (epollEvents & EPOLLIN) {
awoken();//
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake read pipe.", epollEvents);
}
} else {
...
}
}
Done: ;
...
return result;
}
關鍵代碼在於awaken()
方法:
void Looper::awoken() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ awoken", this);
#endif
char buffer[16];
ssize_t nRead;
do {
nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));//可以看到讀取了管道中的內容
} while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));
}
那麼read(..)方法執行了,哪裏進行write(..)方法的操作呢?答案在於我們將消息push到MessageQueue中時候,即MessageQueue.enqueueMessages(...)方法中,裏面會執行:
nativeWake(mPtr);
這個最終會調用到native層的Looper中的wake()方法:
void Looper::wake() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ wake", this);
#endif
ssize_t nWrite;
do {
nWrite = write(mWakeWritePipeFd, "W", 1);//進行了寫操作
} while (nWrite == -1 && errno == EINTR);
if (nWrite != 1) {
if (errno != EAGAIN) {
ALOGW("Could not write wake signal, errno=%d", errno);
}
}
}
Handler在native層主要的邏輯代碼已經瞭解了,那麼總結一下:
引用Gityuan大神的解釋:
在主線程的MessageQueue沒有消息時,便阻塞在loop的queue.next()中的nativePollOnce()方法裏,詳情見Android消息機制1-Handler(Java層),此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,通過往pipe管道寫端寫入數據來喚醒主線程工作。這裏採用的epoll機制,是一種IO多路複用機制,可以同時監控多個描述符,當某個描述符就緒(讀或寫就緒),則立刻通知相應程序進行讀或寫操作,本質同步I/O,即讀寫是阻塞的。 所以說,主線程大多數時候都是處於休眠狀態,並不會消耗大量CPU資源。