InputChannel的Socket实现,本质是基于socketpair()创建的"全双工、带边界"的Unix域套接字对。它的精巧之处在于:用Binder传FD完成"握手",用Socket传数据完成"通信"------分工极其干净。
直接看源码的三个核心环节:创建、传输、监听与收发。
🔌 一、创建环节:socketpair()的精确选型
这是所有InputChannel的"出生地"。代码在InputTransport.cpp:
cpp
status_t InputChannel::openInputChannelPair(const std::string& name,
sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) {
int sockets[2];
// ★★★ 核心:创建一对已连接的Unix域套接字 ★★★
if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {
return -errno;
}
// 设置收发缓冲区大小(32KB),避免高频触摸事件丢包
int bufferSize = SOCKET_BUFFER_SIZE; // 32 * 1024
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));
// 封装成InputChannel对象(持有fd + 调试用name)
outServerChannel = new InputChannel(name + " (server)", sockets[0]);
outClientChannel = new InputChannel(name + " (client)", sockets[1]);
return OK;
}
关键设计决策(此处需深刻理解):
| 参数/选项 | 为什么这么选 | 如果选别的会怎样 |
|---|---|---|
AF_UNIX |
同一主机跨进程通信,不走网络协议栈,纯内存拷贝 | AF_INET会经loopback,增加无意义开销 |
SOCK_SEQPACKET |
保序 + 消息边界 + 可靠传输------完美匹配触摸事件"一次触摸一个报文"的特性 | SOCK_STREAM:无边界,应用层需自己分包;SOCK_DGRAM:可能丢包、无拥塞控制 |
protocol=0 |
Unix域只有一种协议,填0即可 | - |
SO_SNDBUF/RCVBUF |
32KB可缓存数十个MotionEvent,防止短暂拥塞丢事件 | 默认值可能过小,高采样率下易丢帧 |
一个历史细节 :Android 4.1之前曾用共享内存 + 管道做传输,后全面切为socketpair。原因很简单------共享内存需要自己处理同步与边界,不如socketpair"开箱即用"。
📦 二、传输环节:如何把"一个端"送到客户端?(Binder传FD)
这是最反直觉但最精妙的一步:Server端创建了socket[0]和socket[1],但客户端进程如何拿到socket[1]?
答案 :通过Binder传递文件描述符 。Linux内核支持在跨进程传递Binder数据时,将"当前进程的文件描述符"映射为目标进程的有效文件描述符。
WMS中的关键代码(WindowManagerService.java):
java
// 1. 创建一对InputChannel
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
// 2. server端自己保留sockets[0](服务端)
win.setInputChannel(inputChannels[0]);
// 3. ★★★ 核心:将sockets[1]"塞进"Binder回传数据中 ★★★
inputChannels[1].transferTo(outInputChannel);
// transferTo最终调用JNI:parcel->writeDupFileDescriptor(inputChannel->getFd());
JNI层的实现(android_view_InputChannel.cpp):
cpp
static void android_view_InputChannel_nativeWriteToParcel(JNIEnv* env, jobject obj,
jobject parcelObj) {
Parcel* parcel = parcelForJavaObject(env, parcelObj);
// 写入标记:1表示有FD,0表示空
parcel->writeInt32(1);
// 写入debug用的name
parcel->writeString8(String8(inputChannel->getName().c_str()));
// ★★★ 核心API:将FD"复制一份"送给接收进程 ★★★
parcel->writeDupFileDescriptor(inputChannel->getFd());
}
客户端反序列化(ViewRootImpl收到Binder返回时):
cpp
// 从Parcel中读取FD
int rawFd = parcel->readFileDescriptor();
// ★★★ 必须dup!因为接收进程不能直接持有原FD(归属权问题)★★★
int dupFd = dup(rawFd);
InputChannel* inputChannel = new InputChannel(name.string(), dupFd);
为什么必须dup?
Binder传递FD时,内核会在目标进程创建新的文件描述符 ,指向同一个内核文件结构体 。如果不dup,直接parcel->readFileDescriptor()得到的FD可直接用。但源码中确实先读rawFd再dup------这是为了解耦生命周期:原rawFd可能在Parcel析构时关闭,dup后完全由InputChannel管理。
🎧 三、监听与收发环节:双方如何"等数据"?
3.1 服务端(InputDispatcher)侧监听
WMS注册服务端InputChannel时,InputDispatcher将其FD加入自己的Looper:
cpp
status_t InputDispatcher::registerInputChannel(...) {
int fd = inputChannel->getFd();
// 将connection保存,fd->Connection映射
mConnectionsByFd.add(fd, connection);
// ★★★ 核心:将FD加入epoll,回调handleReceiveCallback ★★★
mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this);
}
服务端主要监听两个方向:
- 写方向 :主动调用
InputPublisher.publishXxxEvent(),向socket写入InputMessage - 读方向 :等待客户端的
TYPE_FINISHED消息(确认消费),在handleReceiveCallback中处理
3.2 客户端(应用)侧监听
ViewRootImpl拿到Binder传回的InputChannel后,创建WindowInputEventReceiver:
cpp
// ViewRootImpl.java
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper());
JNI层实现(android_view_InputEventReceiver.cpp):
cpp
status_t NativeInputEventReceiver::initialize() {
int fd = mInputConsumer.getChannel()->getFd();
// ★★★ 同样:将FD加入应用主线程的Looper(epoll) ★★★
mMessageQueue->getLooper()->addFd(fd, 0, ALOOPER_EVENT_INPUT, this, NULL);
return OK;
}
关键对称性 :双方都用Looper的epoll监听FD。这意味着:
- 应用主线程在
epoll_wait上休眠 - 服务端写入socket → 内核使客户端FD可读 → epoll唤醒 → 回调
handleEvent - 全程无额外线程、无轮询
📨 四、消息格式:InputMessage------严格的二进制协议
Socket传输的不是原始MotionEvent,而是精心设计的InputMessage结构体 (定义在InputTransport.h)。
cpp
struct InputMessage {
struct Header {
uint32_t type; // TYPE_KEY, TYPE_MOTION, TYPE_FINISHED
uint32_t padding; // 保证body 8字节对齐
} header;
union Body {
struct Key { ... } key; // 按键事件
struct Motion { ... } motion; // 触摸/轨迹球事件
struct Finished { // ★★★ 双向通信的关键 ★★★
uint32_t seq; // 原事件的序列号
bool handled; // 应用是否消费
} finished;
} __attribute__((aligned(8))) body;
};
设计意图:
TYPE_FINISHED实现了"确认"机制 :应用处理完事件后,必须通过InputConsumer.sendFinishedSignal()发回一个Finished消息。只有收到确认,Dispatcher才会从waitQueue移除该事件,继续发下一个。- 8字节对齐 + 固定字段 + 变长指针数组 :MotionEvent可能带多个触摸点,
pointerCount字段后面紧接变长的Pointer数组。这是带边界的变长协议------SOCK_SEQPACKET恰好完美适配(一次recv正好一个报文)。 - seq字段 :每个事件携带自增序列号,用于匹配
Finished。避免了乱序确认。
🧵 五、完整数据流时序(Debug视角)
假设触摸屏上报一个DOWN事件:
scss
时序 | 进程A (system_server) | 进程B (应用)
-----|----------------------------------------|-----------------------------------
t1 | InputDispatcher.selectTargetWindow() |
t2 | → Connection中找到InputChannel |
t3 | → InputPublisher.publishMotionEvent() |
t4 | → socket[0]写入InputMessage |
t5 | | epoll_wait返回(socket[1]可读)
t6 | | NativeInputEventReceiver.handleEvent()
t7 | | → InputConsumer.consume()
t8 | | → JNI回调Java InputEventReceiver
t9 | | View.dispatchTouchEvent()
t10 | | InputConsumer.sendFinishedSignal(true)
t11 | | → socket[1]写入Finished
t12 | epoll_wait返回(socket[0]可读) |
t13 | InputDispatcher.handleReceiveCallback()|
t14 | → 从waitQueue移除该seq的事件 |
注意t10 :Finished信号必须由应用主动发送,不是内核自动完成。如果应用主线程卡死,这个Finished永远不会发,Dispatcher等待5秒后触发ANR。
📊 六、总结:InputChannel Socket实现的三层解读
| 层级 | 实现 | 技术本质 | 设计意图 |
|---|---|---|---|
| 创建 | socketpair(AF_UNIX, SOCK_SEQPACKET) |
全双工、带边界、可靠 | 一次触摸一个报文,天然对齐触摸事件模型 |
| 分发 | Binder传递FD(writeDupFileDescriptor) |
跨进程移交文件描述符 | 无需额外握手,创建即连接 |
| 监听 | Looper.addFd() → epoll | I/O多路复用 | 主线程零额外开销,与UI绘制共用消息循环 |
| 协议 | InputMessage固定头+变长体 |
带边界的二进制协议 | 自描述、跨32/64位、低解析开销 |
| 确认 | TYPE_FINISHED反向消息 |
显式ACK | 避免事件积压,实现背压与ANR监控 |