尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

17、触摸事件分发流程:MotionEvent的封装,从InputDispatcher到ViewRootImpl

17、触摸事件分发流程:MotionEvent的封装,从InputDispatcher到ViewRootImpl 17.1 事件封装的起点InputDispatcher好我们直接进入正题。上一章我们聊到InputDispatcher从队列里取出事件准备发给目标窗口。但这里有个关键动作——事件封装。说白了就是把原始的输入数据包装成一个Java层能识别的MotionEvent对象。我个人习惯把InputDispatcher看作一个“分拣员”。它手里拿着的原始数据是C层的NotifyArgs结构体。里面包含了事件类型、坐标、时间戳、设备ID等等。但这些信息Java层不认识。所以必须做一次“翻译”。这个过程发生在InputDispatcher::dispatchOnceInnerLocked()里。它会调用InputDispatcher::prepareDispatchCycleLocked()然后走到InputChannel::sendMessage()。嗯这里要注意发送的不是MotionEvent本身而是一个InputMessage。核心要点InputDispatcher发送的是序列化后的InputMessage而不是直接传MotionEvent。MotionEvent的创建发生在接收端——也就是ViewRootImpl所在的进程。17.2 跨进程的“快递”InputMessage的结构你想想看InputDispatcher运行在system_server进程而我们的应用进程是独立的。跨进程传递对象必须序列化。InputMessage就是那个“快递包裹”。它的结构大概是这样struct InputMessage { enum class Type : uint32_t { KEY, MOTION, FINISHED, FOCUS, CAPTURE, DRAG, TIMELINE, }; Header header; union { KeyEntry key; MotionEntry motion; FinishedEntry finished; FocusEntry focus; CaptureEntry capture; DragEntry drag; TimelineEntry timeline; } body; };对于触摸事件我们关注的是MotionEntry。它包含了事件类型ACTION_DOWN、ACTION_MOVE等事件发生的时间戳指针数量及每个指针的坐标、压力、大小元键状态shift、ctrl等设备ID、来源source我记得有一次排查一个多点触控的bug发现某个设备上报的指针ID顺序乱了。最后定位到是驱动层的问题但当时我盯着InputMessage里的pointerIds字段看了半天。这个字段在封装时如果处理不当就会出现“指针漂移”的现象。17.3 接收端的重生ViewRootImpl中的MotionEvent创建好包裹送到了。应用进程的InputEventReceiver通过InputChannel读取到InputMessage。然后调用InputEventReceiver::consumeBatchedInputEvents()最终走到NativeInputEventReceiver::handleEvent()。这里有个关键函数NativeInputEventReceiver::consumeEvents()。它会调用InputEvent::create()根据InputMessage的类型创建对应的Java层事件对象。对于触摸事件创建的是MotionEvent。这个过程我建议你重点关注// 伪代码展示核心逻辑 jobject MotionEvent::create(JNIEnv* env, const InputMessage msg) { // 1. 从InputMessage中提取原始数据 nsecs_t eventTime msg.body.motion.eventTime; int32_t action msg.body.motion.action; int32_t pointerCount msg.body.motion.pointerCount; // 2. 构造PointerProperties数组 PointerProperties pointerProperties[pointerCount]; for (int i 0; i pointerCount; i) { pointerProperties[i].id msg.body.motion.pointerIds[i]; pointerProperties[i].toolType msg.body.motion.toolTypes[i]; } // 3. 构造PointerCoords数组 PointerCoords pointerCoords[pointerCount]; for (int i 0; i pointerCount; i) { pointerCoords[i].x msg.body.motion.pointers[i].x; pointerCoords[i].y msg.body.motion.pointers[i].y; pointerCoords[i].pressure msg.body.motion.pointers[i].pressure; pointerCoords[i].size msg.body.motion.pointers[i].size; } // 4. 调用Java层的MotionEvent.obtain()静态方法 return env-CallStaticObjectMethod( gMotionEventClassInfo.clazz, gMotionEventClassInfo.obtain, eventTime, action, pointerCount, pointerProperties, pointerCoords, msg.body.motion.metaState, msg.body.motion.buttonState, msg.body.motion.xPrecision, msg.body.motion.yPrecision, msg.body.motion.deviceId, msg.body.motion.edgeFlags, msg.body.motion.source, msg.body.motion.flags ); }避坑指南我曾经遇到过一个性能问题每次触摸事件都卡顿。后来发现是MotionEvent.obtain()频繁创建对象导致GC。实际上Android系统内部维护了一个MotionEvent的对象池obtain()会优先从池中复用。但如果你的代码里手动调用了MotionEvent.obtain()却没有及时recycle()就会导致池子耗尽频繁触发GC。切记谁obtain谁recycle。17.4 从Native到Java的桥梁InputEventReceiverMotionEvent创建完成后怎么交给ViewRootImpl呢这里有个中间人——InputEventReceiver。在Java层ViewRootImpl内部维护了一个WindowInputEventReceiver它继承自InputEventReceiver。当Native层创建好MotionEvent后会回调Java层的onInputEvent()方法。流程是这样的Native层调用InputEventReceiver.dispatchInputEvent()JNI方法Java层的dispatchInputEvent()被调用参数就是刚刚创建的MotionEvent该方法内部调用onInputEvent()由WindowInputEventReceiver实现WindowInputEventReceiver将事件加入队列等待处理嗯这里要注意一个细节。事件并不是直接交给ViewRootImpl的。WindowInputEventReceiver内部有一个InputStage链。事件会经过多个处理阶段阶段名称作用ViewPreImeInputStage在输入法之前处理比如处理一些系统按键ImeInputStage交给输入法处理ViewPostImeInputStage输入法处理完后交给View层次SyntheticInputStage处理一些合成事件比如长按、双击等对于触摸事件最终会走到ViewPostImeInputStage的processPointerEvent()方法。这里会调用ViewRootImpl::enqueueInputEvent()把事件正式交给ViewRootImpl的事件队列。17.5 ViewRootImpl的最终处理事件入队与分发到了ViewRootImpl这里事件已经是一个完整的MotionEvent对象了。但ViewRootImpl不会立刻处理它。它会先把事件加入一个mPendingInputEvents队列然后通过scheduleConsumeBatchedInput()发起一个异步任务。为什么这么设计说白了是为了批量处理。如果用户快速滑动屏幕会产生大量MotionEvent。如果每个事件都立即处理UI线程会忙不过来。所以系统会把多个事件攒起来在下一个Vsync信号到来时一次性处理。最终在doConsumeBatchedInput()方法中ViewRootImpl会从队列中取出事件调用deliverInputEvent()。这个方法会找到当前拥有焦点的View或者根据坐标找到目标View然后调用View.dispatchTouchEvent()。至此事件从InputDispatcher到ViewRootImpl的旅程就结束了。接下来就是View层次内部的分发逻辑——那是下一章的内容。注意事项MotionEvent在跨进程传递时坐标是相对于屏幕的绝对坐标。但到了ViewRootImpl后需要转换成相对于窗口的坐标。这个转换发生在ViewRootImpl::deliverInputEvent()中通过mAttachInfo.mWindowLeft和mAttachInfo.mWindowTop进行偏移。如果你在开发中遇到触摸坐标不准的问题先检查一下窗口位置是否正确。17.6 小结回顾一下整个流程InputDispatcher从队列取出事件封装成InputMessageInputMessage通过InputChannel跨进程传递到应用进程Native层的InputEventReceiver解析InputMessage创建MotionEvent对象MotionEvent通过JNI回调到Java层的InputEventReceiver经过InputStage链最终到达ViewRootImpl的事件队列ViewRootImpl在Vsync回调中批量处理事件开始View层次的分发我个人觉得理解这个流程的关键在于“跨进程”和“异步”这两个概念。很多开发者以为触摸事件是实时响应的其实中间隔了好几层缓冲。这也是为什么有时候触摸会感觉“延迟”——嗯那是系统在等你下一个Vsync信号呢。
返回列表