浅谈 Android MotionEvent 批处理的原理及避免

2,139 阅读5分钟

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 的批处理实际是一项系统默认的优化行为,绝大部分情况下我们都不需要干预它。

参考