iOS中的Touch响应全链路
Author:akinIan
部分知识源于网络学习,如有错误欢迎指正
一、前言
尝试学习了iOS响应触摸方式的某些步骤后,发现大部分文章都会选择性的省略某些步骤,阅读后还是会有一知半解的感觉,所以决定自己从头分析一下我们手中的iPhone是如何处理我们的每一次点击、滑动的。
二、交互过程
1. 从触摸屏幕,到系统对触摸事件的封装
我们手中的iPhone使用的屏幕,大都是 “电容触控屏”,这样的屏幕由一个覆盖着透明导电材料的感应网格组成,当我们的手指靠近或者触摸屏幕的时候,屏幕网格上的电场分布会变化,而手机就会通过检测到的电荷变化,来推测出手指的坐标位置。
采集到触摸信息后,通过 触摸控制器芯片 把采集到的信息,打包成标准输入事件,并且交给驱动、I/O Kit来处理。
而 I/O Kit 会通过与iOS的BackBoard的配合,由BackBoard来进行原始触摸数据的解析,并判断他属于哪个App的窗口。一旦系统判断发现了这个触摸事件应该交给前台App进行处理,那么就会将这个事件 转入UIKit层 来进行处理!
💡 SpringBoard 与 BackBoard
SpringBoard是iOS系统中的一个核心进程,主要负责管理应用程序的启动、切换和显示。它控制着主屏幕(Home Screen)的显示、应用图标的布局、通知中心等UI元素,可以说是用户与iOS系统交互的主要界面管理者。
BackBoard是iOS系统中的另一个重要组件,主要负责处理底层的触摸事件和硬件输入。它作为系统和硬件之间的中间层,接收来自触摸控制器的输入事件,并将其转发给合适的应用程序或系统组件。BackBoard的存在使得触摸事件的处理更加高效和统一。
这两个组件的协同工作,保证了iOS系统流畅的触摸响应体验:BackBoard负责接收和预处理触摸事件,而SpringBoard则负责根据这些事件执行相应的UI操作。
2. 当事件分配给App,初步进入UIKit层后…
- 在这里先介绍两个核心的概念:什么是UIApplication?什么是Runloop?
💡 UIApplication
UIApplication是iOS应用程序的核心对象,每个iOS应用都有且仅有一个UIApplication实例,它在应用启动时由系统自动创建。这个实例对象主要负责:
- 管理应用程序的生命周期(启动、挂起、恢复、终止等状态)
- 处理系统事件(包括触摸事件、远程通知等)
- 维护一个开放的URL列表(处理应用间通信)
- 协调其他高级app行为(如状态栏的显示、后台任务等)
在事件处理方面,UIApplication扮演着重要角色。当系统将触摸事件传递给应用程序时,事件会首先经过UIApplication对象。UIApplication会将这些事件放入事件队列中,然后在主线程的下一个运行循环中,将它们派发给合适的响应者对象进行处理。
UIApplication的这种设计保证了事件处理的有序性和可控性,同时也为开发者提供了一个统一的接口来管理应用程序的行为。
💡 Runloop
Runloop(运行循环)是iOS中一个核心的事件处理机制,它维护了一个持续运行的循环,用于处理输入事件、定时器和其他各种任务。其主要职责包括:
- 接收和处理各种事件(用户输入、系统事件等)
- 调度定时器的触发
- 处理UI更新
- 协调线程间的通信
每个线程都可以有自己的Runloop,但只有主线程的Runloop是默认创建并运行的。其他线程的Runloop需要手动创建和运行。
在事件处理的过程中,Runloop扮演着重要角色:它会不断地从事件队列中取出事件,将其分发给相应的处理者。当没有事件需要处理时,Runloop会使线程进入休眠状态,从而节省系统资源。一旦有新的事件到来,Runloop就会被唤醒,继续处理新的事件。
这种机制确保了应用程序能够持续响应用户输入,同时在没有任务时不会占用过多系统资源。对于触摸事件的处理,Runloop确保了事件能够按照正确的顺序被处理,并维护了整个事件响应链的稳定运行。
当前App的主线程runloop是基于 CFRunLoop
的,而UIKit会在 UIApplication
主循环中监听是否有新的事件到来 ,当一个触摸的事件到达这个 UIApplication
的时候,UIKit会将其封装为UITouch与UIEvent:
- UIEvent:表示一整个用户交互事件(包括多个手指)
- UITouch:表示单个手指的状态(起点、位置、阶段)
一个UIEvent里可能包含了多个UITouch,系统先创建UITouch,然后把这些touches封装进了UIEvent,再交给App
等当前的 UIEvent
封装好后,UIApplication
会把这个Event分发给当前的 UIWindow
,这个步骤就是我们常说的sendEvent:
💡 UIWindow
UIWindow是iOS应用程序中视图层次结构的最顶层容器,继承自UIView。每个iOS应用程序至少需要一个UIWindow实例(通常称为主窗口)来展示内容。UIWindow在视图层级中扮演着关键角色:
- 作为视图层次结构的根视图,所有的UI元素最终都必须添加到UIWindow上才能显示
- 负责协调视图之间的层级关系,管理视图控制器的切换
- 在事件响应链中扮演重要角色,接收来自UIApplication的事件并将其分发给合适的视图
在UIKit架构中,UIWindow与UIApplication的关系是这样的:
- UIApplication持有一个windows数组,包含了应用程序的所有窗口
- 其中keyWindow(现在更推荐使用windowScene.keyWindow)是当前接收键盘和其他非触摸事件的主窗口
当一个事件通过sendEvent到达UIWindow后,UIWindow会通过hitTest:withEvent:方法来确定第一响应者,这个响应者通常是用户触摸点所在的最上层的视图。
💡 sendEvent
sendEvent 是
UIApplication
和UIWindow
都实现的一个方法,它的完整声明是:
1 - (void)sendEvent:(UIEvent *)event;在UIApplication中,这个方法负责将接收到的事件分发给合适的UIWindow。而在UIWindow中,这个方法则负责将事件分发给具体的视图。
事件传递的基本流程是:
- UIApplication接收到系统传来的事件后,调用sendEvent:将事件传递给合适的UIWindow
- UIWindow通过自己的sendEvent:方法,结合hitTest:withEvent:的结果,将事件分发给具体的视图
- 目标视图接收到事件后,开始在响应链中进行处理
值得注意的是,这个方法通常不需要我们自己去调用或重写,它是UIKit框架内部用于事件分发的重要机制。
3. UIWindow是如何处理触摸event的?
在上一部分,UIApplication通过sendEvent,把事件分发给了当前活跃中的UIWindow:
1 | [[UIApplication sharedApplication] sendEvent:event]; |
从上边UIWindow的介绍,我们得知了 UIWindow
中也有 sendEvent:
方法,那么这里的sendEvent是在进行什么操作呢,首先我们先从一段伪代码引入这个问题
1 | - (void)sendEvent:(UIEvent *)event { |
这里我们可以看到,首先会从event中取出所有的touch,在 UITouchPhaseBegan
的时候,会先触发一次 hitTest:withEvent:
,找出哪个view最适合处理本次触摸,找到后将其设置为 touch.view
,并且接下来的Moved/Ended/Canceled都发送给这个View。
在这里,首先我们回来介绍一下 hitTest:withEvent:
这个方法是定义在UIView中的,并且所有的UIView子类(UIWindow也是UIView的子类!) 都会继承这个方法。每一个View在被调用这个方法的时候,都会执行以下几个步骤:
1 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { |
可以看出,整个hitTest就是一个从UIWindow开始,递归遍历其所有子View和子View的子View 的过程。
在hitTest的过程中,系统在寻找 “最合适的响应者” 的时候,优先考虑以下这几个条件:
- 可交互:
userInteractionEnabled == YES
- 可见:
hidden == NO
,alpha > 0.01
- 点击在范围内:
[self pointInside:point withEvent:event] == YES
- Z轴最上:
// 先检查最上层 subview(数组倒序)
- 最深处(递归):
// 会一直往下递归查找 subview 中最深的匹配者
4. 找到了最适合响应的View后,View会如何处理这个响应事件
在通过hitTest找到了这个View后:
- 系统将其赋值给touch.view
- 首先判断这个view有没有
UIGestureRecognizer
: - 如果这个View有
UIGestureRecognizer
,此时有一个关键的配置是 cancelsTouchesInView(默认为YES):以一个UITapGestureRecognizer *tap = ***
为例 - 如果tap.cancelsTouchesInView = YES
- 用户点击后,先执行
touchesBegan:
- GestureRecognizer 识别成功的时候 (GestureRecognizer识别失败则正常的进行touches方法 )
- UIKit 会发送
touchesCancelled:
给 View - View就不再接收后续事件了
- 用户点击后,先执行
- 如果tap.cancelsTouchesInView = NO
- 用户点击后,先执行
touchesBegan:
- 即使GestureRecognizer 识别成功,View依旧可以接收完整的touchesBegan、touchesMoved、touchesEnded;
- 双方可以并行处理
- 用户点击后,先执行
下边我们再回来介绍一下上边提到的UIGestureRecognizer:
首先,我们要先明确这个UIGestureRecognizer 是由用户添加而非系统自动添加的,开发者可以通过手动添加这些机制,更加高级、结构化的处理touch事件,比如:
- 单击、双击(
UITapGestureRecognizer
) - 长按(
UILongPressGestureRecognizer
) - 拖动(
UIPanGestureRecognizer
) - 缩放(
UIPinchGestureRecognizer
) - 旋转(
UIRotationGestureRecognizer
) - 轻扫(
UISwipeGestureRecognizer
)
在代码中,我们首先要创建并初始化手势识别器,并将其添加到View上:
1 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; |
此时,self.view接收到的tap事件就会由这个handleTap来处理了
除了使用UIGestureRecognizer之外,开发者也可以使用重写UIView方法的方式来进行自定义响应逻辑:
1 | - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { |
场景 | 推荐方法 |
---|---|
简单手势(点击、长按、滑动) | 使用 UIGestureRecognizer ,结构清晰、解耦 |
更复杂逻辑、手势状态控制、响应区域自定义 | 使用 touchesBegan: 系列方法手动处理 |
想做一些自定义响应(如事件穿透、模拟按钮) | 重写 hitTest: 或 pointInside: 并配合 touches 系列方法 |
三、事件处理过程 (技术角度)
那么经过了上边的第二部分,我们就已经梳理好了点击事件被手机响应的整个流程,那么现在我们再从技术视角总结一下这个过程,来形成更好的理解:
UIApplication层
触摸到达UIApplication,封装为UIEvent 与 UITouch:
1 | @interface UIEvent : NSObject |
最终事件通过 [UIApplication sendEvent:] 送入App的主事件循环
1 | - (void)sendEvent:(UIEvent *)event { |
UIWindow & UIView hitTest层
UIWindow开始调用自己的sendEvent,来分发事件(再次提醒UIWindow是UIView的子类)
1 | // UIWindow.m |
并且逐步递归的进行hitTest:
UIWindow.hitTest → UIView.hitTest(递归)→ 返回最深可响应 View
1 | // UIView.m |
UIView 响应层
找到了targetView后,并行或独立的触发Gesture和Touches方法
(取决于gestureRecognizer.cancelsTouchesInView)
1. GestureRecognizer
1 | for (UIGestureRecognizer *gr in view.gestureRecognizers) { |
- 系统调用内部私有方法
_receiveTouch:
(等价于touchesBegan:
) - 每个UIGestureRecognizer都注册自己的状态机(长按、滑动、双击等)
- 若识别成功,进入
.recognized
状态,并调用这个Recognizer的target-action
- 此时,若
gestureRecognizer.cancelsTouchesInView = YES
,则后续的touchesMoved
、touchesEnded
会被取消。否则照常执行。
2. View的touch响应
如果这个view没有注册GestureRecognizer,那么在第一步touchesBegan:
后,自然也不会有gesture识别成功,则会继续进入touchesMoved与touchEnded函数:
1 | touch.view → touchesBegan: |
view可以通过重写这些方法来自定义响应:
1 | - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { |
汇总
步骤 | 所在类 | 方法 |
---|---|---|
1 | UIApplication | sendEvent: |
2 | UIWindow | sendEvent: |
3 | UIView(递归) | hitTest:withEvent: |
4 | UIView(或其子类) | pointInside:withEvent: |
5 | UIGestureRecognizer | _receiveTouch: (监听识别) |
6 | UIView(或其子类) | touchesBegan: 等 |
四、结尾
那么到这里,完整的touch链路就被完全梳理完成了。如有错误欢迎勘误~