现象
版本说明(2026-10-13 核查) :这是 2019 年的历史案例,原文未保留完整 Trace、ROM 构建号或相关应用版本。旧调用链以下文链接的 AOSP android-9.0.0_r1 为参照,不能据此认定原设备就是该标签;另补 android-16.0.0_r1 的差异。结论限定于当时的配置和观察,不代表所有无障碍服务或这些应用的当前版本。
有用户反馈,手机在滑动的时候, 列表会一抖一抖的, 滑动桌面或者设置(只要是可以滑动的),都会出现,但是这个并不是必现,而是某些用户会出现,某些用户则不会出现。
吃瓜群众可以直接拉到下面看 罪魁祸首和自检 ,对分析问题比较感兴趣的可以看一下分析的过程。
Systrace 分析 本地测试有一台复现, 拿过来之后分析发现,手指滑动桌面或者设置,都会必现卡顿, 从 Trace 上看就是下面这样
红色箭头是我标出的更新间隔异常的位置。图中 Launcher 的 Buffer 供给有间断,但这张裁图没有完整帧时间和呈现信息,不能仅据计数认定 SurfaceFlinger 没有工作:没有新 Buffer 时,合成器也可能继续显示上一帧。
对应的 Launcher Trace 中,部分位置的输入处理与绘制活动出现间隔,提示可以沿输入路径继续查。它还不足以证明“这帧没有输入,所以必然没有绘制”;动画、惯性滚动和其他更新并不要求每帧都有新的触摸事件。
当时的现象发生在连续拖动中,所以我们先怀疑输入报点或交付延迟。不过触摸采样、事件批处理和显示刷新并非一一对应,不能要求每个 Vsync 都有独立的输入回调。检查硬件之前,先对照 InputReader 与 InputDispatcher 的活动:
图中 InputReader 仍有活动,而 InputDispatcher 的活动节奏不同于下面的对照。线程在运行并不能单独证明硬件报点和每个事件都正常,后面还需要核对事件时间与唤醒来源。
再回头看有问题的 Trace,我发现 InputDispatcher 的部分活动更接近 Vsync 节奏。这里应理解为这些事件经过了另一个交付路径,而不是整个 InputDispatcher 的所有唤醒都被 Vsync 接管。
我当时又点开调度详情,看到部分唤醒来自 system_server 主线程,而非 InputReader。当前保留的图没有展开这些 wakeup 详情,因此这里只能沿用当时的观察记录;要复核仍需完整 Trace,不能只凭两行切片对齐确认。
接下来从代码看,什么条件会让输入离开原来的直接入队路径。
代码分析 下面以 AOSP android-9.0.0_r1 的触摸 Move 路径为参照。这些是平台源码片段,不是可独立编译的应用示例;首段省略调试日志。源码中的 needWake 有条件判断,队列里已有工作时,不必每个事件都再次唤醒线程。
InputDispatcher.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 void InputDispatcher::notifyMotion (const NotifyMotionArgs* args) { if (!validateMotionEvent (args->action, args->actionButton, args->pointerCount, args->pointerProperties)) { return ; } uint32_t policyFlags = args->policyFlags; policyFlags |= POLICY_FLAG_TRUSTED; android::base::Timer t; mPolicy->interceptMotionBeforeQueueing (args->eventTime, policyFlags); if (t.duration () > SLOW_INTERCEPTION_THRESHOLD) { ALOGW ("Excessive delay in interceptMotionBeforeQueueing; took %s ms" , std::to_string (t.duration ().count ()).c_str ()); } bool needWake; { mLock.lock (); if (shouldSendMotionToInputFilterLocked (args)) { mLock.unlock (); MotionEvent event; event.initialize (args->deviceId, args->source, args->action, args->actionButton, args->flags, args->edgeFlags, args->metaState, args->buttonState, 0 , 0 , args->xPrecision, args->yPrecision, args->downTime, args->eventTime, args->pointerCount, args->pointerProperties, args->pointerCoords); policyFlags |= POLICY_FLAG_FILTERED; if (!mPolicy->filterInputEvent (&event, policyFlags)) { return ; } mLock.lock (); } MotionEntry* newEntry = new MotionEntry (args->eventTime, args->deviceId, args->source, policyFlags, args->action, args->actionButton, args->flags, args->metaState, args->buttonState, args->edgeFlags, args->xPrecision, args->yPrecision, args->downTime, args->displayId, args->pointerCount, args->pointerProperties, args->pointerCoords, 0 , 0 ); needWake = enqueueInboundEventLocked (newEntry); mLock.unlock (); } if (needWake) { mLooper->wake (); } }
这里 filterInputEvent() 返回 false 时,当前 notifyMotion() 跳过直接入队与本次唤醒。这表示过滤器接管了事件,并不等于事件永久丢失,也不是 InputReader 已停止运行。
继续看 JNI 中的 filterInputEvent :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 bool NativeInputManager::filterInputEvent (const InputEvent* inputEvent, uint32_t policyFlags) { ATRACE_CALL (); jobject inputEventObj; JNIEnv* env = jniEnv (); switch (inputEvent->getType ()) { case AINPUT_EVENT_TYPE_KEY: inputEventObj = android_view_KeyEvent_fromNative (env, static_cast <const KeyEvent*>(inputEvent)); break ; case AINPUT_EVENT_TYPE_MOTION: inputEventObj = android_view_MotionEvent_obtainAsCopy (env, static_cast <const MotionEvent*>(inputEvent)); break ; default : return true ; } if (!inputEventObj) { ALOGE ("Failed to obtain input event object for filterInputEvent." ); return true ; } jboolean pass = env->CallBooleanMethod (mServiceObj, gServiceClassInfo.filterInputEvent, inputEventObj, policyFlags); if (checkAndClearExceptionFromCallback (env, "filterInputEvent" )) { pass = true ; } env->DeleteLocalRef (inputEventObj); return pass; }
JNI 调回 Java 层的 InputManagerService.filterInputEvent 。上面的对象创建失败或 Java 回调抛异常时,源码允许回到正常派发路径;不能漏掉这些返回 true 的分支。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 final boolean filterInputEvent (InputEvent event, int policyFlags) { synchronized (mInputFilterLock) { if (mInputFilter != null ) { try { mInputFilter.filterInputEvent(event, policyFlags); } catch (RemoteException e) { } return false ; } } event.recycle(); return true ; }
本例关注 mInputFilter 为 AccessibilityInputFilter 的情况。AccessibilityManagerService.updateInputFilter 按启用功能组合 flags,非零时安装过滤器;不是任何一个无障碍开关都会启用同一条 MotionEvent 路径。
InputFilter 先把事件投递到 Handler,下面第二段是 H.handleMessage() 中的分支。AccessibilityInputFilter 使用 context.getMainLooper();图中 system_server 的主线程与单独名为 android.ui 的线程不要混为一谈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 final public void filterInputEvent (InputEvent event, int policyFlags) { mH.obtainMessage(MSG_INPUT_EVENT, policyFlags, 0 , event).sendToTarget(); } case MSG_INPUT_EVENT: { final InputEvent event = (InputEvent)msg.obj; try { if (mInboundInputEventConsistencyVerifier != null ) { mInboundInputEventConsistencyVerifier.onInputEvent(event, 0 ); } onInputEvent(event, msg.arg1); } finally { event.recycle(); } break ; }
继续看 AccessibilityInputFilter.onInputEvent :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @Override public void onInputEvent (InputEvent event, int policyFlags) { if (DEBUG) { Slog.d(TAG, "Received event: " + event + ", policyFlags=0x" + Integer.toHexString(policyFlags)); } if (mEventHandler == null ) { if (DEBUG) Slog.d(TAG, "mEventHandler == null for event " + event); super .onInputEvent(event, policyFlags); return ; } EventStreamState state = getEventStreamState(event); if (state == null ) { super .onInputEvent(event, policyFlags); return ; } int eventSource = event.getSource(); if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0 ) { state.reset(); mEventHandler.clearEvents(eventSource); super .onInputEvent(event, policyFlags); return ; } if (state.updateDeviceId(event.getDeviceId())) { mEventHandler.clearEvents(eventSource); } if (!state.deviceIdValid()) { super .onInputEvent(event, policyFlags); return ; } if (event instanceof MotionEvent) { if ((mEnabledFeatures & FEATURES_AFFECTING_MOTION_EVENTS) != 0 ) { MotionEvent motionEvent = (MotionEvent) event; processMotionEvent(state, motionEvent, policyFlags); return ; } else { super .onInputEvent(event, policyFlags); } } else if (event instanceof KeyEvent) { KeyEvent keyEvent = (KeyEvent) event; processKeyEvent(state, keyEvent, policyFlags); } }
继续看 processMotionEvent
1 2 3 4 5 6 7 8 9 10 11 12 private void processMotionEvent (EventStreamState state, MotionEvent event, int policyFlags) { if (!state.shouldProcessScroll() && event.getActionMasked() == MotionEvent.ACTION_SCROLL) { super .onInputEvent(event, policyFlags); return ; } if (!state.shouldProcessMotionEvent(event)) { return ; } batchMotionEvent(event, policyFlags); }
继续看 batchMotionEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private void batchMotionEvent (MotionEvent event, int policyFlags) { if (DEBUG) { Slog.i(TAG, "Batching event: " + event + ", policyFlags: " + policyFlags); } if (mEventQueue == null ) { mEventQueue = MotionEventHolder.obtain(event, policyFlags); scheduleProcessBatchedEvents(); return ; } if (mEventQueue.event.addBatch(event)) { return ; } MotionEventHolder holder = MotionEventHolder.obtain(event, policyFlags); holder.next = mEventQueue; mEventQueue.previous = holder; mEventQueue = holder; }
继续看 scheduleProcessBatchedEvents
1 2 3 4 private void scheduleProcessBatchedEvents () { mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, mProcessBatchedEventsRunnable, null ); }
它把任务安排在 Choreographer 的 CALLBACK_INPUT 阶段。何时实际执行还取决于主 Looper 调度;不能保证固定等一个 Vsync 后就完成交付。对应的完整 Runnable 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private final Runnable mProcessBatchedEventsRunnable = new Runnable () { @Override public void run () { final long frameTimeNanos = mChoreographer.getFrameTimeNanos(); if (DEBUG) { Slog.i(TAG, "Begin batch processing for frame: " + frameTimeNanos); } processBatchedEvents(frameTimeNanos); if (DEBUG) { Slog.i(TAG, "End batch processing." ); } if (mEventQueue != null ) { scheduleProcessBatchedEvents(); } } };
还要往下看 processBatchedEvents()。它只处理事件时间早于本次 frameNanos 的条目,时间大于或等于该值的剩余部分留给后续回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private void processBatchedEvents (long frameNanos) { MotionEventHolder current = mEventQueue; if (current == null ) { return ; } while (current.next != null ) { current = current.next; } while (true ) { if (current == null ) { mEventQueue = null ; break ; } if (current.event.getEventTimeNano() >= frameNanos) { current.next = null ; break ; } handleMotionEvent(current.event, current.policyFlags); MotionEventHolder prior = current; current = current.previous; prior.recycle(); } }
需要继续交付的事件沿变换链到 AccessibilityInputFilter.onMotionEvent(),再经 InputFilter.sendInputEvent()、InputManagerService.InputFilterHost.sendInputEvent() 以 FLAG_FILTERED 标记异步注入 native 派发路径。过滤器也可能有意消费或变换事件,因此提前 return 本身不证明事件丢失。
因此,Android 9 这条路径中的主线程排队、批处理时间边界和后续派发,都可能增加输入交付延迟。它与我当时观察到的节奏变化相符,但不能仅凭代码断言每次开启该功能都必然掉帧,或者每个事件恰好多等 16 ms。下面的箭头仍是原案例标注:
Android 16 的路径变化 在 AOSP android-16.0.0_r1 的同名类 中,processMotionEvent() 已直接调用 handleMotionEvent(),不再经过上面这组 batchMotionEvent() / Choreographer 队列:
1 2 3 4 5 6 7 8 9 10 11 12 private void processMotionEvent (EventStreamState state, MotionEvent event, int policyFlags) { if (!state.shouldProcessScroll() && event.getActionMasked() == ACTION_SCROLL) { super .onInputEvent(event, policyFlags); return ; } if (!state.shouldProcessMotionEvent(event)) { return ; } handleMotionEvent(event, policyFlags); }
这个对比只能证明所核对的版本实现不同,不能据此确定首次修复版本,也不能证明所有现代设备都没有无障碍相关延迟。过滤、线程调度、具体服务逻辑及厂商修改仍需看实际系统。
罪魁祸首 当时我们把触发条件定位到部分已启用的无障碍服务,在 Pixel 和其他厂商设备上都见过相似现象,涉及讯飞输入法和应用宝。由于没有保留对应 ROM、应用版本和完整对照数据,这里保留为 2019 年的案例记录 ,不把它写成这两个应用当前版本的检测结果。
无障碍服务用于帮助视障、运动障碍等用户操作设备,部分应用也使用它实现自动化。不同能力会启用不同输入处理功能,应按实际服务配置分析。下面是当时的设置页,列出服务不等于全部已开启,更不等于图中每个服务都与本例有关:
原文引用的权限弹框来自 Voice Access ,不是讯飞或应用宝的数据访问记录。它提示这类能力可能接触敏感界面信息;不能据弹框断言某服务已读取所有密码、银行卡或聊天内容。能获得什么,取决于服务声明、系统版本、应用暴露的无障碍节点及字段保护。
Android 14(API 34)还提供 accessibilityDataSensitive 等机制,限制未声明 isAccessibilityTool 的服务与敏感视图交互。这不是对某个服务可信程度的认证,也不能取代对具体应用和数据暴露范围的判断。
下面讯飞输入法弹框中的“执行手势”应与“监听手势”区分。它对应 canPerformGestures 能力;Android 7.0(API 24)加入的 dispatchGesture() 允许服务注入手势,不是仅订阅触摸通知。
Android 9 的 updatePerformGesturesLocked() 检查已绑定服务的 CAPABILITY_CAN_PERFORM_GESTURES,进而启用 FLAG_FEATURE_INJECT_MOTION_EVENTS,该 flag 属于 FEATURES_AFFECTING_MOTION_EVENTS。所以即使当时没有主动调用 dispatchGesture(),已启用服务的这项能力也可能让真实触摸走前面的过滤路径;放大、触摸探索等功能也可能进入相关路径,不能归结为一个通用“手势监听开关”。
自检 不需要的服务可以关闭,但不要为了排查而一口气关闭自己依赖的读屏、语音控制或其他辅助功能。若怀疑某项新增服务与卡顿有关,可以这样核对:
记录机型、系统构建号、应用版本、启用的服务和具体复现场景。设置入口通常在“无障碍”中,名称和层级随厂商变化。
在不影响自己操作设备的前提下,一次只改变一个可疑服务的开关,重复相同的滑动场景,并恢复需要的功能。一次感觉变顺不能替代多次对照,也不要据此推出所有无障碍服务都有问题。
开发者用 Perfetto 等工具对齐输入事件时间、主 Looper 排队、InputDispatcher 派发和实际画面更新。Android 12 起可结合 FrameTimeline ,但它不能代替完整输入延迟分析;没有产生新帧的等待,也不一定直接表现为某个已生成帧的 deadline miss。
服务开发者只声明实际需要的能力,在目标系统上检查输入路径和性能。对于必须使用执行手势或触摸探索的辅助功能,应修复额外延迟,而不是简单要求用户停用。
本文知乎地址 由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面知乎 - Android 平台应用宝和讯飞输入法无障碍服务导致的全局卡顿分析
关于我 && 博客 下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!
博主个人介绍 :里面有个人的微信和微信群链接。
本博客内容导航 :个人博客内容的一个导航。
个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
Android性能优化知识星球 :付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。
一个人可以走的更快 , 一群人可以走的更远