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队列中。这一点还是很有意思的。(管道不能传输太多数据?

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

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

image.png

这图。。放大看吧

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容