iOS 知识点 - 输入事件系统(UIKit 事件传递机制、响应链机制)

iOS 事件系统全景图(硬件 → UIKit → 控件)

一个用户手指触摸屏幕的事件,从硬件到应用层,大致的经历是:

[ 触摸屏幕 ]
    ↓
[ 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;
  • 这些 IOHIDEventbackboardd 转发给前台进程(Your App);
  • 主线程 RunLoop 注册了 _UIApplicationHandleEventQueue() 作为输入源,接收事件。

二、触摸事件系统

iOS 的输入事件分为几种类型:

类型 描述 相关类
Touch 单指/多指触摸 UITouch
Press 按压 UIPress
Motion 摇一摇、重力加速度 UIEventSubtypeMotionShake
Remote Control 耳机线控 / 外设 UIEventSubtypeRemoteControl
  1. UITouch

    • 每根手指独立对应一个 UITouch 对象
    • 保存触摸状态、位置、timestamp、phase、唯一 identifier
    • phase 会随手指动作变化(Began → Moved → Ended/Cancelled)
  2. 触摸序列 (Touch Sequence):一个概念(用来描述 “一次连续的触摸过程”)

    • 单指连续触摸,从手指接触到抬起或取消
    • 对应一个 UITouch 对象的完整生命周期
  3. 多指触摸

    • 每根手指都有自己的 UITouch → 多个触摸序列并行
    • UIEvent 封装同一时间点的所有触摸
  4. UIEvent

    • 一个 UIEvent 对象封装一批同时发生的 UITouch(或 presses/motion/remote 控件事件)
    • event.timestamp = 事件发生的时间点
    • event.type = touches / presses / motion / remoteControl

三、UIKit 分发层(事件传递机制)

UIKit 在接收到事件后开始做「命中检测」🎯

核心调用链 是:

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 未识别成功 条件不满足(时间太短、移动太远)
  • 状态迁移 大致是:
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️⃣ 事件流向(子 -> 父)

image

1 - UIView
    → 2 - UIViewController (若有)
    → 3 - UIWindow
    → 4 - UIApplication
    → 5 - AppDelegate
  • 如果当前 responder 不处理事件,会传递给 nextResponder

六、控件事件系统(UIControl

UIControl → UIView → UIResponder → NSObject

UIKit 在响应链之上又封装了一层抽象机制:Target-Action

  • UIButton/UISwitch/UISlider 等继承自 UIControl
  • UIControl 通过 touches 系列方法 监控触摸,然后触发事件。

流程:

[ 触摸序列 → 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 上 → 通过事件传递机制 → 设置 UIButtonFirst Responder 候选人
    • 但是 UIScrollView 内部的 panGestureRecognizer 也会监听同一触摸序列:
      • 手势识别器在 touchesBegan 延迟期间观察手势意图;
        • 如果 panGesture 成功,UIKit 会将触摸序列会被标记 “被 UIScrollView 占用” → UIButton 收到 touchesCancelled
        • 如果 panGesture 失败,触摸序列被 UIButton 占有。

这个延迟可以通过 UIScrollViewdelaysContentTouches 字段取消掉。

posted @ 2025-12-05 17:22  齐生  阅读(3)  评论(0)    收藏  举报