Android-Framework(二十二)-Input 事件的接受处理过程

文章内容

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:事件接收全链路总览

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: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 打开会话,之后同一进程复用该对象。

java
    // 获取进程内复用的 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。

java
    // 暴露给应用进程的添加窗口接口    @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 返回成功,再把通道交给事件接收器。

java
            // 请求 WMS 添加窗口并填充输入通道            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,                    // 传递窗口可见性和显示设备编号                    getHostVisibility(), mDisplay.getDisplayId(),                    // 传递内容区与稳定区输出对象                    mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,                    // 传递外扩区域和 InputChannel 外壳                    mAttachInfo.mOutsets, mInputChannel);            // 调用返回后,同一个 Java 外壳已经获得 native PTR

图 3:addToDisplay 时序
图 3:addToDisplay 时序

2.5 WMS.addWindow:创建一对 socket

WMS 确认窗口需要输入通道后调用 openInputChannelPair。Java 方法完成参数检查,创建工作进入 native 层。

java
    // 打开系统端与应用端配对的输入通道    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。它既能在本机进程间传递数据,也能保留消息边界,适合承载一条条输入消息。

cpp
// 定义输入通道对的创建方法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] 转移到应用传来的外壳。

java
        // 确认调用方需要通道且窗口未禁用输入通道        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:多窗口多管道
图 4:多窗口多管道

三、View 信息的注册:让 InputDispatcher 认识窗口

3.1 registerInputChannel 与 Connection

管道创建完成还不够。InputDispatcher 需要知道 fd 对应哪个窗口以及怎样维护待发送、待确认的事件,registerInputChannel 完成这项登记。

java
    // 注册可作为输入目标的通道    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 只是给已有通道补充分发状态。

cpp
// 注册已有通道并绑定窗口信息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 面向单条通道的运行时状态。

cpp
// 管理单条输入通道的分发状态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 等数据。

cpp
// 表示输入系统侧的窗口句柄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:没有设置它们时,窗口按触摸模态处理;否则还要检查坐标是否位于可触摸区域。可见窗口不一定能接收当前触点,层级靠前也不等于必定截获屏幕上的全部事件。

cpp
// 按显示设备和坐标寻找目标窗口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:注册与分发决策
图 5:注册与分发决策

四、监听管道:WindowInputEventReceiver 与主线程 epoll

4.1 setView 尾部创建接收器

addToDisplay 成功后,ViewRootImpl 才能用已经获得 PTR 的 mInputChannel 创建 WindowInputEventReceiver。同一阶段还搭建了 InputStage 链,为后续 Java 分发准备入口。

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 接收器回收。

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。

cpp
// 初始化 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 管应用窗口内部的处理顺序。

cpp
// 接收 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:接收端监听
图 6:接收端监听

五、读到事件并进入 View 树

5.1 handleEvent 与 consumeEvents

fd 可读触发 handleEvent,随后 consumeEvents 让 InputConsumer 从 socket 取消息并解析为 native InputEvent。读取动作发生在应用进程的主线程 Looper 回调中。

handleEvent 是注册在 Looper 上的回调:对端关闭或出错时返回 0 让 Looper 移除该 fd;可读时调用 consumeEvents 并检查异常。返回值告诉 Looper 是否继续监听这个 fd。

cpp
// 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 还可以按帧批量消费。

cpp
// 从通道消费事件并转换为 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。

java
    // 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 回执使用统一的数据结构,也防止注入事件异常的时间戳破坏接收次序。

java
    // 把 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 决定同步进入处理管道,或发异步消息稍后处理。

java
    // 提供默认延迟处理的重载    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 树
图 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 级默认行为。

java
    // 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"的机制来源。

java
    // 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);    }
java
    // PhoneWindow 对外的树形分发入口,供 Activity.dispatchTouchEvent 调用    @Override    public boolean superDispatchTouchEvent(MotionEvent event) {        // 转交给 DecorView 的同名方法        return mDecor.superDispatchTouchEvent(event);    }

单个 View 内部先检查无障碍焦点与安全过滤,再让 OnTouchListener 获得优先处理机会,未消费时调用 onTouchEvent。

java
    // 分发单个 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 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Android-Framework(二十二)-Input 事件的接受处理过程 · Yinchen 的博客