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直接抛异常。
18.2 InputDispatcher:事件派发的“交通警察”
接下来我们聊聊InputDispatcher。这个家伙是Android输入系统的核心,它负责把硬件上报的输入事件,派发给合适的窗口。
InputDispatcher的工作流程大致是这样的:
- 从InputReader线程拿到原始事件(比如触摸坐标、按键码)。
- 根据当前窗口的焦点状态,找到目标窗口。
- 把事件封装成InputMessage,通过socket发送给目标进程。
- 等待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周期。而对齐后,触摸到渲染的延迟可以降低整整16.6ms(60Hz下)。
18.4 触摸延迟优化:实战三板斧
好了,理论讲完了,我们来点实际的。触摸延迟优化,我总结了三板斧:
第一板斧:减少事件派发路径
InputDispatcher从收到事件到派发给App,中间要经过多次Binder调用和socket通信。每多一次调用,就多一次延迟。我建议:
- 减少窗口层级,避免焦点窗口在深层嵌套中。
- 使用
InputFilter时注意性能,不要在过滤器中做耗时操作。 - 对于高频事件(如滑动),考虑使用
MotionEvent的批量处理机制。
第二板斧:对齐VSync
这个主要靠系统框架来做,但App也可以配合:
- 使用
Choreographer来注册帧回调,而不是自己开线程轮询。 - 避免在
onTouchEvent中做耗时操作,把计算放到下一帧。 - 如果使用SurfaceView,注意
onDraw的调用时机,尽量与VSync对齐。
第三板斧:预判与预测
这是比较高级的技巧了。通过预测用户的触摸轨迹,提前开始渲染。比如:
- 在滑动场景中,根据前几帧的速度预测下一帧的位置,提前加载内容。
- 使用
VelocityTracker计算速度,提前分配渲染资源。
最后说一句,我见过很多团队在优化触摸延迟时,只盯着InputDispatcher或者只盯着渲染管线,结果效果都不理想。其实这是一个系统工程,需要从窗口管理、事件派发、渲染调度三个维度同时下手。嗯,今天就先聊到这里,大家回去可以看看自己项目里的触摸延迟数据,看看是哪一块拖了后腿。