Android InputChannel socket 笔记

5 阅读6分钟

InputChannel的Socket实现,本质是基于socketpair()创建的“全双工、带边界”的Unix域套接字对。它的精巧之处在于:用Binder传FD完成“握手”,用Socket传数据完成“通信”——分工极其干净。

直接看源码的三个核心环节:创建、传输、监听与收发


🔌 一、创建环节:socketpair()的精确选型

这是所有InputChannel的“出生地”。代码在InputTransport.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=0Unix域只有一种协议,填0即可-
SO_SNDBUF/RCVBUF32KB可缓存数十个MotionEvent,防止短暂拥塞丢事件默认值可能过小,高采样率下易丢帧

一个历史细节:Android 4.1之前曾用共享内存 + 管道做传输,后全面切为socketpair。原因很简单——共享内存需要自己处理同步与边界,不如socketpair“开箱即用”。


📦 二、传输环节:如何把“一个端”送到客户端?(Binder传FD)

这是最反直觉但最精妙的一步:Server端创建了socket[0]和socket[1],但客户端进程如何拿到socket[1]?

答案通过Binder传递文件描述符。Linux内核支持在跨进程传递Binder数据时,将“当前进程的文件描述符”映射为目标进程的有效文件描述符

WMS中的关键代码(WindowManagerService.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):

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返回时):

// 从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

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

// ViewRootImpl.java
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper());

JNI层实现(android_view_InputEventReceiver.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)。

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;
};

设计意图

  1. TYPE_FINISHED 实现了“确认”机制:应用处理完事件后,必须通过InputConsumer.sendFinishedSignal()发回一个Finished消息。只有收到确认,Dispatcher才会从waitQueue移除该事件,继续发下一个
  2. 8字节对齐 + 固定字段 + 变长指针数组:MotionEvent可能带多个触摸点,pointerCount字段后面紧接变长的Pointer数组。这是带边界的变长协议——SOCK_SEQPACKET恰好完美适配(一次recv正好一个报文)。
  3. seq字段:每个事件携带自增序列号,用于匹配Finished避免了乱序确认

🧵 五、完整数据流时序(Debug视角)

假设触摸屏上报一个DOWN事件:

时序 | 进程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的事件           |

注意t10Finished信号必须由应用主动发送,不是内核自动完成。如果应用主线程卡死,这个Finished永远不会发,Dispatcher等待5秒后触发ANR。


📊 六、总结:InputChannel Socket实现的三层解读

层级实现技术本质设计意图
创建socketpair(AF_UNIX, SOCK_SEQPACKET)全双工、带边界、可靠一次触摸一个报文,天然对齐触摸事件模型
分发Binder传递FD(writeDupFileDescriptor跨进程移交文件描述符无需额外握手,创建即连接
监听Looper.addFd() → epollI/O多路复用主线程零额外开销,与UI绘制共用消息循环
协议InputMessage固定头+变长体带边界的二进制协议自描述、跨32/64位、低解析开销
确认TYPE_FINISHED反向消息显式ACK避免事件积压,实现背压与ANR监控