当用户在 Android 系统设备上触摸屏幕或者按键操作时,会先触发硬件驱动,硬件驱动受到事件后,会将相应事件写入到设备节点(/dev/input/
目录下),这便产生了最原生态的内核事件。比如我的测试设备有以下一些设备节点:
OnePlus5T:/ $ ls /dev/input/
event0 event1 event2 event3 event4 event5 event6 mice mouse0
有 event0 代表电源键输入,event3 代表音量键输入,event4 代表屏幕输入等。
可通过 getevent
命令实时监听某个设备节点的事件写入情况,如一个轻触屏幕的点击会产生以下事件:
OnePlus5T:/ $ getevent -lt /dev/input/event4
[ 277099.294712] EV_ABS ABS_MT_TRACKING_ID 00002e0a
[ 277099.294712] EV_KEY BTN_TOOL_FINGER DOWN
[ 277099.294712] EV_ABS ABS_MT_POSITION_X 00000361
[ 277099.294712] EV_ABS ABS_MT_POSITION_Y 0000056a
[ 277099.294712] EV_ABS ABS_MT_TOUCH_MAJOR 00000005
[ 277099.294712] EV_ABS ABS_MT_TOUCH_MINOR 00000005
[ 277099.294712] EV_SYN SYN_REPORT 00000000
[ 277099.335669] EV_ABS ABS_MT_TRACKING_ID ffffffff
[ 277099.335669] EV_KEY BTN_TOOL_FINGER UP
[ 277099.335669] EV_SYN SYN_REPORT 00000000
而后这一些输入事件会被输入系统服务来读取以及分发,直至最后被处理消费。
InputManagerService 的启动
Android 的输入系统服务类为 InputManagerService,其在启动时会进一步在 native 层启动两个核心的功能模版 InputReader 和 InputDispatcher。InputReader 用于输入事件的读取,而 InputDispatcher 用于输入事件的分发。
整体关系如下:
InputManagerService
服务启动整体流程的时序图如下:
frameworks/base/services/core/java/com/android/server/input/
- InputManagerService.java
frameworks/base/services/core/jni/
- com_android_server_input_InputManagerService.cpp
frameworks/native/services/inputflinger/
- InputManager.cpp
frameworks/native/services/inputflinger/dispatcher/
- InputDispatcher.cpp
frameworks/native/services/inputflinger/reader/
- InputReader.cpp
InputReader 读取设备输入事件
InputReader
内会启动一个同名的 Looper 线程专用于循环来读取设备的输入事件:
status_t InputReader::start() {
if (mThread) {
return ALREADY_EXISTS;
}
mThread = std::make_unique<InputThread>(
"InputReader", [this]() { loopOnce(); }, [this]() { mEventHub->wake(); });
return OK;
}
loopOnce()
为每次循环执行的方法,其内会通过 EventHub 类来扫描设备的输入事件(/dev/input
目录下),整理调用流程如下:
frameworks/native/services/inputflinger/reader/
- EventHub.h
- EventHub.cpp
其后,InputReader 会将同一设备的原始事件 RawEvent
打包一起,然后根据事件类型,找到合适的 InputMapper 来进一步处理。
InputMapper 根据设备类型分成几类,下面列举一些常见的 InputMapper:
InputMapper | 说明 |
---|---|
KeyboardInputMapper | 键盘类设备 |
CursorInputMapper | 鼠标类设备 |
MultiTouchInputMapper | 多点触摸屏类设备 |
SingleTouchInputMapper | 单点触摸屏类设备 |
对于触摸事件而言,由 TouchInputMappper(Multi or Single) 作处理,并传递到 InputDispatcher 中,整体流程如下:
frameworks/native/services/inputflinger/reader/mapper/
- TouchInputMapper.cpp
frameworks/native/services/inputflinger/include/
- InputListener.h
frameworks/native/services/inputflinger/
- InputListener.cpp
frameworks/native/services/inputflinger/dispatcher/
- Entry.h
InputDispatcher 分发事件
InputDispatcher 顾名思义,用于分发从 InputReader 中读取出来的事件,其在启动同样会开启一个 Looper 线程循环执行事件的分发:
status_t InputDispatcher::start() {
if (mThread) {
return ALREADY_EXISTS;
}
mThread = std::make_unique<InputThread>(
"InputDispatcher", [this]() { dispatchOnce(); }, [this]() { mLooper->wake(); });
return OK;
}
每次触摸事件分发的处理中,会通过 findTouchedWindowTargetsLocked()
方法找到可接受事件的目标窗口,做进一步的派发,整体流程如下:
frameworks/native/include/input/
- InputWindow.h
- InputWindow.cpp
查找响应事件的目标窗口
那 InputDispatcher 是如果查找到应该处理事件的目标窗口呢?首先看下设备中的各个窗口(包括系统和应用端的)是如何和 InputDispatcher 关联起来的。
对 Android 系统而言,各类型的窗口统一通过系统服务 WindowManagerService 来管理增删改等操作的。以窗口新增为例,每当有新的窗口添加时,以 addWindow()
为出发点,会逐步通知到 InputDispatcher 更新窗口的信息,整体流程如下:
frameworks/base/services/core/java/com/android/server/wm/
- WindowManagerService.java
- InputMonitor.java
frameworks/native/services/surfaceflinger/
- SurfaceFlinger.cpp
frameworks/native/include/input/
- IInputFlinger.h
- InputWindow.h
其中,每个 InputWindowInfo 实例包含了一个窗口的 displayId、token、布局参数等关键信息:
struct InputWindowInfo {
...
sp<IBinder> token; // 窗口 token
int32_t id = -1;
std::string name;
int32_t layoutParamsFlags = 0;
int32_t layoutParamsType = 0;
...
Region touchableRegion; // 可触摸区域
bool visible = false; // 窗口是否可见
bool canReceiveKeys = false;
bool hasFocus = false;
bool hasWallpaper = false;
bool paused = false;
int32_t ownerPid = -1;
int32_t ownerUid = -1;
int32_t inputFeatures = 0;
int32_t displayId = ADISPLAY_ID_NONE; // displayId
int32_t portalToDisplayId = ADISPLAY_ID_NONE;
InputApplicationInfo applicationInfo;
...
}
然后,回到 InputDispatcher 内,其在 findTouchedWindowTargetsLocked()
方法中会通过一系列的判断条件,从全部的窗口信息中找到应该接受该触摸事件的目标窗口,并分发事件到该窗口上。判断的条件主要有以下几点:
- displayId 一致;
- 窗口当前时可见的;
- 目标事件的触摸点坐标在窗口可触控区域内;
在 InputDispatcher 找到分发事件的目标窗口信息后,要怎么实现通信把具体的事件数据发送到该窗口上呢?
由于 InputDispatcher 线程运行在系统服务所在的 system_server 进程,每个应用的窗口所操控的 RootView 运行在各自的应用进程,这里就需要进行跨进程通信。这里,系统采用了 socket 的通信方式。
InputChannel 通信渠道的建立
双端 InputChannel 的创建
首先,在每个窗口视图创建时,会在应用进程的 ViewRootImpl setView()
方法中创建一个 InputChannel
对象,该对象已实现可序列化,然后与 WindowManagerService 的 Binder 跨进程通信中传递:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
...
// 创建应用进程的 InputChannel 对象
InputChannel inputChannel = null;
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
inputChannel = new InputChannel();
}
...
try {
// Binder 跨进程通信添加窗口,inputChannel 实质内容在 system_server 进程填充
res = mWindowSession.addToDisplayAsUser(..., inputChannel, ...);
} ...
}
然后,system_server
进程中,WindowManagerService 会进一步调用并创建返回两个 InputChannel
对象,分别对应 client 端和 server 端,并把 client 的 native 对象数据转移到上一步 ViewRootImpl 创建的 InputChannel 对象中,整理流程如下:
frameworks/base/core/java/android/view/
- ViewRootImpl.java
- InputChannel.java
frameworks/base/services/core/java/com/android/server/wm/
- WindowState.java
frameworks/base/core/jni/
- android_view_InputChannel.cpp
frameworks/native/libs/input/
- InputTransport.cpp
流程的最后,在 InputChannel(native) 的 openInputChannelPair()
方法中,真正实现了 socket 对以及 InputChannel 对象的创建。代码实例如下:
status_t InputChannel::openInputChannelPair(const std::string& name,
sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) {
int sockets[2];
// 创建 socket 对
if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {
...
return result;
}
int bufferSize = SOCKET_BUFFER_SIZE; // 32KB
setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
sp<IBinder> token = new BBinder();
std::string serverChannelName = name + " (server)";
android::base::unique_fd serverFd(sockets[0]);
// 创建 server 端 InputChannel 对象,名称后缀为 (server)
outServerChannel = InputChannel::create(serverChannelName, std::move(serverFd), token);
std::string clientChannelName = name + " (client)";
android::base::unique_fd clientFd(sockets[1]);
// 创建 client 端 InputChannel 对象,名称后缀为 (client)
outClientChannel = InputChannel::create(clientChannelName, std::move(clientFd), token);
return OK;
}
在 client 和 server 的通信渠道 InputChannel
对象创建后,分别有以下绑定关系:
- InputChannel(client) :与窗口视图所在的应用进程的主线程绑定;
- InputChannel(server) :与 system_server 进程的 InputDispatcher 线程绑定;
server 端 InputChannel 的注册
我们先看 server 端 InputChannel
对象的注册,调用流程如下:
具体看 InputDispatcher 的 registerInputChannel()
方法实现:
status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel) {
{ // 获取锁
std::scoped_lock _l(mLock);
...
sp<Connection> connection = new Connection(inputChannel, false, mIdGenerator);
int fd = inputChannel->getFd();
mConnectionsByFd[fd] = connection;
mInputChannelsByToken[inputChannel->getConnectionToken()] = inputChannel;
// 将该 InputChannel 的 socket fd 添加到 InputDispatcher 的 Looper 中去
mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this);
} // 释放锁
mLooper->wake(); // connections 改变,唤醒 Looper
return OK;
}
这里把 socket server 端的 fd 通过 epoll 机制注册监听,回调方法为 handleReceiveCallback()
。
client 端 InputChannel 的注册
同理,client 端 InputChannel
的注册机制是类似的,在 ViewRootImpl 创建 InputChannel 对象并跨进程通过 WindowManagerService 填充 native 对象数据后,进一步创建 InputEventReceiver 对象用于输入事件的接受并把 InputChannel 的 fd 注册到当前应用主线程的 Looper 中去。
整体调用如下:
frameworks/base/core/java/android/view/
- InputEventReceiver.java
frameworks/base/core/jni/
- android_view_InputEventReceiver.cpp
至此,双端通信的模型边建立起来:
触摸事件分发至应用端窗口
回到 InputDispatcher 中的事件分发逻辑,在 diaptchEventLocked()
方法执行中,逐步调用,最终通过注册了 socket 实现的 InputChannel(server) 发送信息,通知应用进程窗口视图接受触摸事件:
在应用进程主线程处,NativeInputEventReceiver 会接受到信息的到来,并回调 handleEvent()
方法开始接手事件的处理,并逐步分发至窗口根视图 RootView 上:
在触摸事件分发完成后,会通过 InputChannel(client) 的 socket 发送 finished
信号信息回系统服务进程的 InputDispatcher 线程中通知当前输入事件已消费完毕,并开始下一个事件的分发处理:
可见,一个触摸事件分发的完整过程共有两次 socket 通信:
- 系统服务 InputDispatcher 线程 -> 应用主线程:发送新触摸事件信息
- 应用主线程 -> 系统服务 InputDispatcher 线程:发送触摸事件已被消费完成信号
至此,一个完整触摸事件的分发流程便完成。
总结
按时间线顺序,整个流程可归纳为:
- 设备启动时,系统进程启动服务 InputManagerService,并创建 InputReader 和 InputDispatcher 两个 Looper 线程;
- 在有新窗口创建时,更新窗口信息至 InputDispatcher;
- 同时创建应用进程和系统服务进程间的双端通信渠道 InputChannel 并通过 socket 实现通信;
- 屏幕硬件设备被触碰时,产生原始事件写入到
/dev/input/
目录下的设备节点中; - InputReader 线程扫描设备节点并读取设备输入事件,发送至 InputDispatcher 中;
- InputDispatcher 内找到需要处理事件的窗口信息,通过 socket 发送至目标应用的窗口;
- 目标应用窗口接受并分发、消费目标事件,再通过 socket 通知 InputDispatcher 事件已消费完成。