事件分发
Java层回答
- 当用户点击屏幕时, 将产生事件序列, 包括DOWN、MOVE、UP、CANCEL
- 事件分发的本质, 其实就是将事件列传递到某个具体View的过程
- 事件分发主要由三个方法协作完成: dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent
- dispatchTouchEvent 用于分发点击事件
- onInterceptTouchEvent 在 dispatchTouchEvent 内部调用, 是 ViewGroup 独有, 用于 ViewGroup 判断是否拦截事件
- onTouchEvent 在 dispatchTouchEvent 内部调用, 用于处理事件序列
- 如果想要聊清楚事件分发, 就需要从头到尾梳理, 这里面有几个关键点
- Activity 事件分发机制
- ViewGroup 事件分发机制
- View 事件分发机制
- 用户点击屏幕 (ACTION_DOWN)
- ViewRootImpl
- ↓ DecorView
- ↓ Activity.dispatchTouchEvent
- ↓ PhoneWindow.superDispatchTouchEvent
- ↓ DecorView.superDispatchTouchEvent
- ↓ 根ViewGroup.dispatchTouchEvent
- return true
- 事件被消费, Activity.dispatchTouchEvent 返回 true, 事件分发结束
- return false
- 事件未消费, 执行 Activity.onTouchEvent
- ViewGroup.dispatchTouchEvent
- ↓ 条件判断: disallowIntercept || !onInterceptTouchEvent()
- /* disallowIntercept默认false, 可经 ViewGroup.requestDisallowInterceptTouchEvent 设置为true, 用于强制关停ViewGroup的onInterceptTouchEvent的事件拦截控制 */
- true -> ViewGroup 不拦截事件
- ↓ 倒序遍历子View
- ↓ 判断当前遍历View是否正在点击
- ↓ 执行并判断某子View.dispatchTouchEvent
- true: 该子View消费了点击事件, mFirstTouchTarget 记录为该View, 方便相同事件序列的事件快速被消费, 而不需要再次遍历
- false, 该子View不消费点击事件
- 子View们不消费事件, 执行ViewGroup之父类View的dispatchTouchEvent
- false -> 拦截事件
- 会执行ViewGroup.super.dispatchTouchEvent
- 调用父View.dispatchTouchEvent
- 先检查 OnTouchListener,
- 返回 true,事件消费, 分发流程结束
- 返回 false
- 调用 onTouchEvent
- 处理在 ACTION_UP 时触发点击 performClick
- performClick 内部触发 OnClickListener
- View.dispatchTouchEvent
- 是否外部注册了 onTouch 事件监听 (View.registerOnTouchListener())
- 是
- onTouch 返回 true
- View.dispatchTouchEvent 返回true, 消费该事件
- onTouch 返回 false
- 往下走, 执行 View.onTouchEvent
- 否
硬件层回答
- 用户执行按下、滑动行为. 硬件层产生按下、滑动事件
- 内核层
- 事件到达 Native 层, InputDispatcher 通过 InputChannel (Socket对的服务端) 将事件发送到应用进程的Native缓存
- 主线程Looper执行到MessageQueue.next.nativePollOnce, 会通过 NativeInputEventReceiver 读取 Socket 数据
- 如果是Down事件
- 直接回调给 Java 层的 ViewRootImpl.WindleInputEventReceiver.onInputEvent
- 执行enqueueInputEvent, 传入的 “立即执行” 参数为true
- 这使得接下来执行 doProcessInputEvents , 他不会发送异步消息, 更不会等待, 而是直接冲进分发流程
- 如果是Move事件
批处理 + 异步消息, 追求渲染流畅性(对齐 VSYNC 分发)
- 不直接回调给 Java 层的 ViewRootImpl.WindleInputEventReceiver.onInputEvent, 而是将事件合并, 然后回调给ViewRootImpl.WindleInputEventReceiver.onBatchedInputEventPending
- 这个方法会调用 ViewRootImpl.scheduleConsumeBatchedInput, 发送一个消费Batch队列的任务(ConsumeBatchedInputRunnable)消息, type是CALLBACK_INPUT
- 当 VSYNC 信号到来, 底层唤醒主线程, Choreographer 会严格按下面优先级调度
- CALLBACK_INPUT: 处理 ACTION_MOVE 的分发、处理手势计算
- CALLBACK_ANIMATION: 执行 ObjectAnimator、ValueAnimator 的数值更新。
- CALLBACK_INERTS_ANIMATION: 处理软键盘、状态栏弹出过程中的位移动画。
- CALLBACK_TRACERSAL: 执行 ViewRootImpl 的 Measure、Layout、Draw。
- CALLBACK_COMMIT: 最后的清理工作(现在版本较少使用)
- Choreographer.CALLBACK_INPUT 被触发, 会执行 ViewRootImpl.ConsumeBatchedInputRunnable.run,
- 这里面先执行 consumeBatchedInputEvents, 将 Native 中的 Batch队列 捞出来放进Java层的 mPendingInputEventHead 链表中
- 然后就会执行到 doProcessInputEvents 遍历链表中的事件进行分发, 开始 DecorView -> Activity -> PhoneWindow -> DecorView -> ViewGroup -> View 这个大家熟悉的事件分发流程了
- 总结:
- 流程:
- 硬件触摸信号
- (Linux kernel) InputChannel
- (Native) InputDispatcher
- (Native) Batch 队列
- (Java) ViewRootImpl.WindleInputEventReceiver.onBatchedInputEventPending
- (硬件) VSYNC
- (Java) Choreographer.CALLBACK_INPUT
- (Java) ViewRootImpl.ConsumeBatchedInputRunnable
- Batch 队列 -> 事件链表
- doProcessInputEvents
- deliverInputEvent
- Java 层标准事件分发流程
- 为什么这么设计 MOVE 事件分发
- 解决 “生产” 与 “消费” 速率不匹配问题
- 现在主流手机采样率(如 240Hz)远高于刷新率(如 60Hz)。这意味着硬件每秒产生 240 个 MOVE 事件,但显示器每秒只能消费(渲染)60 帧。
- 如果每一条 MOVE 都触发一次分发和 View 树重绘,主线程会进行 180 次无效的计算。因为即便 View 状态更新了,屏幕由于没到 VSYNC 信号也不会刷新,用户根本看不见中间的变化。
- 这种“强行刷新”会导致主线程消息堆积。240 个事件会产生 240 个高优先级的任务,将真正重要的“渲染任务”挤到后面。一旦主线程处理这些冗余事件的时间超过 16.6ms,就会错过 VSYNC 信号,产生明显的掉帧(Jank)
- Android 引入 批处理 + VSYNC + Choreographer, 将多个MOVE事件在下一个VSYNC时刻中处理, 实现极致的流程性
- 这种设计把事件处理从‘事件驱动’转变成了‘时钟驱动’(VSYNC 驱动)。不再被动地被上百个滑动信号牵着走,而是主动地按照显示的节奏去提取数据。这才是解决滑动卡顿的终极方案。
总结
doProcessInputEvents 之后的分发流程
面试题
- 面试官: View注册了onTouch监听、onClick监听, onTouch中返回false, onClick会执行吗?
- 我: 会, onTouch返回false, 会执行View.onTouchEvent, 内部逻辑会执行View.performClick, 然后检查是否注册点击监听, 有的话会回调onClick
- 面试官: 那返回true呢?
- 我: 不会, onTouch返回true, View.dispatchTouchEvent就会直接返回true, 这样就不会再执行View.onTouchEvent了, 也就不会回调onClick
- 面试官: DOWN、MOVE、UP 在流程中怎么处理的?
- 我:
- DOWN 是一次事件序列的起始点, 中间会有0或多次的MOVE, 事件序列最终结束于UP或CANCEL
- DOWN、UP 是快速执行的, 不存在批处理 和 异步消息
- MOVE 为追求稳定, 需要 VSYNC + Choreographer 组合起来, 对MOVE事件进行批处理 + 异步消息, 处理采样率和刷新率不一致的问题,
- 面试官: 当某子View消费了Down事件, 后续ViewGroup的Move、Up会怎么分发?
- 我:
- 在父ViewGroup明确不消费后续Move、Up的条件下, 某许的Move、Up都会交给该子View处理
- 因为如果某个子View消费了DOWN事件, 会被记为mFirstTouchTarget, 在父ViewGroup.onInterceptTouchEvent不拦截MOVE、UP的条件下, 后续的MOVE、UP都会交给该子View消费
- 面试官: 既如此, 那父ViewGroup拦截了Move, 消费Down事件的子View会怎样?
- 我: 消费Down事件的子View, 会收到Cancel的事件, 其他无关子View, 不会收到任何事件
- 面试官: 聊一聊这个mFirstTouchTarget字段
- 当ViewGroup.dispatchOnTouchEvent收到Down事件
- 会将mFirstTouchTarget置为null
- 判断ViewGroup是否拦截
- 拦截: mFirstTouchTarget会一直为null, 事件最终都由该ViewGroup之父类View的dispatchTouchEvent处理, 最终消不消费, 还是由onTouchEvent决定
- 不拦截: 倒序到消费的子View, 找不到, 事件还由ViewGroup处理, 找得到, mFirstTouchTarget置为该子View的包装对象, 事件也给该子View处理
- 收到后续的Move、Up事件
- 检查ViewGroup是否拦截
- 不拦截, 交给mFirstTouchTarget处理
- 拦截, ViewGroup自己处理, mFirstTouchTarget收到Cancel事件
- 面试官: 聊一下从硬件层到软件层的点击事件传递
硬件层 -> Linux内核 -> Android系统服务 -> 应用进程(跨进程接收 -> 主线程接收 -> 视图树分发 -> 事件消费 -> 反馈)
- 我
- 从硬件层, 到框架层, 再到应用层描述一下
- 硬件:
- 用户触摸屏幕, 电容屏触发控制器产生电信号, 触发硬件中断
- Linux 内核:
- “中断服务程序” 读取原始坐标数据, 通过 “输入子系统” 将其标准化为 input_event , 并写入 /dev/input/eventX 设备节点
- Android系统服务:
- EventHub 通过 epoll 监听 /dev/input/ 下的设备文件, 读取原始的RawEvent
- InputReader 在一个16ms间隔的loop中, 从EventHub中得到RawEvent并加工
- 包括: 坐标校准、去抖、多点触摸跟踪、手势初步识别(单击长按)
- 数据封装成 NotifyMotionArgs 后, 调用InputDispatcher.notifyMotion进行事件分发
- InputDispatcher 分发事件
- 想WMS查找触摸点最顶点可触摸的窗口
- 通过InputChannel(一对Socket的Server端)将事件发到目标应用进程
- 如果是ACTION_DOWN, 会同时启动一个5s的ANR计时器
- 应用进程:
- 跨进程接收: 应用进程的 ViewRootImpl 通过 InputEventReceiver 监听 InputChannel(一对Socket的Client端)
- 主线程接收❌❌❌: ViewRootImpl 将事件包装成 MotionEvent 对象, 并通过 Handler 发送给主线程
- DOWN 是直接执行的
- MOVE 才会被(批处理 + 异步消息)发送给主线程Handler, 在下一个VSYNC优先执行
- 应用视图树分发:
- ViewRootImpl 将事件传递给根视图 DecorView.dispatchTouchEvent
- DecorView 将事件传递给 Activity.dispatchTouchEvent
- Activity 将事件传递给 PhoneWindow.dispatchTouchEvent
- PhoneWindow 再将事件传回给 DecorView
- 从 DecorView(一个FrameLayout) 开始, 就进入了标准的 ViewGroup 事件分发流程了
- 事件消费与反馈:
- 事件消费:
- 最终消费的 View.dispatchTouchEvent 会返回true, 事件记为被消费, 分发流程结束
- 如果找不到消费该事件的 View, 事件记为未被消费, 分发流程结束
- 反馈
- ViewRootImpl 通过 finishInputEvent(handled:Boolean) 向 InputDispatcher 发送结束结束信号
- InputDispatcher 会停止该事件的 ANR 计时
- 如果ACTION_DOWN不消费, 那接下来的整个事件序列会怎样?
- 基于 “指责分离原则” , InputDispatcher 依旧会给应用进程的 ViewRootImpl 发送事件, 但是去到ViewGroup.dispatchTouchEvent 后会执行“快速失败”流程, 即直接走mFirstTouchTarget==null的路径, 直接执行 ViewGroup 父类 View 的dispatchTouchEvent
- 面试官: 为什么是 16 ms
- 面试官: ViewRootImpl 中的 InputEventReceiver 和 Choreographer 如何协同?同步屏障(SyncBarrier)在事件处理中起什么作用?
- 我
- 针对ACTION_DOWN, 是快速直达的, 立即响应, 在主线程Looper执行到MessageQueue.next时, 通过NativeInputEventReceiver拿到DOWN事件回调给ViewRootIMpl.WindowInputEventReceiver.onInputEvent, 这个方法最后会直接冲进事件分发流程, 执行DecorView往后的dispatchTouchEvent
- 针对ACTION_MOVE, 因为存在屏幕采样率和屏幕刷新率不一致的问题, 因此更注重稳健性, 不允许一次采样就刷新一次屏幕的 “事件驱动” 机制, 而是引入VSYNC + Choreographer这种 “时间驱动”机制, 将一次次的MOVE事件择机打包, 在VSYNC触发时由Choreopher协调MOVE事件包怎么执行
- MOVE跟DOWN事件不一样, 不一直接执行, 而是会择机将一次次的MOVE采样打包, 并将处理任务当成异步任务发送到主线程Handler, 在VSYNC信号来临时, 由 Choreograher 协调执行, 当MessageQueue中存在同步屏障, 会往后遍历最近的异步消息, 这里同步屏障的作用就是为了MOVE事件快点处理
- 面试官: 为什么 View.post 时才能拿到View的宽高?
- 我: 因为View.post发送的Message是在TRAVERSAL的Message后面执行的, TRAVERSAL进行了measure\layout\draw
- 面试官: ViewRootImpl.WindleInputEventReceiver.onInputEvent 和 onBatchedInputEventPending 分别用于处理什么事件?
- onInputEvent
- 处理的即时响应的事件
- ACTION_MOVE
- ACTION_UP
- KeyEvent: 返回键、音量键等物理按钮输入
- onBatchedInputEventPending