消息循环
public final class ActivityThread {
......
public static final void main(String[] args) {
Looper.prepareMainLooper(); //sThreadLocal.set(new Looper()); 创建looper对象
ActivityThread thread = new ActivityThread(); //H mH = new H();
thread.attach(false);
Looper.loop();
thread.detach();
}
}
//sThreadLocal.set(new Looper());
每个线程的threadLocal
中存储自己的Looper
,每个Looper
有自己的消息队列MessageQueue
以上是主线程消息循环的创建过程。对于子线程来说,创建Handler时使用new Handler(thread.getLooper())
Looper.cpp
每个Java
层的Looper
都会在native
层对应一个Looper
,(MessageQueue)
类似,下面来看看native
层的Looper
的初始过程。
int result = pipe(wakeFds); //创建管道
mWakeReadPipeFd = wakeFds[0];
mWakeWritePipeFd = wakeFds[1];
mEpollFd = epoll_create(EPOLL_SIZE_HINT); //创建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,主线程在消息队列中没有消息时要进入等待状态以及当消息队列有消息时要把应用程序主线程唤醒
监控mWakeReadPipeFd
文件描述符的EPOLLIN事
件,即当管道中有内容可读时,就唤醒当前正在等待管道中的内容的线程。
这里解释一下,管道和epoll
的作用。管道是负责读和取。作为Linux弱智的我一开始总是不明白为什么有了管道还要有epoll
,其实这两个是并列的关系,epoll/poll/select
都是会阻塞进程的,在有多个Fd
的时候,使用它们不需要使用多个程序来一一控制,多路复用最大的意义在于可以一个socket
控制多个Fd
。一个程序可以程序监视多个文件句柄(file descriptor)的状态变化。在后面,我们会看到Looper
有一个addFd
的接口,通过epoll
,mLooper
中的一个`mEpollFd可以控制监视多个文件句柄。
可以简单介绍一下epoll
与select
最大的区别,epoll
中会维持一个队列,记录发生事件的Fd
,而select
不会维护导致上层需要遍历找到发生事件的Fd
。epoll注册的时候可以有callback,当Fd发生事件时,会去回调这个callback。
也就是说epoll
机制接管了管道读写,它站在了更上层。
Looper.loop
初始化Ok后,进入loop循环。从此就兢兢业业从message.next()
读取消息。
Message.next
主要执行 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
等待事件发生或是超时,然后从readPipeFd
中读出数据。
消息发送
queue.enqueueMessage(msg, uptimeMillis);
将消息按消息的when
插入到mMessage
队列中,如果当前消息队列没有消息,nativeWake
唤醒主线程。最后也就是通过Looper
唤醒:
nWrite = write(mWakeWritePipeFd, "W", 1);
向管道写入一个1
消息接受
通过write
唤醒。
这里特别要注意,pipe
生成的read/write Fd
只是用来通知唤醒,write
只会往里写一个简单的1,read
也只会去读出内容是否为1。真正的消息还是存放在下图中的mMessage
队列中。这一点还是很有意思的。(管道不能传输太多数据?
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在创建的时候会进行Handler myHandler = new Handler()
所以,如果不在主线程明显这句代码是会抛出错误的
new Thread(){
public void run(){
Looper.prepare();//给当前线程初始化Looper
Toast.makeText(getApplicationContext(),"你猜我能不能弹出来~~",0).show();//Toast初始化的时候会new Handler();无参构造默认获取当前线程的Looper,如果没有prepare过,则抛出题主描述的异常。上一句代码初始化过了,就不会出错。
Looper.loop();//这句执行,Toast排队show所依赖的Handler发出的消息就有人处理了,Toast就可以吐出来了。但是,这个Thread也阻塞这里了,因为loop()是个for (;;) ...
}
}.start();
InputManager
最近分析软键盘。。顺便写一起了
这图。。放大看吧
WindowManagerService在启动的时候就会通过系统输入管理器InputManager来总负责监控键盘消息。这些键盘消息一般都是分发给当前激活的Activity窗口来处理的,因此,当前激活的Activity窗口在创建的时候,会到WindowManagerService中去注册一个接收键盘消息的通道,表明它要处理键盘消息,而当InputManager监控到有键盘消息时,就会分给给它处理。当当前激活的Activity窗口不再处于激活状态时,它也会到WindowManagerService中去反注册之前的键盘消息接收通道
EventHub
具体与键盘设备交互的类。太底层了懒得分析了,反正InputReader
线程就是通过它去读具体的键盘事件的。
ViewRoot.setView
sWindowSession.add
if (outInputChannel != null) {
String name = win.makeInputChannelName();
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name); //创建一对输入通道
win.mInputChannel = inputChannels[0];
inputChannels[1].transferToBinderOutParameter(outInputChannel); //通过outInputChannel参数返回到应用程序中
mInputManager.registerInputChannel(win.mInputChannel);
}
首先会创建一堆输入通道,一个供server InputManager
使用,一个供应用层client
使用。那么这个输入通道具体是什么,这个就比较复杂了,会创建一个匿名共享内存文件和两个管道,看大图也能知道两个管道一个负责client->server的读写,一个负责server->client的读写
具体来说,Server端和Client端的InputChannel分别是这样构成的:
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
可以同时监控多个Fd
,所以这里发现可以有读的内容的时候就会调用handleReceiveCallback
poll
inputReadThread
会通过EventHub
看是否有键盘事件发生,如果没有,通过poll
来睡眠等待
int pollResult = poll(mFDs, mFDCount, -1);
这是一个Linux系统的文件操作系统调用,它用来查询指定的文件列表是否有有可读写的,如果有,就马上返回,否则的话,就阻塞线程,并等待驱动程序唤醒,重新调用poll函数,或超时返回。在我们的这个场景中,就是要查询是否有键盘事件发生,如果有的话,就返回,否则的话,当前线程就睡眠等待键盘事件的发生了。
当键盘事件发生后,就通过mLooper->wake();
唤醒睡眠着的InputDispatcherThread
线程
然后inputDispatcherThread
找到之前存储的被激活的窗口后,把之前的Connection
拿出来,并把要发送的键盘事件封装后塞入Connection
的outboundQueue
事件队列,最后放到通过前面创建的匿名共享内存里(忘了吗!),并通过管道通知应用层。。所以跟handler的消息机制一样,这里的管道只起一个通知的作用,真正是从匿名共享内存进行读取的。
应用层处理
应用层获取到键盘消息后,通知InputMethodManager .dispatchKeyEvent
处理,如果该manager
没有处理就通过View .dispatchKeyEvent
处理。
总结
我们可以总结一下,
A. 键盘事件发生,InputManager中的InputReader被唤醒,此前InputReader睡眠在/dev/input/event0这个设备文件上;
B. InputReader被唤醒后,它接着唤醒InputManager中的InputDispatcher,此前InputDispatcher睡眠在InputManager所运行的线程中的Looper对象里面的管道的读端上;这是looper内部的管道
C. InputDispatcher被唤醒后,它接着唤醒应用程序的主线程来处理这个键盘事件,此前应用程序的主线程睡眠在Client端InputChannel中的前向管道的读端上;
D. 应用程序处理处理键盘事件之后,它接着唤醒InputDispatcher来执行善后工作,此前InputDispatcher睡眠在Server端InputChannel的反向管道的读端上,注意这里与第二个线索处的区别。
C/D是Looper来自外部的管道
最后再次敬佩老罗!