MotionEvent 的文档上提到,为了提高效率,多个 ACTION_MOVE 事件可能会被合并为同一个 MotionEvent 对象再回调给应用。这里简单看下具体 Android 系统实现上是怎么做的 MotionEvent 批处理,以及应用侧如何改变这一行为?
MotionEvent 批处理的实现
先简单讲下 Input 事件分发到 Window 的流程。 WindowManager addView 时 创建 ViewRootImpl,ViewRootImpl 会驱动 WMS 创建对应的 Window 和用于发送 Input 事件的 SocketPair,然后 ViewRootImpl 再拿到 SocketPair 的 Client 端创建 InputEventReceiver, InputEventReceiver 内部会将 SocketPair 的 Client 端注册到主线程的 Looper( 基于 epoll 做同步非阻塞 IO ),之后 Input 事件 ready 时走 SocketPair 发到应用主线程,由对应的 InputEventReceiver 来处理。
Input 事件到来时,对于非 MOVE 事件,直接走 InputEventReceiver.dispatchInputEvent 分发,最终会直接走到 ViewRootImpl 的事件分发流程中( ViewRootImpl.deliverInputEvent )。注意到这种情况下 Input 事件是直接在 Native 层 epoll 事件 ready 时层层分发上来的,流程上没有额外的抛 Handler 处理,即是我们常说的 Looper Printer 监控不到的 Input 事件分发的情况。
对于 MOVE 事件的情况,以下基于 master 的 Android 源码做正常流程的简单分析。 在 Input 事件到来时,NativeInputEventReceiver::handleEvent 被触发,
int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {
// ...
if (events & ALOOPER_EVENT_INPUT) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
status_t status = consumeEvents(env, false /*consumeBatches*/, -1, nullptr); // 关键逻辑1:调用 consumeEvents,consumeBatches 传 false
mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");
return status == OK || status == NO_MEMORY ? KEEP_CALLBACK : REMOVE_CALLBACK;
}
// ...
}
通过调用 NativeInputEventReceiver::consumeEvents 来真正触发 Input 事件的消费,此时 consumeEvents 的参数 consumeBatches 传false,
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
// ...
for (;;) {
// ...
status_t status = mInputConsumer.consume(&mInputEventFactory,
consumeBatches, frameTime, &seq, &inputEvent); // 关键逻辑2:调用 consume,consumeBatches 传 false
// ...
}
}
NativeInputEventReceiver::consumeEvents 内部调用 InputConsumer::consume,此时 consume 的参数 consumeBatches 传false,
status_t InputConsumer::consume(InputEventFactoryInterface* factory, bool consumeBatches,
nsecs_t frameTime, uint32_t* outSeq, InputEvent** outEvent) {
// ...
*outSeq = 0;
*outEvent = nullptr;
// Fetch the next input message.
// Loop until an event can be returned or no additional events are received.
while (!*outEvent) {
if (mMsgDeferred) {
// mMsg contains a valid input message from the previous call to consume
// that has not yet been processed.
mMsgDeferred = false;
} else {
// Receive a fresh message.
status_t result = mChannel->receiveMessage(&mMsg); // 关键逻辑1:receiveMessage,首次调用时接收到 MOVE 事件,返回 OK;二次进入时 SocketPair Server 端无新事件产生,则返回 WOULD_BLOCK
if (result == OK) {
mConsumeTimes.emplace(mMsg.header.seq, systemTime(SYSTEM_TIME_MONOTONIC));
}
if (result) { // 关键逻辑2:result 非 OK的情况
// Consume the next batched event unless batches are being held for later.
if (consumeBatches || result != WOULD_BLOCK) { // 关键逻辑3:二次进入时,consumeBatches 为 false,result 为 WOULD_BLOCK,不进入
result = consumeBatch(factory, frameTime, outSeq, outEvent);
if (*outEvent) {
if (DEBUG_TRANSPORT_ACTIONS) {
ALOGD("channel '%s' consumer ~ consumed batch event, seq=%u",
mChannel->getName().c_str(), *outSeq);
}
break;
}
}
return result; // 关键逻辑4:二次进入时,返回 WOULD_BLOCK
}
}
switch (mMsg.header.type) {
// ...
case InputMessage::Type::MOTION: {
// ...
// Start a new batch if needed.
if (mMsg.body.motion.action == AMOTION_EVENT_ACTION_MOVE ||
mMsg.body.motion.action == AMOTION_EVENT_ACTION_HOVER_MOVE) { // 关键逻辑4:对于 MOVE 事件,缓存到 mBatches 中
Batch batch;
batch.samples.push_back(mMsg);
mBatches.push_back(batch);
if (DEBUG_TRANSPORT_ACTIONS) {
ALOGD("channel '%s' consumer ~ started batch event",
mChannel->getName().c_str());
}
break;
}
// ...
}
// ...
}
}
return OK;
}
InputConsumer::consume 中,首先从 mChannel( 即从 SocketPair 的 Server 端读 )中接收到 MOVE 事件,然后缓存到 mBatches 中,接着再次尝试从 mChannel 中读取 Input 事件,此时 receiveMessage 返回 WOULD_BLOCK,函数返回, 继续看 NativeInputEventReceiver::consumeEvents,
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
// ...
if (consumeBatches) {
mBatchedInputEventPending = false; // 关键逻辑1:mBatchedInputEventPending 初始值也是 false
}
// ...
bool skipCallbacks = false;
for (;;) {
uint32_t seq;
InputEvent* inputEvent;
status_t status = mInputConsumer.consume(&mInputEventFactory,
consumeBatches, frameTime, &seq, &inputEvent); // 关键逻辑2:调用 consume,consumeBatches 传 false
// ...
if (status == WOULD_BLOCK) {
if (!skipCallbacks && !mBatchedInputEventPending && mInputConsumer.hasPendingBatch()) { // 关键逻辑3:InputConsumer::consume 返回 WOULD_BLOCK,skipCallbacks 和 mBatchedInputEventPending 都为 false,且已经有 pending 的 MOVE事件,进入
// ...
mBatchedInputEventPending = true;
// ...
env->CallVoidMethod(receiverObj.get(),
gInputEventReceiverClassInfo.onBatchedInputEventPending,
mInputConsumer.getPendingBatchSource()); // 关键逻辑4:调用 Java 的 InputEventReceiver 的 onBatchedInputEventPending
// ...
}
return OK;
}
// ....
}
}
InputConsumer::consume 返回 WOULD_BLOCK 后,调用 Java InputEventReceiver 的 onBatchedInputEventPending( 即 ViewRootImpl 中 WindowInputEventReceiver 的 onBatchedInputEventPending),
public void onBatchedInputEventPending(int source) {
final boolean unbuffered = mUnbufferedInputDispatch
|| (source & mUnbufferedInputSource) != SOURCE_CLASS_NONE;
if (unbuffered) {
if (mConsumeBatchedInputScheduled) {
unscheduleConsumeBatchedInput();
}
// Consume event immediately if unbuffered input dispatch has been requested.
consumeBatchedInputEvents(-1); // 关键逻辑1:这里从注释上看到有个避免批处理立即消费的处理逻辑,关键是要使得 mUnbufferedInputDispatch 为 true
return;
}
scheduleConsumeBatchedInput(); // 关键逻辑2:调用 ViewRootImpl 的 scheduleConsumeBatchedInput
}
WindowInputEventReceiver.onBatchedInputEventPending 中调用 ViewRootImpl.scheduleConsumeBatchedInput,
void scheduleConsumeBatchedInput() {
// If anything is currently scheduled to consume batched input then there's no point in
// scheduling it again.
if (!mConsumeBatchedInputScheduled && !mConsumeBatchedInputImmediatelyScheduled) {
mConsumeBatchedInputScheduled = true;
mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,
mConsumedBatchedInputRunnable, null); // 关键逻辑1:往 Choreographer 中注册一个 Input Callback 来消费批量的 Input 事件
}
}
最终是往 Choreographer 中注册一个 Input Callback 来消费批量的 Input 事件。 简单来说 MOVE 事件的批处理即是在屏幕触摸采样率高于刷新率的情况下,将一帧内的所有 MOVE 事件合并之后再统一分发给应用侧。MOVE 事件在到达应用进程之后就会被缓存,等 VSync 到来后再在 Input 阶段统一合成一个 MotionEvent 来分发。 注意到这种情况下 Input 事件最终才是在 VSync 的 Input 阶段分发的,基于 FrameMetrics 统计到的 Input 处理耗时实际涵盖的是这类情况。
应用侧避免 MotionEvent 批处理
应用侧可以避免 MotionEvent 的批处理行为吗?注意到前面分析提到 ViewRootImpl 的 成员变量 mUnbufferedInputDispatch 可以控制 MOVE 事件被立即消费。实际我们可以直接调用 View.requestUnbufferedDispatch 来修改它的值,但 mUnbufferedInputDispatch 的值在下次事件分发( Down -> Move -> Up / Cancel )时会重置,实际我们需要在每次 DOWN 事件到来时都调用 requestUnbufferedDispatch 才能始终避免 MOVE 事件的批处理。可参考 Chromium 或者 Flutter 的实现,但正如 requestUnbufferedDispatch 方法的文档所说,MotionEvent 的批处理实际是一项系统默认的优化行为,绝大部分情况下我们都不需要干预它。
参考
- Batched consumption 文档: android.googlesource.com/platform/fr…