文章内容
- 1.1 事件归属由系统侧决定
- 1.2 为什么不能透传
- 2.1 从 setContentView 到 DecorView
- 2.2 WindowManager.addView 与 ViewRootImpl 的诞生
- 2.3 WindowSession:应用与 WMS 的跨进程通道
- 2.4 setView 中的 addToDisplay
- 2.5 WMS.addWindow:创建一对 socket
- 2.6 每个窗口都有独立管道
- 3.1 registerInputChannel 与 Connection
- 3.2 InputWindowHandle:窗口的可触摸信息
- 3.3 setInputWindows:同步窗口状态
- 3.4 findTouchedWindowAtLocked:不可触摸就是“不经过”
- 4.1 setView 尾部创建接收器
- 4.2 nativeInit:Java 壳与 native peer
- 4.3 setFdEvents 与 Looper.addFd
- 4.4 主线程 Looper 的 epoll 机制
- 5.1 handleEvent 与 consumeEvents
- 5.2 native 事件转成 Java 对象
- 5.3 dispatchInputEvent、onInputEvent 与入队
- 5.4 InputStage 与 ViewPostImeInputStage
- 5.5 Activity入口
- 5.6 Activity 提前消费会截断 View 树
- 6.1 进程内根 View 的统一观察
- 6.2 按键事件要区分系统策略
InputDispatcher 已经选中目标窗口,并把触摸事件写进对应的 socket。应用进程此时没有主动向系统轮询,它怎么及时接住事件,又怎么把一个 native 消息送到 Java 的 View 树?另一个常见疑问是:上层窗口没有消费事件,系统能否把同一事件继续交给下层窗口?
应用侧的完整链路由注册、监听、读取和分发组成。ViewRootImpl.setView 通过 WindowSession.addToDisplay 请求 WindowManagerService(WMS)创建一对 socket;服务端交给 InputDispatcher,客户端返回应用。WindowInputEventReceiver 把客户端 fd 注册到主线程 Looper 的 epoll;fd 可读时,native 层取出事件并构造 Java 对象;事件随后进入 InputStage,经 Activity 的窗口回调到达 View 树。
沿调用栈展开,这条路径从 ActivityThread.handleResumeActivity 进入 WindowManager.addView、WindowManagerGlobal.addView 和 ViewRootImpl.setView。窗口注册成功后,路径从 Binder 返回应用,转入 WindowInputEventReceiver、NativeInputEventReceiver 与 Looper;事件到达时再反向跨过 JNI,进入 enqueueInputEvent 和 View 分发。创建阶段与收取阶段发生在不同时间,把二者混在一起容易误以为 ViewRootImpl 会主动读取 InputDispatcher 的队列。实际连接点是 InputChannel 的 fd。
一、应用侧接收链路的两段式设计
1.1 事件归属由系统侧决定
InputManagerService(IMS)中的 InputDispatcher 掌握全部可接收输入的窗口信息。一次触摸到来时,它按显示设备、窗口层级、窗口标志和可触摸区域选择目标。应用接收链路处理的是“已经属于本窗口”的事件,不负责在多个窗口之间重新仲裁。
这形成了清楚的两段边界:系统侧确定目标并把事件写入管道,应用侧监听本窗口的管道并沿 View 层级分发。Activity、WebView、悬浮窗都要服从同一边界。
窗口层级和可触摸区域来自 WMS 持续同步的 InputWindowHandle,并非 InputDispatcher 临时查询每个 Activity。窗口移动、缩放或更新布局后,WMS 要把新的 frame 与 touchableRegion 交给输入系统。选窗使用这份系统侧快照;应用 View 树里的 onTouchEvent 返回值,在这一轮选窗时尚未产生,自然无法参与目标窗口判定。
图 1:事件接收全链路总览
1.2 为什么不能透传
每次 addView 都会为窗口建立一对独立的 InputChannel。InputDispatcher 只向命中的那一对管道写事件,其他窗口的客户端 fd 不会变为可读。因而,上层窗口在 dispatchTouchEvent 中返回 false,影响的是该窗口内部的分发结果;它不能让下层窗口凭空收到一份相同的事件。
这里还要区分“窗口内继续分发”和“窗口间重新分发”。前者发生在 DecorView、ViewGroup 与子 View 之间,返回值可以让父节点继续处理;后者需要 InputDispatcher 再选择另一条 Connection。常规触摸链没有把应用返回的 false 解释成重新选窗的协议,因此两个层面的消费语义不能互换。
悬浮球能说明这个差别。一个 50×50 的悬浮球拥有自己的窗口和管道。触点位于其可触摸区域时,事件进入悬浮球管道;触点落在右下角且悬浮球不是触摸模态窗口时,InputDispatcher 会选中下面的窗口。后一种情况是系统在选窗阶段跳过悬浮球,并非悬浮球收到事件后再“透传”。
二、ViewRootImpl 与 WMS 的通信:窗口如何建立接收管道
2.1 从 setContentView 到 DecorView
setContentView 负责构造和装配对象树。系统布局的根节点是 DecorView,应用传入的布局被放进 R.id.content 容器;测量、布局和绘制要等窗口加入 WindowManager 后由 ViewRootImpl 驱动。因此,应用布局不是 Activity 窗口的根节点。
图 2:DecorView 结构
2.2 WindowManager.addView 与 ViewRootImpl 的诞生
Activity 在 attach 阶段把 WindowManager 绑定给 Window。恢复 Activity 时,handleResumeActivity 取得 DecorView,经 WindowManager.addView 进入 WindowManagerGlobal.addView,后者创建 ViewRootImpl 并调用 setView。一次 addView 对应一个 ViewRootImpl;WindowManagerGlobal.mViews 保存进程内已加入窗口系统的根 View,这也是进程内统一埋点、点击回放或监听方案常用的观察入口。
setContentView 与 addView 的时间边界很重要。前者完成 DecorView 内部的对象关系,后者才把这棵树作为窗口内容提交给 WMS,并建立 ViewRootImpl、Surface 和输入通道等运行条件。调用 setContentView 后立即存在的 View 对象,不代表窗口已经具备接收系统输入的连接。
2.3 WindowSession:应用与 WMS 的跨进程通道
ViewRootImpl 持有的 mWindowSession 是 IWindowSession Binder 接口。进程第一次需要它时,WindowManagerGlobal 通过 IWindowManager.openSession 向 WMS 打开会话,之后同一进程复用该对象。
// 获取进程内复用的 WindowSession public static IWindowSession getWindowSession() { // 用类锁保护会话的延迟创建 synchronized (WindowManagerGlobal.class) { // 首次调用才向 WMS 打开会话 if (sWindowSession == null) { // Binder 调用可能抛出远程异常 try { // 取得输入法管理器及其客户端 InputMethodManager imm = InputMethodManager.getInstance(); // 取得 WMS 的 Binder 代理 IWindowManager windowManager = getWindowManagerService(); // 打开窗口会话并注册动画缩放回调 sWindowSession = windowManager.openSession( // 构造服务端可回调的 Binder 对象 new IWindowSessionCallback.Stub() { // 接收系统动画缩放变化 @Override // 把系统缩放值同步到当前进程 public void onAnimatorScaleChanged(float scale) { // 更新 ValueAnimator 的全局时长比例 ValueAnimator.setDurationScale(scale); } // 传入输入法客户端和输入上下文 }, imm.getClient(), imm.getInputContext()); // 捕获跨进程调用失败 } catch (RemoteException e) { // 记录会话打开失败 Log.e(TAG, "Failed to open window session", e); } } // 返回进程内复用的会话 return sWindowSession; } }Session 只做一次薄转发,调用的输入包含窗口 Binder、布局参数和一个用于接收结果的 outInputChannel。
// 暴露给应用进程的添加窗口接口 @Override // 接收窗口信息和输出参数 public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, // 继续声明显示和可见性参数 int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, // 声明其余输出参数,其中 InputChannel 由服务端填充 Rect outOutsets, InputChannel outInputChannel) { // 转交 WMS 完成窗口注册 return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, // 传递布局结果与输入通道外壳 outContentInsets, outStableInsets, outOutsets, outInputChannel); }2.4 setView 中的 addToDisplay
mInputChannel 传出前只是 Java 外壳,还没有可用的 native 指针。Binder 调用返回时,WMS 已把客户端通道经 transferTo 写入这个对象:native 层从 Parcel 反序列化出 fd 与 native 对象地址,赋给 Java 对象的 PTR(nativeReadFromParcel 是这条路的核心),之后 Java 层才能拿它做任何事。
为什么要由应用先传一个空对象?addToDisplay 是跨进程调用,WMS 创建的 native 通道不能靠普通 Java 引用直接跨越 Binder。输出参数承担了承接 Parcel 数据的角色:调用前保留应用侧对象身份,返回阶段再从 Parcel 恢复 native 资源并更新 PTR。ViewRootImpl 因而必须等 Binder 返回成功,再把通道交给事件接收器。
// 请求 WMS 添加窗口并填充输入通道 res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, // 传递窗口可见性和显示设备编号 getHostVisibility(), mDisplay.getDisplayId(), // 传递内容区与稳定区输出对象 mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, // 传递外扩区域和 InputChannel 外壳 mAttachInfo.mOutsets, mInputChannel); // 调用返回后,同一个 Java 外壳已经获得 native PTR图 3:addToDisplay 时序
2.5 WMS.addWindow:创建一对 socket
WMS 确认窗口需要输入通道后调用 openInputChannelPair。Java 方法完成参数检查,创建工作进入 native 层。
// 打开系统端与应用端配对的输入通道 public static InputChannel[] openInputChannelPair(String name) { // 拒绝空名称 if (name == null) { // 抛出参数异常 throw new IllegalArgumentException("name must not be null"); } // 调试模式记录通道名称 if (DEBUG) { // 输出打开日志 Slog.d(TAG, "opening input channel pair '" + name + "'"); } // 调用 native 方法创建通道对 return nativeOpenInputChannelPair(name); }native 实现采用 AF_UNIX 与 SOCK_SEQPACKET。它既能在本机进程间传递数据,也能保留消息边界,适合承载一条条输入消息。
// 定义输入通道对的创建方法status_t InputChannel::openInputChannelPair(const String8& name, // 接收服务端和客户端通道 sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) { // 保存 socketpair 返回的两个 fd int sockets[2]; // 创建 UNIX 域顺序包 socket 对 if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) { // 保存系统错误码 status_t result = -errno; // 记录创建失败及 errno ALOGE("channel '%s' Could not create socket pair. errno=%d", // 提交通道名和错误码 name.string(), errno); // 清理服务端输出参数 outServerChannel.clear(); // 清理客户端输出参数 outClientChannel.clear(); // 返回失败状态 return result; } // 采用输入系统预设的缓冲区大小 int bufferSize = SOCKET_BUFFER_SIZE; // 调整服务端发送缓冲区 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)); // 复制基础名称供服务端使用 String8 serverChannelName = name; // 添加服务端标识便于诊断 serverChannelName.append(" (server)"); // 用 sockets[0] 构造服务端通道 outServerChannel = new InputChannel(serverChannelName, sockets[0]); // 客户端沿用基础名称 String8 clientChannelName = name; // 用 sockets[1] 构造客户端通道 outClientChannel = new InputChannel(clientChannelName, sockets[1]); // 返回成功状态 return OK;}WMS 把 channel[0] 交给 WindowState 并注册到输入管理服务,把 channel[1] 转移到应用传来的外壳。
// 确认调用方需要通道且窗口未禁用输入通道 if (outInputChannel != null && (attrs.inputFeatures // 检查 NO_INPUT_CHANNEL 标志 & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { // 用窗口 token 生成便于追踪的名称 String name = win.makeInputChannelName(); // 创建服务端和客户端通道 InputChannel[] inputChannels = InputChannel.openInputChannelPair(name); // WindowState 持有服务端通道 win.setInputChannel(inputChannels[0]); // Java 层释放服务端包装对象 inputChannels[0].dispose(); // 把客户端通道转移给应用传来的输出对象,随 Binder 返回 inputChannels[1].transferTo(outInputChannel); // 向输入管理服务登记服务端通道和窗口句柄 mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle); }2.6 每个窗口都有独立管道
一次 addView 创建一对通道。三个 Activity 窗口与一个悬浮球会得到四对 socket:服务端一端由 InputDispatcher 持有,客户端一端由相应的 ViewRootImpl 持有。这个一一对应关系隔离了窗口输入,也给“不能透传”提供了结构上的解释。
管道的“服务端”和“客户端”描述两端职责,不表示所有读写都固定为单向。InputDispatcher 用 InputPublisher 向应用发布事件,应用消费后还要沿通道报告 finished 状态。Connection 中的 outboundQueue 与 waitQueue 分别记录待发布事件、已经发布但尚未确认的事件,系统可据此维护分发进度和异常状态。
图 4:多窗口多管道
三、View 信息的注册:让 InputDispatcher 认识窗口
3.1 registerInputChannel 与 Connection
管道创建完成还不够。InputDispatcher 需要知道 fd 对应哪个窗口以及怎样维护待发送、待确认的事件,registerInputChannel 完成这项登记。
// 注册可作为输入目标的通道 public void registerInputChannel(InputChannel inputChannel, // 同时传入描述窗口的句柄 InputWindowHandle inputWindowHandle) { // 拒绝空通道 if (inputChannel == null) { // 抛出参数异常 throw new IllegalArgumentException("inputChannel must not be null."); } // 进入 native InputDispatcher 完成登记 nativeRegisterInputChannel(mPtr, inputChannel, inputWindowHandle, false); }native 层创建 Connection,并建立 fd → connection 映射。这里不会再创建管道;Connection 只是给已有通道补充分发状态。
// 注册已有通道并绑定窗口信息status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel, // 接收窗口句柄和监控标志 const sp<InputWindowHandle>& inputWindowHandle, bool monitor) { // 加锁保护连接集合 AutoMutex _l(mLock); // 拒绝重复注册 if (getConnectionIndexLocked(inputChannel) >= 0) { // 记录重复注册的通道名 ALOGW("Attempted to register already registered input channel '%s'", // 提交通道名称 inputChannel->getName().string()); // 返回参数错误 return BAD_VALUE; } // 为已有通道创建分发连接 sp<Connection> connection = new Connection(inputChannel, inputWindowHandle, monitor); // 取得通道 fd int fd = inputChannel->getFd(); // 建立 fd 到连接的索引 mConnectionsByFd.add(fd, connection); // 判断是否属于监控通道 if (monitor) { // 单独保存监控通道 mMonitoringChannels.push(inputChannel); } // 监听对端回执等可读消息 mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this); // 唤醒 Looper 处理集合变化 mLooper->wake(); // 返回成功 return OK;}Connection 保存发布器、输入状态、出站队列和等待回执队列。它是 InputDispatcher 面向单条通道的运行时状态。
// 管理单条输入通道的分发状态class Connection : public RefBase {// 以下成员只供类及子类访问protected: // 定义连接生命周期状态 enum Status { // 通道可正常通信 STATUS_NORMAL, // 通道发生不可恢复错误 STATUS_BROKEN, // 通道已注销但对象尚待清理 STATUS_ZOMBIE };// 以下成员对调用方公开public: // 持有输入通道 sp<InputChannel> inputChannel; // 持有关联窗口句柄 sp<InputWindowHandle> inputWindowHandle; // 标记监控连接 bool monitor; // 向通道写入事件 InputPublisher inputPublisher; // 跟踪按键和触摸状态 InputState inputState; // 保存尚未发布的事件 Queue<DispatchEntry> outboundQueue; // 保存已发布但未确认的事件 Queue<DispatchEntry> waitQueue; // 记录通道当前生命周期状态 Status status;};3.2 InputWindowHandle:窗口的可触摸信息
InputChannel 回答“往哪里写”,InputWindowHandle 回答“该不该写”。其 InputWindowInfo 保存 frame、touchableRegion、可见性、焦点、层级、所属进程、输入特性和 displayId 等数据。
// 表示输入系统侧的窗口句柄class InputWindowHandle : public RefBase {// 暴露窗口查询接口public: // 关联应用级输入句柄 const sp<InputApplicationHandle> inputApplicationHandle; // 返回当前窗口信息 inline const InputWindowInfo* getInfo() const { return mInfo; } // 返回窗口关联的输入通道 inline sp<InputChannel> getInputChannel() const { // 信息有效时取通道,否则返回空 return mInfo ? mInfo->inputChannel : NULL; } // 返回窗口名或无效占位名 inline String8 getName() const { return mInfo ? mInfo->name : String8("<invalid>"); }// 限制构造、析构与更新接口的访问范围protected: // 构造时绑定应用句柄 InputWindowHandle(const sp<InputApplicationHandle>& inputApplicationHandle); // 析构时释放相关资源 virtual ~InputWindowHandle(); // 由子类同步 Java 侧的最新状态 virtual bool updateInfo() = 0; // 释放缓存信息 void releaseInfo(); // 保存窗口信息指针 InputWindowInfo* mInfo;};3.3 setInputWindows:同步窗口状态
WMS 创建 WindowState 时准备对应的 InputWindowHandle,并在布局、尺寸和层级变化后更新信息。setInputWindows 把窗口句柄列表同步给 InputDispatcher,同时处理焦点窗口与 hover 窗口变化。通道注册解决连接索引,窗口列表则提供每次选窗需要的动态状态,两者职责不同。
这一拆分避免把静态连接与频繁变化的窗口几何信息绑在一起。fd 注册后通常可以继续使用;窗口的 frame、焦点或可触摸区域却可能随布局反复变化。InputDispatcher 通过 Connection 找到可写通道,通过 mWindowHandles 判断本轮事件应选哪个窗口。两份数据最终在被选中的 InputWindowHandle 及其 InputChannel 上汇合。
3.4 findTouchedWindowAtLocked:不可触摸就是“不经过”
InputDispatcher 从高层窗口向低层遍历。显示设备不匹配时跳过;窗口是 touch modal,或者坐标命中 touchableRegion 时,当前窗口成为目标。
FLAG_NOT_TOUCHABLE 表示窗口不接受触摸,FLAG_NOT_TOUCH_MODAL 允许窗口区域外的触点继续参与后续窗口匹配。代码把两个标志合并计算 isTouchModal:没有设置它们时,窗口按触摸模态处理;否则还要检查坐标是否位于可触摸区域。可见窗口不一定能接收当前触点,层级靠前也不等于必定截获屏幕上的全部事件。
// 按显示设备和坐标寻找目标窗口sp<InputWindowHandle> InputDispatcher::findTouchedWindowAtLocked(int32_t displayId, // 接收触摸坐标 int32_t x, int32_t y) { // 取得候选窗口数量 size_t numWindows = mWindowHandles.size(); // 按当前列表的层级顺序遍历 for (size_t i = 0; i < numWindows; i++) { // 取出候选窗口句柄 sp<InputWindowHandle> windowHandle = mWindowHandles.itemAt(i); // 读取用于决策的窗口信息 const InputWindowInfo* windowInfo = windowHandle->getInfo(); // 只检查目标显示设备上的窗口 if (windowInfo->displayId == displayId) { // 根据窗口标志计算触摸模态属性 bool isTouchModal = (windowInfo->flags // 同时检查非模态和不可触摸标志 & (InputWindowInfo::FLAG_NOT_TOUCH_MODAL // 合并不可触摸标志 | InputWindowInfo::FLAG_NOT_TOUCHABLE)) == 0; // 模态窗口或坐标命中可触摸区域时选中 if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) { // 返回目标窗口 return windowHandle; } } } // 没有候选窗口命中 return NULL;}图 5:注册与分发决策
四、监听管道:WindowInputEventReceiver 与主线程 epoll
4.1 setView 尾部创建接收器
addToDisplay 成功后,ViewRootImpl 才能用已经获得 PTR 的 mInputChannel 创建 WindowInputEventReceiver。同一阶段还搭建了 InputStage 链,为后续 Java 分发准备入口。
// 确认 WMS 已返回输入通道 if (mInputChannel != null) { // 检查 View 是否要接管原始输入队列 if (mInputQueueCallback != null) { // 创建输入队列 mInputQueue = new InputQueue(); // 通知 View 输入队列已经可用 mInputQueueCallback.onInputQueueCreated(mInputQueue); } // 创建绑定主线程 Looper 的接收器 mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, // 使用当前线程的 Looper Looper.myLooper()); } // 把 ViewRootImpl 设为根 View 的父节点 view.assignParent(this); // 省略中间无关源码 ... // 创建合成事件阶段 mSyntheticInputStage = new SyntheticInputStage(); // 创建最终进入 View 树的阶段 InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage, // 设置跟踪名称 "aq:view-post-ime:" + counterSuffix); // 创建 native post-IME 阶段 InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage, // 设置跟踪名称 "aq:native-post-ime:" + counterSuffix); // 创建早期 post-IME 阶段 InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage, // 设置跟踪名称 "aq:early-post-ime:" + counterSuffix); // 创建输入法阶段 InputStage imeStage = new ImeInputStage(earlyPostImeStage, // 设置跟踪名称 "aq:ime:" + counterSuffix); // 创建 native pre-IME 阶段 InputStage nativePreImeStage = new NativePreImeInputStage(imeStage, // 设置跟踪名称 "aq:native-pre-ime:" + counterSuffix); // 保存完整管道入口 mFirstInputStage = nativePreImeStage; // 保存 post-IME 管道入口 mFirstPostImeInputStage = earlyPostImeStage;4.2 nativeInit:Java 壳与 native peer
InputEventReceiver 保存通道和 Looper 的 MessageQueue,nativeInit 创建与 Java 对象配对的 NativeInputEventReceiver。弱引用避免 native peer 阻止 Java 接收器回收。
// 构造绑定通道与 Looper 的接收器 public InputEventReceiver(InputChannel inputChannel, Looper looper) { // 拒绝空输入通道 if (inputChannel == null) { // 抛出通道参数异常 throw new IllegalArgumentException("inputChannel must not be null"); } // 拒绝空 Looper if (looper == null) { // 抛出 Looper 参数异常 throw new IllegalArgumentException("looper must not be null"); } // 保存客户端输入通道 mInputChannel = inputChannel; // 保存 Looper 对应的消息队列 mMessageQueue = looper.getQueue(); // 创建 native peer 并保存指针 mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this), // 把通道和消息队列传给 native 层 inputChannel, mMessageQueue); }4.3 setFdEvents 与 Looper.addFd
native peer 初始化时监听 ALOOPER_EVENT_INPUT。setFdEvents(0) 则在释放阶段移除 fd。
// 初始化 native 接收器status_t NativeInputEventReceiver::initialize() { // 监听输入通道的可读事件 setFdEvents(ALOOPER_EVENT_INPUT); // 返回初始化成功 return OK;} // 更新 fd 监听事件集合void NativeInputEventReceiver::setFdEvents(int events) { // 仅在事件集合改变时操作 Looper if (mFdEvents != events) { // 保存新的事件集合 mFdEvents = events; // 从 InputConsumer 取得通道 fd int fd = mInputConsumer.getChannel()->getFd(); // 非零集合表示注册或更新监听 if (events) { // 把 fd 和回调加入主线程 Looper mMessageQueue->getLooper()->addFd(fd, 0, events, handleEventCallback, this); // 零集合表示停止监听 } else { // 从 Looper 移除 fd mMessageQueue->getLooper()->removeFd(fd); } }}4.4 主线程 Looper 的 epoll 机制
Looper::addFd 把 fd、事件集合、回调和私有数据封装为 Request。重建 epoll 集合后,主线程的 nativePollOnce 最终阻塞在 epoll_wait;InputDispatcher 写入客户端对应的 socket 后,等待结束并回调 handleEvent。它与 Handler 消息共享同一个 Looper,不需要额外接收线程。
epoll 负责通知“哪个 fd 已就绪”,不负责理解 MotionEvent。handleEvent 被唤醒后,InputConsumer 才从 socket 读取协议消息并还原输入对象。这样,线程等待机制、通道协议解析和 Java 事件分发保持分层:Looper 管就绪通知,InputConsumer 管消息,ViewRootImpl 管应用窗口内部的处理顺序。
// 接收 C 风格回调的重载int Looper::addFd(int fd, int ident, int events, Looper_callbackFunc callback, void* data) { // 有回调时包装为 SimpleLooperCallback return addFd(fd, ident, events, callback ? new SimpleLooperCallback(callback) : NULL, data);} // 接收 LooperCallback 对象的核心实现int Looper::addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data) { // 检查空回调是否被允许 if (!callback.get()) { // 当前 Looper 禁止无回调请求时拒绝注册 if (!mAllowNonCallbacks) { // 记录非法空回调 ALOGE("Invalid attempt to set NULL callback but not allowed for this looper."); // 返回注册失败 return -1; } // 无回调时要求非负 ident if (ident < 0) { // 记录非法 ident ALOGE("Invalid attempt to set NULL callback with ident < 0."); // 返回注册失败 return -1; } // 有回调时进入统一回调模式 } else { // 固定使用 POLL_CALLBACK 标识 ident = POLL_CALLBACK; } // 加锁保护请求集合 AutoMutex _l(mLock); // 创建 fd 请求对象 Request request; // 保存 fd request.fd = fd; // 保存标识 request.ident = ident; // 保存关注的事件 request.events = events; // 保存回调对象 request.callback = callback; // 保存调用方数据 request.data = data; // 加入请求集合,供 epoll 重建使用 mRequests.push(request); // 唤醒 Looper 处理新请求 wake(); // 返回注册成功 return 1;}图 6:接收端监听
五、读到事件并进入 View 树
5.1 handleEvent 与 consumeEvents
fd 可读触发 handleEvent,随后 consumeEvents 让 InputConsumer 从 socket 取消息并解析为 native InputEvent。读取动作发生在应用进程的主线程 Looper 回调中。
handleEvent 是注册在 Looper 上的回调:对端关闭或出错时返回 0 让 Looper 移除该 fd;可读时调用 consumeEvents 并检查异常。返回值告诉 Looper 是否继续监听这个 fd。
// Looper 检测到 fd 可读、出错或挂起时的统一回调int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) { // 对端关闭通道或发生错误时不再监听该 fd if (events & (ALOOPER_EVENT_ERROR | ALOOPER_EVENT_HANGUP)) { // 返回 0 表示让 Looper 移除这个 fd 的回调 return 0; } // 有可读事件说明系统侧写入了新消息 if (events & ALOOPER_EVENT_INPUT) { // 取得当前线程的 JNIEnv JNIEnv* env = AndroidRuntime::getJNIEnv(); // 消费通道中的事件并回调 Java 层 dispatchInputEvent status_t status = consumeEvents(env, false /*consumeBatches*/, -1, NULL); // 把 native 侧捕获的异常转成 Java 异常抛出 mMessageQueue->raiseAndClearException(env, "handleReceiveCallback"); // 正常或内存不足暂缓时继续监听,其余错误移除回调 return status == OK || status == NO_MEMORY ? 1 : 0; } // 其他事件类型保持监听 return 1;}5.2 native 事件转成 Java 对象
按键通过 android_view_KeyEvent_fromNative 转成 KeyEvent;触摸通过 android_view_MotionEvent_obtainAsCopy 从对象池取得 MotionEvent,减少持续移动时的对象分配。MOVE 还可以按帧批量消费。
// 从通道消费事件并转换为 Java 对象status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, // 接收批处理参数与输出标记 bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) { // 省略 InputConsumer 读取事件的代码 ... // 声明 Java 事件引用 jobject inputEventObj; // 按 native 事件类型转换 switch (inputEvent->getType()) { // 处理按键事件 case AINPUT_EVENT_TYPE_KEY: { // 构造 Java KeyEvent inputEventObj = android_view_KeyEvent_fromNative(env, // 转为 native KeyEvent static_cast<KeyEvent*>(inputEvent)); break; } // 处理触摸事件 case AINPUT_EVENT_TYPE_MOTION: { // 转为 native MotionEvent MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent); // 标记可按帧批量消费的 MOVE if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) { // 向调用方报告已消费批次 *outConsumedBatch = true; } // 从对象池取得 Java MotionEvent 副本 inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent); break; } // 防御未知事件类型 default: // 输入消费者不应产生其他类型 assert(false); // 清空 Java 引用 inputEventObj = NULL; break; } // 成功生成 Java 对象后回调 Java 层 if (inputEventObj) { // 调用 dispatchInputEvent env->CallVoidMethod(receiverObj.get(), // 传入方法、序号和事件对象 gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj); // 检查 Java 回调异常 if (env->ExceptionCheck()) { // 记录分发异常 ALOGE("Exception dispatching input event."); // 禁止继续执行回调 skipCallbacks = true; } // 释放 JNI 局部引用 env->DeleteLocalRef(inputEventObj); } // 省略后续收尾代码 ...}5.3 dispatchInputEvent、onInputEvent 与入队
JNI 调用 Java InputEventReceiver.dispatchInputEvent 后,虚方法落到 WindowInputEventReceiver.onInputEvent。普通事件立即入队处理;批量事件根据非缓冲模式或 vsync 调度处理。
dispatchInputEvent 是 Java 层的入口方法,它记录 native 序号与事件对象的映射(finishInputEvent 回执要靠这个映射找到对应事件),然后转交给子类实现的 onInputEvent。
// native 层通过 JNI 回调的入口,应用代码不应直接调用 @SuppressWarnings("unused") private void dispatchInputEvent(int seq, InputEvent event) { // 记录 native 序号与事件对象的映射,供 finishInputEvent 回执使用 mSeqMap.put(event.getSequenceNumber(), seq); // 转交给子类实现(如 WindowInputEventReceiver.onInputEvent) onInputEvent(event); }这里的“立即”表示在当前回调中调用 doProcessInputEvents,不表示跳过队列。每个事件仍被包装成 QueuedInputEvent,并按到达顺序插入到 mPendingInputEventHead/Tail 组成的链表。队列让兼容调整、InputStage 处理和 finished 回执使用统一的数据结构,也防止注入事件异常的时间戳破坏接收次序。
// 把 native 回调接入 ViewRootImpl final class WindowInputEventReceiver extends InputEventReceiver { // 绑定输入通道和主线程 Looper public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) { // 调用父类创建 native peer super(inputChannel, looper); } // 覆盖单事件回调 @Override // 接收已经转换的 Java 事件 public void onInputEvent(InputEvent event) { // 按接收顺序立即入队处理 enqueueInputEvent(event, this, 0, true); } // 覆盖批量事件待处理回调 @Override // 根据缓冲模式选择消费时机 public void onBatchedInputEventPending() { // 非缓冲模式沿用父类行为 if (mUnbufferedInputDispatch) { // 立即处理待消费批次 super.onBatchedInputEventPending(); // 缓冲模式等待调度 } else { // 安排随帧消费批量事件 scheduleConsumeBatchedInput(); } } // 覆盖资源释放方法 @Override // 清理接收器状态 public void dispose() { // 取消尚未执行的批量消费 unscheduleConsumeBatchedInput(); // 让父类释放 native peer super.dispose(); } }入队逻辑按接收顺序链接节点,而不按事件时间戳排序,因为注入事件的时间戳未必单调。processImmediately 决定同步进入处理管道,或发异步消息稍后处理。
// 提供默认延迟处理的重载 void enqueueInputEvent(InputEvent event) { // 转到完整参数版本 enqueueInputEvent(event, null, 0, false); } // 把事件加入 ViewRootImpl 待处理队列 void enqueueInputEvent(InputEvent event, InputEventReceiver receiver, int flags, // 接收是否立即处理的开关 boolean processImmediately) { // 调整旧版本兼容行为 adjustInputEventForCompatibility(event); // 从对象池取得队列节点 QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags); // 事件按接收顺序入队,不依赖时间戳 // Always enqueue the input event in order, regardless of its time stamp. // 注入事件的时间戳不保证单调 // ... we cannot trust that the time stamp of injected events is monotonic. // 取得当前队尾 QueuedInputEvent last = mPendingInputEventTail; // 空队列需要同时设置头尾 if (last == null) { // 新节点成为队头 mPendingInputEventHead = q; // 新节点同时成为队尾 mPendingInputEventTail = q; // 非空队列追加节点 } else { // 原队尾指向新节点 last.next = q; // 更新队尾 mPendingInputEventTail = q; } // 更新待处理事件数量 mPendingInputEventCount += 1; // 向 systrace 记录队列长度 Trace.traceCounter(Trace.TRACE_TAG_INPUT, PendingInputEventQueueLengthCounterName, // 传入最新计数 mPendingInputEventCount); // 立即模式直接运行处理循环 if (processImmediately) { // 同步处理待办输入 doProcessInputEvents(); // 延迟模式交给 Handler } else { // 安排异步输入处理消息 scheduleProcessInputEvents(); } }5.4 InputStage 与 ViewPostImeInputStage
事件从 NativePreImeInputStage 起步,依次经过输入法前置处理、IME、EarlyPostIme、NativePostIme,抵达 ViewPostImeInputStage。触摸事件在这里执行 processPointerEvent,再调用根 View 的 dispatchPointerEvent。SyntheticInputStage 位于链尾,用于处理需要合成的输入。
InputStage 采用责任链形式,每个阶段都可以处理、转发或结束事件。按键需要给输入法和 pre-IME 逻辑留下机会,触摸则在 post-IME 的 View 阶段进入指针分发。这个中间层也解释了为何 native 回调没有直接调用 DecorView:窗口级兼容、输入法和 native 阶段必须先完成各自判断,View 树只是管道后段的消费者。
图 7:事件进入 View 树
5.5 Activity入口
根 View 是 DecorView,但触摸不会绕开 Activity。Activity 在 attach 时通过 window.setCallback(this) 成为 Window.Callback。DecorView 分发触摸时先调用这个 callback,于是事件进入 Activity.dispatchTouchEvent;Activity 再调用 getWindow().superDispatchTouchEvent,经 PhoneWindow 和 DecorView 回到 ViewGroup 的树形分发。
这条看似绕行的路径给 Activity 一个窗口级观察点,同时保留 DecorView 对 View 树的控制。onUserInteraction 在 ACTION_DOWN 时先得到通知;View 树若消费事件,Activity 直接返回 true;树上没有处理者时,Activity.onTouchEvent 承担兜底。三个位置分别处理交互通知、层级分发和 Activity 级默认行为。
// Activity 接收窗口触摸事件 public boolean dispatchTouchEvent(MotionEvent ev) { // 按下时触发用户交互通知 if (ev.getAction() == MotionEvent.ACTION_DOWN) { // 调用 Activity 的交互回调 onUserInteraction(); } // 优先交给 Window 和 View 树分发 if (getWindow().superDispatchTouchEvent(ev)) { // View 树已经消费事件 return true; } // View 树未消费时由 Activity 兜底 return onTouchEvent(ev); }Activity 这一步是绕行回来的关键。PhoneWindow.superDispatchTouchEvent 把事件交回 DecorView,而 DecorView 覆写的 dispatchTouchEvent 会先看 Window.Callback 是否存在,存在就先把事件交给 callback,即 Activity。等 Activity 再调回 superDispatchTouchEvent 时,才进入 ViewGroup.dispatchTouchEvent 的树形分发。这段绕行正是"事件首先到达 Activity"的机制来源。
// DecorView 覆写的触摸分发入口 @Override public boolean dispatchTouchEvent(MotionEvent ev) { // 取得当前绑定的 Window.Callback(Activity 在 attach 时设置了自己) final Window.Callback cb = mWindow.getCallback(); // callback 存在、窗口未销毁、且不是子特性窗口时优先交给 callback return cb != null && !mWindow.isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); } // 绕开 DecorView 自身的覆写,直接进入 ViewGroup 树形分发 public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } // PhoneWindow 对外的树形分发入口,供 Activity.dispatchTouchEvent 调用 @Override public boolean superDispatchTouchEvent(MotionEvent event) { // 转交给 DecorView 的同名方法 return mDecor.superDispatchTouchEvent(event); }单个 View 内部先检查无障碍焦点与安全过滤,再让 OnTouchListener 获得优先处理机会,未消费时调用 onTouchEvent。
// 分发单个 View 的触摸事件 public boolean dispatchTouchEvent(MotionEvent event) { // 单独处理面向无障碍焦点的事件 if (event.isTargetAccessibilityFocus()) { // 当前 View 不承载目标焦点时拒绝事件 if (!isAccessibilityFocusedViewOrHost()) { // 返回未消费 return false; } // 清除特殊目标标记并进入普通流程 event.setTargetAccessibilityFocus(false); } // 初始化分发结果 boolean result = false; // 调试构建中校验事件序列 if (mInputEventConsistencyVerifier != null) { // 记录当前触摸事件 mInputEventConsistencyVerifier.onTouchEvent(event, 0); } // 取得不含指针索引的动作类型 final int actionMasked = event.getActionMasked(); // 新手势开始时结束旧的嵌套滚动 if (actionMasked == MotionEvent.ACTION_DOWN) { // 停止嵌套滚动 stopNestedScroll(); } // 安全过滤通过后才交给业务逻辑 if (onFilterTouchEventForSecurity(event)) { // 读取监听器集合 ListenerInfo li = mListenerInfo; // 启用状态下优先调用 OnTouchListener if (li != null && li.mOnTouchListener != null // 检查 View 已启用 && (mViewFlags & ENABLED_MASK) == ENABLED // 调用监听器并读取消费结果 && li.mOnTouchListener.onTouch(this, event)) { // 记录监听器已经消费 result = true; } // 监听器未消费时调用 View 自身逻辑 if (!result && onTouchEvent(event)) { // 记录 View 已经消费 result = true; } } // 返回最终消费状态 return result; }5.6 Activity 提前消费会截断 View 树
重写 Activity.dispatchTouchEvent 时直接 return true,事件会停在 Activity;直接 return onTouchEvent(ev) 也会绕过 Window 的 superDispatchTouchEvent。这会出现 Activity 能收到事件、子 View 始终没有响应的现象。需要保留常规 View 分发时,应调用 super.dispatchTouchEvent(ev),或维持源码中的 Window 分发链。
六、小结
从 setView 建立 InputChannel,到 Looper 用 epoll 监听客户端 fd,再到 consumeEvents、enqueueInputEvent 和 InputStage,应用接收的是 InputDispatcher 已经选定并写入本窗口管道的事件。下层窗口的管道没有数据,应用侧返回未消费也不会触发跨窗口重投;所谓“透传”在这条链路上没有发生的入口。
6.1 进程内根 View 的统一观察
WindowManagerGlobal.mViews 保存了当前进程经 WindowManager 加入的全部根 View。借助反射可以遍历这些入口,为本进程的窗口安装点击埋点、事件回放或监听逻辑。它不提供跨进程能力,也不能改变 InputDispatcher 的选窗结果;系统级无障碍仍应使用平台提供的 AccessibilityService 等正式接口。
这类 hook 还要处理窗口动态增删。新的 Activity、Dialog 或悬浮窗会带来新的根 View,销毁窗口则会移除对应项;缓存一次列表无法持续覆盖进程内后续窗口。反射内部字段也依赖系统实现,适合源码研究或受控环境,不宜视为稳定公开 API。
6.2 按键事件要区分系统策略
KeyEvent 从 consumeEvents 进入 Java 后,会走 dispatchKeyEvent 与 PhoneWindow 的按键链,不复用触摸的 dispatchTouchEvent。Home 等系统键还会被系统策略拦截,应用不能依靠普通 Activity 获得所有按键;要监听这类系统按键行为,可以在 Service 里注册对应广播,具体可用性仍受 Android 版本与系统限制约束。
本文由 AgentWorkFlow 生成