18、窗口与输入系统:WindowToken管理、InputDispatcher与VSync对齐、触摸延迟优化

各位同学,今天我们来聊一个非常硬核的话题——窗口与输入系统。说实话,这个模块是我在Android图形系统里觉得最“拧巴”的一块。为什么?因为窗口管理和输入事件处理,本质上是在两个不同的线程里跑,但它们又必须紧密配合。你想想看,用户点了一下屏幕,从硬件中断到App收到事件,再到最终渲染到屏幕上,这中间经过了多少个环节?任何一个环节慢了,用户就会觉得“卡”、“不跟手”。

我个人习惯把这个问题拆成三个层面来看:窗口的合法性(WindowToken)、事件的派发(InputDispatcher)、以及渲染的节拍(VSync)。这三者对齐了,触摸延迟才能降下来。今天我们就一个一个掰开揉碎了讲。

18.1 WindowToken:窗口的“身份证”

先问一个问题:为什么Android系统要搞一个WindowToken出来?

说白了,WindowToken就是窗口的“身份证”。没有它,WindowManagerService(WMS)根本不会搭理你。我在项目中遇到过好几次,App崩溃后残留了一个窗口,结果整个系统都点不动了——就是因为那个窗口的Token没有被正确清理。

WindowToken的核心作用有两个:

  • 权限校验:只有持有合法Token的进程才能添加、更新或移除窗口。
  • 窗口分组:同一个Token下的窗口属于同一个“窗口族”,比如Dialog和它的宿主Activity共享一个Token。

我们来看一段WMS中addWindow的简化逻辑:

// WindowManagerService.java (简化)
public int addWindow(Session session, IWindow client, ...) {
    WindowToken token = mTokenMap.get(attrs.token);
    if (token == null) {
        // 没有Token?直接拒绝
        return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
    }
    // 校验调用者是否有权限
    if (!token.hasAccess(session.mUid)) {
        return WindowManagerGlobal.ADD_PERMISSION_DENIED;
    }
    // 创建WindowState并关联Token
    WindowState win = new WindowState(this, session, client, token, ...);
    token.addWindow(win);
    ...
}

嗯,这里要注意:attrs.token 这个参数,很多开发者会传null。如果你传null,系统会尝试用当前线程的ActivityRecord去生成一个Token。但如果当前线程没有Activity(比如在Service里弹窗),那就直接崩了。我曾经排查过一个线上bug,就是第三方SDK在后台线程弹窗,传了null Token,导致WMS直接抛异常。

避坑指南:我曾经在优化一个Launcher的弹窗时,发现每次快速切换App都会丢Token。后来定位到是WindowToken的复用逻辑没处理好——同一个Token被两个窗口同时引用,结果一个窗口关闭时把Token也干掉了。解决方案是给每个窗口单独维护一个Token引用计数。

18.2 InputDispatcher:事件派发的“交通警察”

接下来我们聊聊InputDispatcher。这个家伙是Android输入系统的核心,它负责把硬件上报的输入事件,派发给合适的窗口。

InputDispatcher的工作流程大致是这样的:

  1. 从InputReader线程拿到原始事件(比如触摸坐标、按键码)。
  2. 根据当前窗口的焦点状态,找到目标窗口。
  3. 把事件封装成InputMessage,通过socket发送给目标进程。
  4. 等待App处理完事件,返回一个“已完成”的ack。

这里面最坑的一点是:InputDispatcher是单线程的。所有事件都在一个Looper线程里排队处理。如果某个事件处理慢了,后续所有事件都得等着。这就是为什么有时候你点了一下屏幕,过了半秒才有反应——不是系统卡,是事件队列堵了。

我给大家看一个InputDispatcher派发事件的简化代码:

// InputDispatcher.cpp (简化)
void InputDispatcher::dispatchOnce() {
    // 从队列里取出下一个事件
    EventEntry* entry = mPendingEvents.pop();
    if (entry == nullptr) return;

    // 找到目标窗口
    sp<InputWindowHandle> target = findTargetWindow(entry);
    if (target == nullptr) {
        // 没有窗口?丢弃事件
        releaseEventEntry(entry);
        return;
    }

    // 发送事件到目标窗口的socket
    status_t status = sendMessage(target->inputChannel, entry);
    if (status != OK) {
        // 发送失败,可能是窗口挂了
        handleTargetDead(target);
    }
}

你想想看,如果findTargetWindow这一步耗时太长,比如窗口层级特别深,或者焦点窗口正在动画切换,那整个事件派发就卡住了。我优化过一个车载系统的触摸延迟,发现就是窗口焦点切换时,InputDispatcher要遍历所有窗口来确认新焦点,这个遍历在窗口数量超过50个时,一次就要花掉8-10ms。

优化思路:减少窗口层级深度,避免在焦点切换时做大量计算。另外,可以考虑把高频事件(比如滑动)和低频事件(比如按键)分开处理,用不同的优先级队列。

18.3 VSync对齐:让触摸和渲染“同频共振”

好了,现在窗口有了合法身份,事件也派发出去了。但还有一个关键问题:App收到事件后,什么时候开始渲染?

答案是:等下一个VSync信号。

VSync是屏幕刷新的节拍。比如60Hz的屏幕,每16.6ms发出一个VSync信号。App的渲染管线必须在这个信号到来时开始工作,否则就会出现掉帧。

但这里有个坑:InputDispatcher派发事件的时间,和VSync信号的时间,是不同步的。如果事件刚好在VSync之后到达,那App就得等整整一个周期才能开始渲染。这就多出了16.6ms的延迟。

为了解决这个问题,Android引入了VSync对齐机制。简单说,就是让InputDispatcher尽量在VSync信号到来之前,把事件派发到App。这样App收到事件后,可以立刻开始渲染,不用等下一个VSync。

具体实现上,SurfaceFlinger会通过Choreographer来协调。App端注册一个FrameCallback,Choreographer会在VSync到来时回调它。但InputDispatcher的事件是异步的,所以需要一种机制来“预测”下一个VSync的时间。

我给大家画一张图,展示一下事件流和VSync的关系:

触摸事件与VSync对齐流程 时间 VSync VSync VSync VSync 触摸事件 未对齐 等待16ms 触摸事件 已对齐 立即处理 渲染帧1 渲染帧2 渲染帧3 未对齐:事件在VSync后到达,需等待下一个VSync才能渲染 已对齐:事件在VSync前到达,可立即进入渲染管线

从这张图可以看得很清楚:未对齐的事件,白白浪费了一个VSync周期。而对齐后,触摸到渲染的延迟可以降低整整16.6ms(60Hz下)。

18.4 触摸延迟优化:实战三板斧

好了,理论讲完了,我们来点实际的。触摸延迟优化,我总结了三板斧:

第一板斧:减少事件派发路径

InputDispatcher从收到事件到派发给App,中间要经过多次Binder调用和socket通信。每多一次调用,就多一次延迟。我建议:

  • 减少窗口层级,避免焦点窗口在深层嵌套中。
  • 使用InputFilter时注意性能,不要在过滤器中做耗时操作。
  • 对于高频事件(如滑动),考虑使用MotionEvent的批量处理机制。

第二板斧:对齐VSync

这个主要靠系统框架来做,但App也可以配合:

  • 使用Choreographer来注册帧回调,而不是自己开线程轮询。
  • 避免在onTouchEvent中做耗时操作,把计算放到下一帧。
  • 如果使用SurfaceView,注意onDraw的调用时机,尽量与VSync对齐。

第三板斧:预判与预测

这是比较高级的技巧了。通过预测用户的触摸轨迹,提前开始渲染。比如:

  • 在滑动场景中,根据前几帧的速度预测下一帧的位置,提前加载内容。
  • 使用VelocityTracker计算速度,提前分配渲染资源。
核心结论:触摸延迟优化的本质,就是让事件流和渲染流在时间上对齐。WindowToken保证了窗口的合法性,InputDispatcher保证了事件的正确派发,VSync对齐保证了渲染的及时性。三者缺一不可。

最后说一句,我见过很多团队在优化触摸延迟时,只盯着InputDispatcher或者只盯着渲染管线,结果效果都不理想。其实这是一个系统工程,需要从窗口管理、事件派发、渲染调度三个维度同时下手。嗯,今天就先聊到这里,大家回去可以看看自己项目里的触摸延迟数据,看看是哪一块拖了后腿。


公众号:蓝海资料掘金营,微信deep3321