EN

Android 无障碍服务导致的整机卡顿案例分析

字数统计: 3.4k阅读时长: 14 min
2019/01/21
loading

现象

版本说明(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, /*byref*/ 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; //是否需要唤醒
{ // acquire lock
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;
// SystemServer 上层要对事件进行过滤
if (!mPolicy->filterInputEvent(&event, policyFlags)) {
return; // event was consumed by the filter
}

mLock.lock();
}

// Just enqueue a new motion event.
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();
} // release lock

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; // dispatch the event normally
}

if (!inputEventObj) {
ALOGE("Failed to obtain input event object for filterInputEvent.");
return true; // dispatch the event normally
}

// The callee is responsible for recycling the event.
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
// Native callback.
final boolean filterInputEvent(InputEvent event, int policyFlags) {
synchronized (mInputFilterLock) {
if (mInputFilter != null) {
try {
mInputFilter.filterInputEvent(event, policyFlags);
} catch (RemoteException e) {
/* ignore */
}
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) {
// Finished with this choreographer frame. Do the rest on the next one.
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(),已启用服务的这项能力也可能让真实触摸走前面的过滤路径;放大、触摸探索等功能也可能进入相关路径,不能归结为一个通用“手势监听开关”。

自检

不需要的服务可以关闭,但不要为了排查而一口气关闭自己依赖的读屏、语音控制或其他辅助功能。若怀疑某项新增服务与卡顿有关,可以这样核对:

  1. 记录机型、系统构建号、应用版本、启用的服务和具体复现场景。设置入口通常在“无障碍”中,名称和层级随厂商变化。
  2. 在不影响自己操作设备的前提下,一次只改变一个可疑服务的开关,重复相同的滑动场景,并恢复需要的功能。一次感觉变顺不能替代多次对照,也不要据此推出所有无障碍服务都有问题。
  3. 开发者用 Perfetto 等工具对齐输入事件时间、主 Looper 排队、InputDispatcher 派发和实际画面更新。Android 12 起可结合 FrameTimeline,但它不能代替完整输入延迟分析;没有产生新帧的等待,也不一定直接表现为某个已生成帧的 deadline miss。
  4. 服务开发者只声明实际需要的能力,在目标系统上检查输入路径和性能。对于必须使用执行手势或触摸探索的辅助功能,应修复额外延迟,而不是简单要求用户停用。

本文知乎地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 平台应用宝和讯飞输入法无障碍服务导致的全局卡顿分析

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 现象
  2. 2. Systrace 分析
  3. 3. 代码分析
    1. 3.1. Android 16 的路径变化
  4. 4. 罪魁祸首
  5. 5. 自检
  6. 6. 本文知乎地址
  7. 7. 关于我 && 博客