☰
9、事件队列与缓存:InputReader将事件放入QueuedInputListener,批量处理机制
2026/10/8 7:24:43 网站建设 项目流程

9.1 为什么需要批量处理?

我个人习惯,在分析源码前先想清楚设计意图。批量处理的好处,其实就三点:

  • 减少IPC次数:跨进程通信是昂贵的,攒一批发一次,能省不少开销
  • 保证事件顺序:同一批次的事件,在同一个循环里处理,不会出现乱序
  • 降低抖动:避免频繁唤醒InputDispatcher,减少调度延迟

我在项目中遇到过一个问题:某个定制设备上,触摸滑动总是卡顿。后来发现就是事件没做批量处理,每个MotionEvent都单独发,导致InputDispatcher忙不过来。嗯,加了批量后,问题就解决了。

9.2 QueuedInputListener的结构

先看代码。QueuedInputListener定义在InputListener.h里,其实很简单:

// frameworks/native/services/inputflinger/include/InputListener.h class QueuedInputListener : public InputListenerInterface { public: explicit QueuedInputListener(const sp<InputListenerInterface>& innerListener); virtual void notifyKeyEvent(const NotifyKeyEventArgs* args); virtual void notifyMotionEvent(const NotifyMotionEventArgs* args); // ... 其他通知方法 void flush(); private: sp<InputListenerInterface> mInnerListener; std::vector<NotifyArgs> mArgsQueue; };

看到没?核心就两个成员:

  • mInnerListener:真正的下游监听器,也就是InputDispatcher
  • mArgsQueue:一个vector,用来暂存事件

当InputReader处理完一个事件,调用notifyKeyEvent或notifyMotionEvent时,QueuedInputListener并不会立刻转发,而是把事件参数拷贝到队列里。

9.3 事件入队的过程

我们来看一个具体的例子。假设InputReader处理了一个触摸事件:

// frameworks/native/services/inputflinger/reader/InputReader.cpp void InputReader::processEventsLocked(...) { // ... 处理原始事件,生成NotifyMotionArgs // 注意这里!不是直接调用mQueuedListener->notifyMotionEvent // 而是先入队 mQueuedListener->notifyMotionEvent(&args); }

那notifyMotionEvent内部做了什么?

// frameworks/native/services/inputflinger/InputListener.cpp void QueuedInputListener::notifyMotionEvent(const NotifyMotionEventArgs* args) { mArgsQueue.push_back(NotifyArgs(*args)); }

就一行代码——把参数拷贝到vector里。没有IPC,没有锁竞争,非常轻量。

小提示:这里有个细节,NotifyArgs是基类,NotifyKeyEventArgs、NotifyMotionEventArgs都是它的子类。vector里存的是基类对象,所以需要做一次拷贝构造。我建议你去看一下NotifyArgs的拷贝构造函数,它内部用了深拷贝,确保事件数据完整。

9.4 批量刷出的时机

事件攒够了,什么时候发出去?

答案是:每次InputReader循环结束的时候。

看这个关键函数:

// frameworks/native/services/inputflinger/reader/InputReader.cpp void InputReader::loopOnce() { // 1. 从设备读取原始事件 // 2. 处理事件,生成上层事件 // 3. 事件通过QueuedInputListener入队 // 4. 关键步骤:刷出队列 mQueuedListener->flush(); }

flush()的实现:

void QueuedInputListener::flush() { // 遍历队列,逐个转发给真正的监听器 for (const auto& args : mArgsQueue) { switch (args.getType()) { case NotifyArgs::Type::KEY: mInnerListener->notifyKeyEvent( static_cast<const NotifyKeyEventArgs*>(&args)); break; case NotifyArgs::Type::MOTION: mInnerListener->notifyMotionEvent( static_cast<const NotifyMotionEventArgs*>(&args)); break; // ... 其他类型 } } // 清空队列 mArgsQueue.clear(); }

这里有个重要的设计点:flush()是在InputReader线程里同步调用的。也就是说,事件转发给InputDispatcher时,InputReader线程会阻塞,直到InputDispatcher处理完这批事件。

注意:我曾经踩过一个坑。在某个低端设备上,InputDispatcher处理事件太慢,导致InputReader的flush()卡住,进而影响了下一个循环的读取。结果就是触摸响应变慢。后来通过调整批量大小和优先级,才缓解了这个问题。

9.5 批量大小的控制

你可能会问:一次批量处理多少个事件合适?

其实Android没有硬编码的批量大小限制。它取决于InputReader一次循环能读到多少原始事件。

举个例子:

  • 如果设备上报率是60Hz,一次循环可能只读到1-2个事件
  • 如果设备上报率是240Hz,一次循环可能读到4-5个事件
  • 如果设备突然爆发一批事件(比如快速滑动),一次循环可能读到10+个事件

所以批量大小是动态的,由硬件上报速率和系统负载共同决定。

场景典型批量大小说明
普通点击1-2个按下+抬起,通常分两次循环
慢速滑动2-3个手指移动较慢,事件间隔大
快速滑动5-10个手指快速移动,事件密集
游戏场景10-20个高刷新率屏幕+快速操作

9.6 为什么不用环形缓冲区?

看到vector,有些同学可能会问:为什么不用环形缓冲区?那样性能不是更好吗?

嗯,这个问题我当初也想过。后来看了源码才明白:

  • 事件数量不确定:环形缓冲区需要固定大小,但事件数量是动态的
  • 拷贝成本不高:NotifyArgs只是参数结构体,不是完整的事件对象,拷贝很快
  • 清空方便:vector的clear()直接重置size,不需要维护读写指针

说白了,这里用vector是够用且简单的选择。Android源码里很多地方都是这样,不追求极致性能,而是追求代码清晰和可维护性。

9.7 避坑指南

最后,分享几个我实际工作中遇到的坑:

  1. 不要跳过QueuedInputListener:有些定制ROM为了"优化",直接让InputReader调用InputDispatcher。结果就是事件乱序、丢事件。批量处理不是可有可无的,它是保证事件顺序的关键。
  2. 注意flush的线程安全:QueuedInputListener不是线程安全的。InputReader必须在自己的线程里调用flush,不能跨线程。我见过有人试图在另一个线程里手动触发flush,结果导致vector迭代器失效。
  3. 批量事件的内存管理:如果事件队列积压太多(比如InputDispatcher卡住了),mArgsQueue会不断增长。这时候要监控内存使用,必要时丢弃旧事件。Android 12以后加入了事件丢弃机制,就是应对这种情况的。

核心总结:QueuedInputListener是InputReader和InputDispatcher之间的缓冲区。它把零散的事件攒成一批,在每次循环结束时统一转发。这个设计减少了IPC次数,保证了事件顺序,是Input系统高性能的关键一环。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询