一、硬件层 → IOKit 层(物理信号转系统事件)
- 手指触摸屏幕:手指与电容屏接触,改变触摸点下方的电容值。
- 硬件驱动采样:屏幕控制芯片(如基于 I2C 接口的触摸控制器)以高频率(如 120Hz 或 240Hz)采样触摸坐标、压力、手指编号等信息。
- 中断触发 → IOKit 读取:硬件通过中断通知 CPU,IOKit 框架中的触摸设备驱动程序读取硬件寄存器,获取原始触摸数据。
- 封装成 IOHIDEvent:驱动程序将原始数据包装成一个 IOHIDEvent 对象(HID 是 Human Interface Device 的缩写)。该对象包含:
-
- 事件类型(kIOHIDEventTypeDigitizer)
-
- 时间戳
-
- 触摸阶段(开始/移动/结束/取消)
-
- 坐标(经过屏幕校准)
-
- 手指 ID、压力等
- 发送给 SpringBoard:IOKit 通过 mach port(内核与用户空间的通信通道)将 IOHIDEvent 发送给 SpringBoard 进程,SpringBoard 是 iOS 的桌面管理程序,负责接收所有系统事件。
二、系统层(SpringBoard)→ 应用层(App)
- SpringBoard 决定目标 App:SpringBoard 维护当前前台应用的进程 ID,它根据触摸事件的屏幕坐标,结合当前屏幕层级(如锁屏界面、通知中心、控制中心、当前 App 的窗口),判断事件应该送给哪个 App。
- 通过 mach port 转发事件:SpringBoard 将 IOHIDEvent 再次封装,并通过目标 App 的 mach port 发送给该 App 的 UIApplication 进程。
- App 的 RunLoop 接收事件:App 的主线程 RunLoop 注册了负责接收事件的 source1(基于 mach port 的输入源),当事件到达,RunLoop 被唤醒,调用对应的回调函数。
- UIApplication 将系统事件转换为 UIEvent:系统回调将 IOHIDEvent 转换为 UIEvent 对象,内部包含一个或多个 UITouch 对象。每个 UITouch 记录了触摸的位置(在 UIWindow 中的坐标)、阶段、时间、力度等。
- UIApplication 将事件放入队列:UIApplication 将生成的 UIEvent 添加到由它管理的 事件队列(NSEventQueue)中。队列遵循 FIFO(先进先出),确保事件按顺序处理。
- 主线程 RunLoop 处理事件:RunLoop 在循环中检测到事件队列非空,取出最前面的 UIEvent,然后调用 UIApplication 的 sendEvent: 方法开始分发。
三、App事件分发与响应链
① UIWindow 接收事件
- UIApplication 的 sendEvent: 会将事件发送给当前关键的 UIWindow 对象(通常是 keyWindow);
- UIWindow 继承自 UIView,它的 sendEvent: 方法开始处理事件。
② Hit-Testing(寻找初始响应者)
- Hit-Testing 这是最关键的一步,UIWindow 调用 hitTest:withEvent: 方法,递归查找触摸点所在的最终视图。
- hitTest:withEvent: 的内部逻辑(按顺序执行):
-
- 调用 pointInside:withEvent: 检查触摸点是否在当前视图的 bounds 内,若返回 NO,则直接返回 nil,不再检查子视图;
若返回 YES,则进入下一步。
- 调用 pointInside:withEvent: 检查触摸点是否在当前视图的 bounds 内,若返回 NO,则直接返回 nil,不再检查子视图;
-
- 从当前视图的 subviews 数组的末尾(即最上层子视图)开始倒序遍历,对每个子视图递归调用 hitTest:withEvent:,只要某个子视图返回非 nil,立即停止遍历,将该子视图返回给上层。
-
- 如果所有子视图都返回 nil,则当前视图自己就是命中视图,返回 self。
- 视图结构 A(父)→ B(子)→ C(孙),触摸点在 C 内:
-
-
A hitTest:\] 发现点在 A 内,遍历子视图 B;
-
B hitTest:\] 发现点在 B 内,遍历子视图 C;
-
C hitTest:\] 发现点在 C 内,C 没有子视图,返回 C;
- B 收到 C,返回 C;
-
-
- A 收到 C,最终 hitTest 返回 C。
- 如果视图设置了 userInteractionEnabled = NO、hidden = YES、alpha < 0.01,则 hitTest: 直接返回 nil,视为不存在。
③ 确定最终响应者
- UIWindow 的 sendEvent: 得到 hitTest 返回的视图,这个视图就是 第一响应者(first responder)。
④ 事件传递给第一响应者
- UIWindow 调用第一响应者的 touchesBegan:withEvent: 方法(根据触摸阶段可能调用 touchesMoved:、touchesEnded:、touchesCancelled:)。
⑤ 响应者链(Responder Chain)的介入
- 如果第一响应者没有实现 touchesBegan:(或其默认实现调用 super),事件会沿响应者链向上传递。
- 响应者链的传递路径:
swift
第一响应者(UIView)
→ 它的父视图(UIView)
→ 父视图的视图控制器(UIViewController,如果存在)
→ 根视图(UIView)
→ UIWindow
→ UIApplication
→ AppDelegate(如果实现了 UIResponder 方法)
- 每个响应者都可以通过重写 touches* 方法并不调用 [super ...] 来拦截事件,或者调用 super 继续向上传递。
⑥ 手势识别器(UIGestureRecognizer)的介入
- 在 UIWindow 将事件发送给第一响应者之前,UIWindow 会先将事件传递给附加在视图上的手势识别器。
- 处理顺序:
-
- UIWindow 的 sendEvent: 会先遍历视图及其祖先视图上的所有手势识别器;
-
- 手势识别器以并行方式分析触摸流(UIGestureRecognizerState 变化);
-
- 如果某个手势识别器识别成功(状态变为 Ended),它会接管事件。默认情况下,系统会取消原本要发给视图的触摸(发送 touchesCancelled:),然后调用手势识别器的 action 方法;
-
- 可以通过 delaysTouchesBegan / delaysTouchesEnded 等属性调整手势与视图触摸的时序关系。
⑦ UIControl(控件)的特殊处理
- 对于 UIButton、UISlider 等 UIControl 子类,它们重写了 touches* 方法,在内部实现 Target-Action 机制。当触摸事件满足一定条件(如抬起时仍在按钮内),控件会发送 action 消息(如 touchUpInside)。
- 优先级总结(从高到低):
-
- UIControl 的 Target-Action(内部完全处理,不会传递);
-
- 手势识别器(比视图的 touches 方法更早识别);
-
- 视图的 touchesBegan: 等响应者方法。
⑧ 事件处理完成
- 一旦某个响应者或手势识别器消耗了事件(不再向上传递或调用 super),事件生命周期结束,RunLoop 继续处理下一个事件。
四、补充说明
① 多个手指(多点触控)
- UIEvent 中包含一个 touches 集合,每个 UITouch 对应一根手指。
- 通过 UIView 的 multipleTouchEnabled 属性控制是否允许多点触控。
④ 3D Touch / Haptic Touch
- UITouch 提供 force 属性(力度值)。
- 系统会额外发送 UIEventTypePressure 事件。
④ 主动取消(如弹窗出现)
- 当系统弹出模态视图(如系统权限弹窗)或调用 UIApplication 的 beginIgnoringInteractionEvents,当前触摸会被取消,第一响应者收到 touchesCancelled:withEvent:。
⑤ 跨视图跟踪(UIScrollView为例)
- UIScrollView 在 hitTest 中返回自身或子视图后,会在 touchesBegan: 中启动一个 timer,判断用户是否开始拖动。
- 若判断为拖动,UIScrollView 会调用 [super touchesCancelled:...] 取消子视图的响应,并自己处理移动事件。