Android InputChannel socket 笔记

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

设计意图

  1. TYPE_FINISHED 实现了"确认"机制 :应用处理完事件后,必须通过InputConsumer.sendFinishedSignal()发回一个Finished消息。只有收到确认,Dispatcher才会从waitQueue移除该事件,继续发下一个
  2. 8字节对齐 + 固定字段 + 变长指针数组 :MotionEvent可能带多个触摸点,pointerCount字段后面紧接变长的Pointer数组。这是带边界的变长协议------SOCK_SEQPACKET恰好完美适配(一次recv正好一个报文)。
  3. 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的事件           |

注意t10Finished信号必须由应用主动发送,不是内核自动完成。如果应用主线程卡死,这个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监控
相关推荐
城东米粉儿2 小时前
Android View体系 笔记
android
城东米粉儿2 小时前
Android Messenger 笔记
android
城东米粉儿2 小时前
Android消息机制 笔记
android
奥陌陌2 小时前
用SurfaceControlViewHost 跨进程显示view
android
诸神黄昏EX2 小时前
Android SystemServer 系列专题【篇五:SystemConfig系统功能配置】
android
城东米粉儿2 小时前
Android IdleHandler 优化笔记
android
城东米粉儿2 小时前
Android Binder 笔记
android
Android系统攻城狮2 小时前
Android tinyalsa深度解析之pcm_get_available_min调用流程与实战(一百一十六)
android·pcm·tinyalsa·音频进阶·音频性能实战
lxysbly2 小时前
nds模拟器安卓版官网
android