消息循環(huán)
public final class ActivityThread {
......
public static final void main(String[] args) {
Looper.prepareMainLooper(); //sThreadLocal.set(new Looper()); 創(chuàng)建looper對象
ActivityThread thread = new ActivityThread(); //H mH = new H();
thread.attach(false);
Looper.loop();
thread.detach();
}
}
//sThreadLocal.set(new Looper());
每個線程的threadLocal
中存儲自己的Looper
,每個Looper
有自己的消息隊列MessageQueue
以上是主線程消息循環(huán)的創(chuàng)建過程慎璧。對于子線程來說趁矾,創(chuàng)建Handler時使用new Handler(thread.getLooper())
Looper.cpp
每個Java
層的Looper
都會在native
層對應一個Looper
,(MessageQueue)
類似艳狐,下面來看看native
層的Looper
的初始過程芹关。
int result = pipe(wakeFds); //創(chuàng)建管道
mWakeReadPipeFd = wakeFds[0];
mWakeWritePipeFd = wakeFds[1];
mEpollFd = epoll_create(EPOLL_SIZE_HINT); //創(chuàng)建epoll
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeReadPipeFd;
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem); //向epoll注冊讀管道
通過管道+epoll,主線程在消息隊列中沒有消息時要進入等待狀態(tài)以及當消息隊列有消息時要把應用程序主線程喚醒
監(jiān)控mWakeReadPipeFd
文件描述符的EPOLLIN事
件行瑞,即當管道中有內(nèi)容可讀時奸腺,就喚醒當前正在等待管道中的內(nèi)容的線程。
這里解釋一下血久,管道和epoll
的作用突照。管道是負責讀和取。作為Linux弱智的我一開始總是不明白為什么有了管道還要有epoll
,其實這兩個是并列的關(guān)系氧吐,epoll/poll/select
都是會阻塞進程的讹蘑,在有多個Fd
的時候末盔,使用它們不需要使用多個程序來一一控制,多路復用最大的意義在于可以一個socket
控制多個Fd
座慰。一個程序可以程序監(jiān)視多個文件句柄(file descriptor)的狀態(tài)變化陨舱。在后面,我們會看到Looper
有一個addFd
的接口版仔,通過epoll
,mLooper
中的一個`mEpollFd可以控制監(jiān)視多個文件句柄游盲。
可以簡單介紹一下epoll
與select
最大的區(qū)別,epoll
中會維持一個隊列蛮粮,記錄發(fā)生事件的Fd
,而select
不會維護導致上層需要遍歷找到發(fā)生事件的Fd
益缎。epoll注冊的時候可以有callback,當Fd發(fā)生事件時,會去回調(diào)這個callback蝉揍。
也就是說epoll
機制接管了管道讀寫,它站在了更上層链峭。
Looper.loop
初始化Ok后,進入loop循環(huán)畦娄。從此就兢兢業(yè)業(yè)從message.next()
讀取消息又沾。
Message.next
主要執(zhí)行 nativePollOnce(mPtr, nextPollTimeoutMillis);
它背后實際用的就是mLooper->pollOnce(timeoutMillis);-> pollInner
pollInner
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));
使用epoll_wait
等待事件發(fā)生或是超時,然后從readPipeFd
中讀出數(shù)據(jù)熙卡。
消息發(fā)送
queue.enqueueMessage(msg, uptimeMillis);
將消息按消息的when
插入到mMessage
隊列中,如果當前消息隊列沒有消息杖刷,nativeWake
喚醒主線程。最后也就是通過Looper
喚醒:
nWrite = write(mWakeWritePipeFd, "W", 1);
向管道寫入一個1
消息接受
通過write
喚醒驳癌。
這里特別要注意,pipe
生成的read/write Fd
只是用來通知喚醒滑燃,write
只會往里寫一個簡單的1,read
也只會去讀出內(nèi)容是否為1颓鲜。真正的消息還是存放在下圖中的mMessage
隊列中表窘。這一點還是很有意思的。(管道不能傳輸太多數(shù)據(jù)甜滨?
handler.java
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
這個也值得記錄乐严。
HandlerThread
Handler sWorker = new Handler(sWorkerThread.getLooper()); //handlerThread
初始化的時候會Looper.prepare,loop
神秘的Toast報錯 todo
漲姿勢,Toast在創(chuàng)建的時候會進行Handler myHandler = new Handler()
所以衣摩,如果不在主線程明顯這句代碼是會拋出錯誤的
new Thread(){
public void run(){
Looper.prepare();//給當前線程初始化Looper
Toast.makeText(getApplicationContext(),"你猜我能不能彈出來~~",0).show();//Toast初始化的時候會new Handler();無參構(gòu)造默認獲取當前線程的Looper昂验,如果沒有prepare過,則拋出題主描述的異常艾扮。上一句代碼初始化過了既琴,就不會出錯。
Looper.loop();//這句執(zhí)行泡嘴,Toast排隊show所依賴的Handler發(fā)出的消息就有人處理了甫恩,Toast就可以吐出來了。但是酌予,這個Thread也阻塞這里了磺箕,因為loop()是個for (;;) ...
}
}.start();
InputManager
最近分析軟鍵盤纹腌。。順便寫一起了
這圖滞磺。升薯。放大看吧
WindowManagerService在啟動的時候就會通過系統(tǒng)輸入管理器InputManager來總負責監(jiān)控鍵盤消息。這些鍵盤消息一般都是分發(fā)給當前激活的Activity窗口來處理的击困,因此涎劈,當前激活的Activity窗口在創(chuàng)建的時候,會到WindowManagerService中去注冊一個接收鍵盤消息的通道阅茶,表明它要處理鍵盤消息蛛枚,而當InputManager監(jiān)控到有鍵盤消息時,就會分給給它處理脸哀。當當前激活的Activity窗口不再處于激活狀態(tài)時蹦浦,它也會到WindowManagerService中去反注冊之前的鍵盤消息接收通道
EventHub
具體與鍵盤設(shè)備交互的類。太底層了懶得分析了,反正InputReader
線程就是通過它去讀具體的鍵盤事件的撞蜂。
ViewRoot.setView
sWindowSession.add
if (outInputChannel != null) {
String name = win.makeInputChannelName();
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name); //創(chuàng)建一對輸入通道
win.mInputChannel = inputChannels[0];
inputChannels[1].transferToBinderOutParameter(outInputChannel); //通過outInputChannel參數(shù)返回到應用程序中
mInputManager.registerInputChannel(win.mInputChannel);
}
首先會創(chuàng)建一堆輸入通道,一個供server InputManager
使用,一個供應用層client
使用盲镶。那么這個輸入通道具體是什么,這個就比較復雜了,會創(chuàng)建一個匿名共享內(nèi)存文件和兩個管道,看大圖也能知道兩個管道一個負責client->server的讀寫,一個負責server->client的讀寫
具體來說,Server端和Client端的InputChannel分別是這樣構(gòu)成的:
Server Input Channel: ashmem - reverse(read) - forward(write)
Client Input Channel: ashmem - forward(read) - reverse(write)
最后會把兩個inputChannel
分別注冊到服務層和應用層蝌诡。注冊的時候又要搞事,包了一層Connection
:
int32_t receiveFd = inputChannel->getReceivePipeFd(); //反向管道的讀
mConnectionsByReceiveFd.add(receiveFd, connection);
if (monitor) {
mMonitoringChannels.push(inputChannel);
}
mLooper->addFd(receiveFd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this);
又見mLooper->addFd
,前面說過,epoll
可以同時監(jiān)控多個Fd
,所以這里發(fā)現(xiàn)可以有讀的內(nèi)容的時候就會調(diào)用handleReceiveCallback
poll
inputReadThread
會通過EventHub
看是否有鍵盤事件發(fā)生溉贿,如果沒有,通過poll
來睡眠等待
int pollResult = poll(mFDs, mFDCount, -1);
這是一個Linux系統(tǒng)的文件操作系統(tǒng)調(diào)用浦旱,它用來查詢指定的文件列表是否有有可讀寫的宇色,如果有,就馬上返回颁湖,否則的話宣蠕,就阻塞線程,并等待驅(qū)動程序喚醒甥捺,重新調(diào)用poll函數(shù)抢蚀,或超時返回。在我們的這個場景中涎永,就是要查詢是否有鍵盤事件發(fā)生思币,如果有的話,就返回羡微,否則的話谷饿,當前線程就睡眠等待鍵盤事件的發(fā)生了。
當鍵盤事件發(fā)生后,就通過mLooper->wake();
喚醒睡眠著的InputDispatcherThread
線程
然后inputDispatcherThread
找到之前存儲的被激活的窗口后妈倔,把之前的Connection
拿出來,并把要發(fā)送的鍵盤事件封裝后塞入Connection
的outboundQueue
事件隊列,最后放到通過前面創(chuàng)建的匿名共享內(nèi)存里(忘了嗎博投!),并通過管道通知應用層盯蝴。毅哗。所以跟handler的消息機制一樣听怕,這里的管道只起一個通知的作用,真正是從匿名共享內(nèi)存進行讀取的虑绵。
應用層處理
應用層獲取到鍵盤消息后尿瞭,通知InputMethodManager .dispatchKeyEvent
處理,如果該manager
沒有處理就通過View .dispatchKeyEvent
處理。
總結(jié)
我們可以總結(jié)一下翅睛,
A. 鍵盤事件發(fā)生声搁,InputManager中的InputReader被喚醒,此前InputReader睡眠在/dev/input/event0這個設(shè)備文件上捕发;
B. InputReader被喚醒后疏旨,它接著喚醒InputManager中的InputDispatcher,此前InputDispatcher睡眠在InputManager所運行的線程中的Looper對象里面的管道的讀端上扎酷;這是looper內(nèi)部的管道
C. InputDispatcher被喚醒后檐涝,它接著喚醒應用程序的主線程來處理這個鍵盤事件,此前應用程序的主線程睡眠在Client端InputChannel中的前向管道的讀端上法挨;
D. 應用程序處理處理鍵盤事件之后谁榜,它接著喚醒InputDispatcher來執(zhí)行善后工作,此前InputDispatcher睡眠在Server端InputChannel的反向管道的讀端上坷剧,注意這里與第二個線索處的區(qū)別惰爬。
C/D是Looper來自外部的管道
最后再次敬佩老羅!