iOS 事件系统全景图(硬件 → UIKit → 控件)
一个用户手指触摸屏幕的事件,从硬件到应用层,大致的经历是:
scss
[ 触摸屏幕 ]
↓
[ IOKit -> IOHIDEvent ] (硬件事件)
↓
[ SpringBoard / BackBoard / SystemServer ] (系统事件中转)
↓
[ UIApplication → RunLoop Source → _UIApplicationHandleEventQueue ] (App 事件入口)
↓
[ UIKit 生成触摸序列 ] (UITouch / UIEvent)
↓
[ UIWindow → UIView ] (事件传递机制: hitTest / pointInside)
↓
[ UIGestureRecognizer ] (手势识别 / 状态机 / 冲突处理)
↓
[ UIResponder ] (响应链: touchesBegan / nextResponder)
↓
[ UIcontrol → Target-Action ] (控件事件)
| 模块 | 关键词 | 代表类 |
|---|---|---|
| 硬件输入系统 | IOKit / HID / RunLoop Source | --- |
| 触摸事件系统 | Touch / Phase / Event | UITouch / UIEvent |
| 事件传递机制 | hitTest / pointInside | UIView / UIWindow |
| 手势识别机制 | state / requireToFail / delegate | UIGestureRecognizer 系列 |
| 响应链机制 | nextResponder / touches | UIResponder / UIViewController |
| 控件事件系统 | target-action / sendActions | UIControl / UIButton |
| RunLoop驱动层(补充) | CFRunLoopSource, Observer | CFRunLoop, UIApplication |
一、硬件输入系统
- IOKit / HID 驱动 负责把物理触摸信号转成
IOHIDEvent; - 这些
IOHIDEvent由 backboardd 转发给前台进程(Your App); - 主线程 RunLoop 注册了
_UIApplicationHandleEventQueue()作为输入源,接收事件。
二、触摸事件系统
iOS 的输入事件分为几种类型:
| 类型 | 描述 | 相关类 |
|---|---|---|
| Touch | 单指/多指触摸 | UITouch |
| Press | 按压 | UIPress |
| Motion | 摇一摇、重力加速度 | UIEventSubtypeMotionShake |
| Remote Control | 耳机线控 / 外设 | UIEventSubtypeRemoteControl |
-
UITouch- 每根手指独立对应一个
UITouch对象 - 保存触摸状态、位置、timestamp、phase、唯一 identifier
- phase 会随手指动作变化(Began → Moved → Ended/Cancelled)
- 每根手指独立对应一个
-
触摸序列 (Touch Sequence):一个概念(用来描述 "一次连续的触摸过程")
- 单指连续触摸,从手指接触到抬起或取消
- 对应一个
UITouch对象的完整生命周期
-
多指触摸
- 每根手指都有自己的
UITouch→ 多个触摸序列并行 UIEvent封装同一时间点的所有触摸
- 每根手指都有自己的
-
UIEvent
- 一个
UIEvent对象封装一批同时发生的UITouch(或 presses/motion/remote 控件事件) - event.timestamp = 事件发生的时间点
- event.type = touches / presses / motion / remoteControl
- 一个
三、UIKit 分发层(事件传递机制)
UIKit 在接收到事件后开始做「命中检测」🎯
其 核心调用链 是:
objectivec
UIApplication sendEvent:
↓
UIWindow sendEvent: // 从 window 开始
↓
hitTest:withEvent: // 做递归「命中检测」🎯
↓
pointInside:withEvent:
hitTest:规则(可交互条件): 1. view.userInteractionEnabled == YES 2. view.hidden == NO 3. view.alpha > 0.01 4. pointInside == YES- 倒序遍历 subviews,返回最上层命中的 view。
- 将得到的 view 作为 First Responder 候选人。
四、手势识别层(UIGestureRecognizer 系列)
- 核心思想 :手势识别发生在 时间传递后、响应链前 ;手势识别器监听 触摸序列,根据预设规则判断是否满足手势条件。
每个手势识别器都有一套状态机和冲突调度逻辑(手势冲突)
状态机(UIGestureRecognizerState)
| 状态 | 含义 | 触发时机 |
|---|---|---|
| .Possible | 初始状态 | 等待识别开始 |
| .Began | 识别开始 | 手势识别成功,手势开始响应 |
| .Changed | 手势进行中 | 位置/角度变化中 |
| .Ended | 识别完成 | 手势完成(抬手、离开) |
| .Cancelled | 被系统或上层取消 | 如中断或手势冲突 |
| .Failed | 未识别成功 | 条件不满足(时间太短、移动太远) |
- 状态迁移 大致是:
markdown
Possible → Began → Changed → Ended
→ Failed
→ Cancelled
手势冲突与协调机制
多个手势可能同时附着在 同一视图/同一层级 上,系统需要协调 "谁可以先响应"。
- 手势关系 :每个
UIGestureRecognizer都有一个「关系图」,由以下规则控制:
| 规则 | 方法 | 含义 |
|---|---|---|
| 失败依赖 | requireGestureRecognizerToFail: |
让某个手势必须等待另一个手势失败后再识别 |
| 同时识别 | gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: |
允许多个手势同时识别 |
| 禁止识别 | gestureRecognizer:shouldReceiveTouch: |
完全忽略某次触摸 |
| 优先识别 | gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer: |
指定优先级关系 |
-
优先级调度
- 根据依赖关系构建「手势图」;
- 同步触摸输入,驱动每个手势的状态机;
- 当有手势识别成功后,让互斥手势进入 .Failed。
-
举例:在 scrollview 上增加 tap 手势。 [tap requireGestureRecognizerToFail:scrollView.panGestureRecognizer];
- 表示「滚动优先于点击」;只有 pan 失败后,tap 才能触发。
手势与 Touch 的竞争关系
| 场景 | 结果 |
|---|---|
| ✅手势识别 success | 手势回调触发,touches 系列不会再调用 |
| ❌手势识别 failure | 事件进入响应链,触发 touches 系列方法(touchesBegan / Moved / Ended) |
| ❌手势识别 cancel | 调用touchesCancelled,touches 系列不会再调用 |
手势识别器接管 触摸序列 之后,UIKit 不会再把 touches 事件下发给视图层。
五、响应链机制(Responder Chain)
当手势识别失败后,触摸事件才能进入 UIResponder。
1️⃣ 事件流向(子 -> 父)

objectivec
1 - UIView
→ 2 - UIViewController (若有)
→ 3 - UIWindow
→ 4 - UIApplication
→ 5 - AppDelegate
- 如果当前 responder 不处理事件,会传递给 nextResponder。
六、控件事件系统(UIControl)
objectivec
UIControl → UIView → UIResponder → NSObject
UIKit 在响应链之上又封装了一层抽象机制:Target-Action。
UIButton/UISwitch/UISlider等继承自UIControl。UIControl通过 touches 系列方法 监控触摸,然后触发事件。
流程:
css
[ 触摸序列 → UIView (touchesBegan/Moved/Ended) ]
↓
[ UIControl (拦截触摸) ]
↓
判断事件类型
↓
[sendActionsForControlEvents:]
↓
执行注册的 Target-Action 回调
控件事件类型 (常用):
| 类型 | 时机 |
|---|---|
| TouchDown | 手指按下 |
| TouchUpInside | 在控件内抬起(最常用) |
| ValueChanged | 值改变(Slider/Switch) |
思考🤔:为什么在 UIScrollView 上的 UIButton 事件响应有延迟?
现象:
- 点击按钮 → 高亮/触发 action 延迟约 100~200ms
- 滑动触发滚动时,按钮点击可能被"吃掉"
原因分析
| 控件 | 事件处理机制 |
|---|---|
| UIScrollView | 内部有 UIPanGestureRecognizer 判断拖动;默认 delaysContentTouches = YES,会延迟将 touchesBegan 传给子控件 |
| UIButton | 依赖 touchesBegan/Moved/Ended 来管理高亮和触发 action;无法立即处理 touches,如果手势被占用,可能收到 touchesCancelled |
✅ 核心点:
UIScrollView先抢占触摸 → 拖动手势触发 →UIButton延迟或取消事件。UIButton事件依赖 触摸序列未被取消 才能触发 target-action。
为什么 UIScrollView 先抢占触摸 ?
- hitTest 结果
- 手指点击在
UIButton上 → 通过事件传递机制 → 设置UIButton为 First Responder 候选人 - 但是
UIScrollView内部的 panGestureRecognizer 也会监听同一触摸序列:- 手势识别器在
touchesBegan延迟期间观察手势意图;- 如果 panGesture 成功,
UIKit会将触摸序列会被标记 "被UIScrollView占用" → UIButton 收到touchesCancelled。 - 如果 panGesture 失败,触摸序列被
UIButton占有。
- 如果 panGesture 成功,
- 手势识别器在
- 手指点击在
这个延迟可以通过 UIScrollView 的 delaysContentTouches 字段取消掉。