android中的消息循环&键盘事件

消息循环

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可以控制监视多个文件句柄。

可以简单介绍一下epollselect最大的区别,epoll中会维持一个队列,记录发生事件的Fd,而select不会维护导致上层需要遍历找到发生事件的Fdepoll注册的时候可以有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队列中。这一点还是很有意思的。(管道不能传输太多数据?

android中的消息循环&键盘事件_第1张图片
image.png

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

最近分析软键盘。。顺便写一起了

android中的消息循环&键盘事件_第2张图片
image.png

这图。。放大看吧

WindowManagerService在启动的时候就会通过系统输入管理器InputManager来总负责监控键盘消息。这些键盘消息一般都是分发给当前激活的Activity窗口来处理的,因此,当前激活的Activity窗口在创建的时候,会到WindowManagerService中去注册一个接收键盘消息的通道,表明它要处理键盘消息,而当InputManager监控到有键盘消息时,就会分给给它处理。当当前激活的Activity窗口不再处于激活状态时,它也会到WindowManagerService中去反注册之前的键盘消息接收通道

android中的消息循环&键盘事件_第3张图片
5.png

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拿出来,并把要发送的键盘事件封装后塞入ConnectionoutboundQueue事件队列,最后放到通过前面创建的匿名共享内存里(忘了吗!),并通过管道通知应用层。。所以跟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来自外部的管道

最后再次敬佩老罗!

handler内存泄漏 todo

你可能感兴趣的:(android中的消息循环&键盘事件)