Lazy loaded image
☝️iOS中的Touch响应全链路
Words 3646Read Time 10 min
2025-4-30
2025-4-30
type
status
date
slug
summary
tags
category
icon
password
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都实现的一个方法,它的完整声明是:
在UIApplication中,这个方法负责将接收到的事件分发给合适的UIWindow。而在UIWindow中,这个方法则负责将事件分发给具体的视图。
事件传递的基本流程是:
  • 1. UIApplication接收到系统传来的事件后,调用sendEvent:将事件传递给合适的UIWindow
  • 2. UIWindow通过自己的sendEvent:方法,结合hitTest:withEvent:的结果,将事件分发给具体的视图
  • 3. 目标视图接收到事件后,开始在响应链中进行处理
值得注意的是,这个方法通常不需要我们自己去调用或重写,它是UIKit框架内部用于事件分发的重要机制。
 
 

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

在上一部分,UIApplication通过sendEvent,把事件分发给了当前活跃中的UIWindow:
 
从上边UIWindow的介绍,我们得知了 UIWindow 中也有 sendEvent: 方法,那么这里的sendEvent是在进行什么操作呢,首先我们先从一段伪代码引入这个问题
这里我们可以看到,首先会从event中取出所有的touch,在 UITouchPhaseBegan 的时候,会先触发一次 hitTest:withEvent: ,找出哪个view最适合处理本次触摸,找到后将其设置为 touch.view ,并且接下来的Moved/Ended/Canceled都发送给这个View。
 
在这里,首先我们回来介绍一下 hitTest:withEvent:
这个方法是定义在UIView中的,并且所有的UIView子类(UIWindow也是UIView的子类!) 都会继承这个方法。每一个View在被调用这个方法的时候,都会执行以下几个步骤:
可以看出,整个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
  1. 首先判断这个view有没有UIGestureRecognizer
  1. 如果这个View有UIGestureRecognizer,此时有一个关键的配置是 cancelsTouchesInView(默认为YES):以一个UITapGestureRecognizer *tap = *** 为例
  1. 如果tap.cancelsTouchesInView = YES
    1. 用户点击后,先执行 touchesBegan:
    2. GestureRecognizer 识别成功的时候 (GestureRecognizer识别失败则正常的进行touches方法 )
    3. UIKit 会发送 touchesCancelled: 给 View
    4. View就不再接收后续事件了
  1. 如果tap.cancelsTouchesInView = NO
    1. 用户点击后,先执行 touchesBegan:
    2. 即使GestureRecognizer 识别成功,View依旧可以接收完整的touchesBegan、touchesMoved、touchesEnded
    3. 双方可以并行处理
 
下边我们再回来介绍一下上边提到的UIGestureRecognizer:
首先,我们要先明确这个UIGestureRecognizer 是由用户添加而非系统自动添加的,开发者可以通过手动添加这些机制,更加高级、结构化的处理touch事件,比如:
  • 单击、双击(UITapGestureRecognizer
  • 长按(UILongPressGestureRecognizer
  • 拖动(UIPanGestureRecognizer
  • 缩放(UIPinchGestureRecognizer
  • 旋转(UIRotationGestureRecognizer
  • 轻扫(UISwipeGestureRecognizer
 
在代码中,我们首先要创建并初始化手势识别器,并将其添加到View上:
此时,self.view接收到的tap事件就会由这个handleTap来处理了
 
除了使用UIGestureRecognizer之外,开发者也可以使用重写UIView方法的方式来进行自定义响应逻辑:
场景
推荐方法
简单手势(点击、长按、滑动)
使用 UIGestureRecognizer,结构清晰、解耦
更复杂逻辑、手势状态控制、响应区域自定义
使用 touchesBegan: 系列方法手动处理
想做一些自定义响应(如事件穿透、模拟按钮)
重写 hitTest:pointInside: 并配合 touches 系列方法
 
 

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

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

UIApplication层

触摸到达UIApplication,封装为UIEvent 与 UITouch:
最终事件通过 [UIApplication sendEvent:] 送入App的主事件循环
 

UIWindow & UIView hitTest层

UIWindow开始调用自己的sendEvent,来分发事件(再次提醒UIWindow是UIView的子类)
 
并且逐步递归的进行hitTest:
UIWindow.hitTest → UIView.hitTest(递归)→ 返回最深可响应 View
 

UIView 响应层

找到了targetView后,并行或独立的触发Gesture和Touches方法
(取决于gestureRecognizer.cancelsTouchesInView)

1. GestureRecognizer

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

2. View的touch响应

如果这个view没有注册GestureRecognizer,那么在第一步touchesBegan: 后,自然也不会有gesture识别成功,则会继续进入touchesMoved与touchEnded函数:
view可以通过重写这些方法来自定义响应:
 

汇总

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

四、结尾

那么到这里,完整的touch链路就被完全梳理完成了。如有错误欢迎勘误~
 
上一篇
Android火焰图里常见的Slice,他们都是什么?
下一篇
Protocol 与 Delegate