最近在尝试把Android上的游戏手柄的按键给标准化, 通过上下层的分析, 理论上是可行的。
现在先记录下学习的总结。
Android的输入主要分为两部分:
C++层: 一个是底层事件的收集与分发。 (这部分属于 system_process)
Java和C++层: 事件的消费。 (这部分存在于用户的进程)
这两者在不同的进程, 他们的数据传递方式在Android4.1之后是通过SocketPair(我看的是4.4的代码), Android4.1之前是通过共享内存的方式。
网上的文章也很多描述输入部分的, 但是C++层如何传递数据, 写的一般比较清楚, 而且代码比较清晰。 对于如何传递给Java层的就比较模糊。
所以对于C++层的分析, 我推荐一篇Blog比较全面的介绍 http://blog.csdn.net/myarrow/article/details/7091061 。
这篇文章的重点不在于贴上的代码有多少, 而在与底层实现的思想,配上图, 这样对底层有一个大概的了解。(这篇文章也有比较大的局限性, 对事件如何传递到Java层没有做解释)
本文将分为两个部分介绍:
1) 用户进程InputChannel如何同C++层的InputChannel关联起来的(InputChannel的注册流程)
2) C++层如何向Java层传递事件的, 以及Java层是如何消费这个事件的
附: 介绍Java层事件传递的方式
1) 用户进程InputChannel如何同C++层的InputChannel关联起来的(InputChannel的注册流程)
从ViewRoot.java的setView方法开始分析。
大致的流程图:
注册的流程:
1: ViewRoot 首先创建一个Java层的InputChannel, 通过WindowSession同WMS交互
2: WMS首先通过InputChannel.openInputChannelPair(name)再Nativie层创建一对InputChannel(Android4.1开始InputChannel是通过Socket实现的, 之前是通过ShareMemory实现的)。
3:上层应用注册接收端的InputChannel
3-1: ViewRoot拿到刚才已经关联到Native层的InputChannel, 用InputChannel和Looper创建一个WindowInputEventReceiver。
3-2: WindowInputEventReceiver在创建的时候, 调用Natice层的NativeInputEventReceiver,把InputChannel注册到Native层的Looper上监听ALOOPER_EVENT_INPUT事件。
int fd = mInputConsumer.getChannel()->getFd();
mMessageQueue->getLooper()->addFd(fd, 0, events, this, NULL);
4:系统服务注册接收端的InputChannel
4-1:InputManager通过Native方法nativeRegisterInputChannel注册服务端的InputChannel
4-2:Native层先保存这个连接, 然后再跟接收端一样在Looper上进行监听。
sp connection = new Connection(inputChannel, inputWindowHandle, monitor);
int fd = inputChannel->getFd();
mConnectionsByFd.add(fd, connection);
mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this);
整个接收端的注册流程比较简单, 发送端只需要把信息写入发送端的InputChannel中, 接收端就可以接收到信息了。
2) C++层如何向Java层传递事件的, 以及Java层是如何消费这个事件的
2-1: 一个事件如何从InputDispatcher中发到应用层的
从InputDispatcher中等待事件开始分析:
首先在Looper中等待InputReader往InBoundQueue队列中添加事件。
1: 由InputReader唤醒InputDispatcher的等待, 开始执行dispatchOnce。
2: 然后在dispatchOnceInnerLocked中, 把事件从InBoundQueue中取出。
3: 对于KeyEvent, 在dispatchKeyLocked中先拿到当前窗口的InputChannel。
4:接着dispatchEventLocked中取得之前服务端保存的Connection。
5:在enqueueDispatchEntriesLocked方法中, 把事件放入outboundqueue队列中(InboundQueue: 同步InputReader和InputDispatcher的,outboundqueue是同步InputDispatcher和InputPublisher )。
6: 再startDispatchCycleLocked中通过InputPublisher开始传递数据。
7: InputPublisher实际是调用InputChannel, 向之前打开的Socket开始写数据, 所以应用程序端的Socket就会被唤醒。
2-2: 应用层如何从C++层相应事件, 以及上层大致的处理思路
传递的基本流程也比较简单:
1: 应用程序由于之前已经在Looper上注册了InputChannel(即Socket), 2-1最后一步向服务端的Socket写入数据, 所以应用程序的Socket就会被唤醒。
2:根据注册的信息, getLooper()->addFd(fd, 0, events, this, NULL);, 所以唤醒后首先执行回调函数 LooperCallback.handleEvent.
3:根据handleevent的events参数判断是否为ALOOPER_EVENT_OUTPUT事件, 接着调用consumeEvents处理。
4: consumeEvents处理主要分为3部:
4-1: 通过InputConsumer.consume构造一个C++层的InputEvent(这部分首先调用客户端的InputChannel.receiveMessage方法, 这个方法的实现就是从Socket的另一端读出刚才服务器端写入的内容)
4-2: 把C++层的InputEvent构造成为Java层的一个对象。
4-3: 最后通过JNI的反向调用, 执行Java代码 env->CallVoidMethod(receiverObj.get(),gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
至此事件从C++层传递到了Java层。
附: 介绍Java层事件传递的方式
Java层事件分为两类一类是KeyEvent和MotionEvent, 对于KeyEvent的事件传递, 刚才2-2的传递流程是OK的。
对于MontionEvent的传递, android4 加入的Vsync, UI的更新是由Vsync来触发的, 这里Montion事件的传递也是由Vsync来出发的。 (由于UI是有Vsync触发刷新的, 如果Montion事件的传递不是由Vsync触发的话, 可能会导致画面和事件的传递不同步)
KeyEvent事件由于不涉及UI的更新, 所以跟Vsync无关。
由于4.1开始, 再处理UI的时候会阻塞事件的处理, 所以在UI更新完毕之后会主动的调用事件处理函数。
主要代码位于android.view.Choreographer.java
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (frameTimeNanos < mLastFrameTimeNanos) {
scheduleVsyncLocked();
return;
}
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
}
事件在Java层的处理,主要在ViewRootImpl.java中先处理。
不论是KeyEvent还是MotionEvent最终的处理都是在doProcessInputEvents中处理。
1: doProcessInputEvents中主要是循环处理所以的事件。
2: 具体的处理位置在doProcessInputEvents, 在这个函数中首先判断是否把事件交由Ime处理。
3: 具体的处理流程: Java层封装成一个链的流程。 如果最终都没有处理掉的事件, 则交给SyntheticInputStage, 它是用来转换一些特殊事件的。(例如 遥感事件就是在这里转换成方向键的)
至此: 把一个事件从C++层如何完整的传递到Java层的流程分析完毕, 整个流程没有贴大量的代码。
因为代码大家都有, 代码多了反而容易迷糊,篇幅冗余,而且代码太多容易抓不住重点, 贴图加上文字描述, 这样整个流程做到了然于胸, 某个过程不明白的可以去看具体代码。