iOS Touch响应逻辑解析
Keep Team Lv1

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 是 UIApplicationUIWindow都实现的一个方法,它的完整声明是:

1
- (void)sendEvent:(UIEvent *)event;

在UIApplication中,这个方法负责将接收到的事件分发给合适的UIWindow。而在UIWindow中,这个方法则负责将事件分发给具体的视图。

事件传递的基本流程是:

    1. UIApplication接收到系统传来的事件后,调用sendEvent:将事件传递给合适的UIWindow
    1. UIWindow通过自己的sendEvent:方法,结合hitTest:withEvent:的结果,将事件分发给具体的视图
    1. 目标视图接收到事件后,开始在响应链中进行处理

值得注意的是,这个方法通常不需要我们自己去调用或重写,它是UIKit框架内部用于事件分发的重要机制。



3. UIWindow是如何处理触摸event的?

在上一部分,UIApplication通过sendEvent,把事件分发给了当前活跃中的UIWindow:

1
[[UIApplication sharedApplication] sendEvent:event];

从上边UIWindow的介绍,我们得知了 UIWindow 中也有 sendEvent: 方法,那么这里的sendEvent是在进行什么操作呢,首先我们先从一段伪代码引入这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)sendEvent:(UIEvent *)event {
NSSet *allTouches = [event allTouches];
for (UITouch *touch in allTouches) {
switch (touch.phase) {
case UITouchPhaseBegan:
// 查找第一响应者
UIView *target = [self hitTest:[touch locationInView:self] withEvent:event];
touch.view = target;
[target touchesBegan:...];
break;

case UITouchPhaseMoved:
[touch.view touchesMoved:...];
break;

case UITouchPhaseEnded:
[touch.view touchesEnded:...];
break;

...
}
}
}

这里我们可以看到,首先会从event中取出所有的touch,在 UITouchPhaseBegan 的时候,会先触发一次 hitTest:withEvent: ,找出哪个view最适合处理本次触摸,找到后将其设置为 touch.view ,并且接下来的Moved/Ended/Canceled都发送给这个View。

在这里,首先我们回来介绍一下 hitTest:withEvent:

这个方法是定义在UIView中的,并且所有的UIView子类(UIWindow也是UIView的子类!) 都会继承这个方法。每一个View在被调用这个方法的时候,都会执行以下几个步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1. 是否能响应事件?
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
return nil;
}

// 2. 触点是否在视图范围内?
if (![self pointInside:point withEvent:event]) {
return nil;
}

// 3. 从上到下遍历子视图(Z序倒序),递归查找最深处命中的子视图
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
if (hitView) {
return hitView;
}
}

// 4. 没找到更合适的子视图,自己处理
return self;
}

可以看出,整个hitTest就是一个从UIWindow开始,递归遍历其所有子View和子View的子View 的过程。

在hitTest的过程中,系统在寻找 “最合适的响应者” 的时候,优先考虑以下这几个条件:

  • 可交互:userInteractionEnabled == YES
  • 可见:hidden == NOalpha > 0.01
  • 点击在范围内:[self pointInside:point withEvent:event] == YES
  • Z轴最上:// 先检查最上层 subview(数组倒序)
  • 最深处(递归):// 会一直往下递归查找 subview 中最深的匹配者


4. 找到了最适合响应的View后,View会如何处理这个响应事件

在通过hitTest找到了这个View后:

  1. 系统将其赋值给touch.view
  2. 首先判断这个view有没有UIGestureRecognizer
  3. 如果这个View有UIGestureRecognizer,此时有一个关键的配置是 cancelsTouchesInView(默认为YES):以一个UITapGestureRecognizer *tap = *** 为例
  4. 如果tap.cancelsTouchesInView = YES
    1. 用户点击后,先执行 touchesBegan:
    2. GestureRecognizer 识别成功的时候 (GestureRecognizer识别失败则正常的进行touches方法 )
    3. UIKit 会发送 touchesCancelled: 给 View
    4. View就不再接收后续事件了
  5. 如果tap.cancelsTouchesInView = NO
    1. 用户点击后,先执行 touchesBegan:
    2. 即使GestureRecognizer 识别成功,View依旧可以接收完整的touchesBegan、touchesMoved、touchesEnded
    3. 双方可以并行处理

下边我们再回来介绍一下上边提到的UIGestureRecognizer:

首先,我们要先明确这个UIGestureRecognizer 是由用户添加而非系统自动添加的,开发者可以通过手动添加这些机制,更加高级、结构化的处理touch事件,比如:

  • 单击、双击(UITapGestureRecognizer
  • 长按(UILongPressGestureRecognizer
  • 拖动(UIPanGestureRecognizer
  • 缩放(UIPinchGestureRecognizer
  • 旋转(UIRotationGestureRecognizer
  • 轻扫(UISwipeGestureRecognizer

在代码中,我们首先要创建并初始化手势识别器,并将其添加到View上:

1
2
3
4
5
6
7
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
[self.view addGestureRecognizer:tap];

- (void)handleTap:(UITapGestureRecognizer *)gesture {
CGPoint location = [gesture locationInView:self.view];
NSLog(@"点击位置:%@", NSStringFromCGPoint(location));
}

此时,self.view接收到的tap事件就会由这个handleTap来处理了

除了使用UIGestureRecognizer之外,开发者也可以使用重写UIView方法的方式来进行自定义响应逻辑:

1
2
3
4
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
// 增加其他逻辑
}
场景 推荐方法
简单手势(点击、长按、滑动) 使用 UIGestureRecognizer,结构清晰、解耦
更复杂逻辑、手势状态控制、响应区域自定义 使用 touchesBegan: 系列方法手动处理
想做一些自定义响应(如事件穿透、模拟按钮) 重写 hitTest:pointInside: 并配合 touches 系列方法




三、事件处理过程 (技术角度)

那么经过了上边的第二部分,我们就已经梳理好了点击事件被手机响应的整个流程,那么现在我们再从技术视角总结一下这个过程,来形成更好的理解:


UIApplication层

触摸到达UIApplication,封装为UIEvent 与 UITouch:

1
2
3
@interface UIEvent : NSObject
@property(nonatomic, readonly) NSSet<UITouch *> *allTouches;
@end

最终事件通过 [UIApplication sendEvent:] 送入App的主事件循环

1
2
3
4
- (void)sendEvent:(UIEvent *)event {
//交给当前keyWindow
[self.keyWindow sendEvent:event];
}


UIWindow & UIView hitTest层

UIWindow开始调用自己的sendEvent,来分发事件(再次提醒UIWindow是UIView的子类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// UIWindow.m
- (void)sendEvent:(UIEvent *)event {
for (UITouch *touch in [event allTouches]) {
CGPoint location = [touch locationInView:self];

// ✅ 核心:寻找响应者(目标 View)
UIView *targetView = [self hitTest:location withEvent:event];
touch.view = targetView;

// ✅ 首先将事件交给 GestureRecognizer
for (UIGestureRecognizer *gr in targetView.gestureRecognizers) {
[gr _receiveTouch:touch];
}

// ✅ 然后调用 touchesXxx 方法
switch (touch.phase) {
case UITouchPhaseBegan:
[targetView touchesBegan:...];
break;
...
}
}
}

并且逐步递归的进行hitTest:

UIWindow.hitTest → UIView.hitTest(递归)→ 返回最深可响应 View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// UIView.m
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.userInteractionEnabled == NO || self.hidden || self.alpha <= 0.01)
return nil;

if (![self pointInside:point withEvent:event])
return nil;

// 逆序查找子视图(最上层的 subview 最先匹配)
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint subPoint = [subview convertPoint:point fromView:self];
UIView *hitView = [subview hitTest:subPoint withEvent:event];
if (hitView) return hitView;
}

return self;
}



UIView 响应层

找到了targetView后,并行或独立的触发Gesture和Touches方法

(取决于gestureRecognizer.cancelsTouchesInView)


1. GestureRecognizer

1
2
3
for (UIGestureRecognizer *gr in view.gestureRecognizers) {
[gr _receiveTouch:touch];
}
  • 系统调用内部私有方法_receiveTouch: (等价于 touchesBegan: )
  • 每个UIGestureRecognizer都注册自己的状态机(长按、滑动、双击等)
  • 若识别成功,进入 .recognized 状态,并调用这个Recognizer的 target-action
  • 此时,若gestureRecognizer.cancelsTouchesInView = YES,则后续的touchesMovedtouchesEnded会被取消。否则照常执行。

2. View的touch响应

如果这个view没有注册GestureRecognizer,那么在第一步touchesBegan: 后,自然也不会有gesture识别成功,则会继续进入touchesMoved与touchEnded函数:

1
2
3
touch.view → touchesBegan:
touch.view → touchesMoved:
touch.view → touchesEnded:

view可以通过重写这些方法来自定义响应:

1
2
3
4
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
// 增加其他逻辑
}


汇总

步骤 所在类 方法
1 UIApplication sendEvent:
2 UIWindow sendEvent:
3 UIView(递归) hitTest:withEvent:
4 UIView(或其子类) pointInside:withEvent:
5 UIGestureRecognizer _receiveTouch:(监听识别)
6 UIView(或其子类) touchesBegan:




四、结尾

那么到这里,完整的touch链路就被完全梳理完成了。如有错误欢迎勘误~