Chapter 173: Apple Deep Path Touch Event
一次手指点击按钮的路径,表面上只触发一个 IBAction 或一个 UIGestureRecognizer 回调;系统路径却跨过触摸控制器、驱动边界、系统事件服务、主线程 run loop、UIKit 命中测试、响应链、手势状态机和视图层更新。读者读完本章后,应能把“按钮点击慢”“滑动卡顿”“手势抢占”“touchesCancelled 出现”这类现象定位到输入采样、事件分发、主线程处理、手势识别或显示反馈中的一个责任边界。
本章的贯穿材料是一条用户可见行为:用户在 iPhone 屏幕上按下一个自定义按钮,按钮同时挂了一个 UITapGestureRecognizer,视图控制器在回调里更新界面。这个行为从 App 角度只有三类公开入口:UIView 的 touch 方法、UIControl 的 target-action、UIGestureRecognizer 的 action。Apple 公开文档把 App 可见触摸处理收束在 Handling touches in your view、UIEvent、UITouch、UIGestureRecognizer 和 Using responders and the responder chain 这些 API 语义内。驱动、系统事件服务和硬件协同属于 Apple 平台的封闭实现边界,正文会用公开行为和通用系统结构推断责任链,同时标明能从 App 层确认的证据。
触摸事件的核心判断顺序是:先看触摸是否产生了稳定的 UITouch sequence,再看它被哪个 view 命中,再看手势识别器是否接管或取消了 view touch,再看回调是否阻塞主线程,最后看 UI 状态更新是否赶上下一次显示提交。沿着这条顺序排查,问题会落到明确责任主体:硬件采样提供时间戳,系统输入路径把采样标准化,UIKit 按视图层级投递事件,手势层处理竞争关系,App 代码承担回调耗时,图形管线承担反馈呈现。
173.1 Touch Event 的 Apple 跨层路径总览
Touch Event 在 Apple 平台上可以理解为一条从物理接触到 UI 回调的分层路径。手指接触屏幕时,触摸控制器采样电容变化并产生带时间信息的输入数据;驱动和系统输入路径把硬件数据转成系统可处理的事件;UIKit 在 App 主线程附近把事件封装为 UIEvent 和 UITouch;窗口执行 hit-testing 找到目标 view;gesture recognizer 和 responder chain 决定最终回调;App 回调修改状态后,后续章节中的 frame rendering 路径把反馈显示出来。
这张图用于限定本章讨论范围:它描述 App 可见触摸路径和可推断系统边界,图中的 driver 与 daemon 名称不展开到 Apple 私有实现细节。
图中的关键边界有三处。第一处是硬件到驱动边界,App 只能看到坐标、phase、timestamp、tap count 等已经抽象后的触摸对象,看不到触摸矩阵、固件滤波或驱动队列。第二处是系统事件到 UIKit 边界,App 处理的是 UIEvent 与 UITouch,通常通过 view、control、gesture 入口接收。第三处是回调到显示反馈边界,触摸回调本身只改变 App 状态;用户看到的按钮高亮、列表滚动和动画反馈,还需要主线程提交布局和 Core Animation 事务,并通过显示系统完成呈现。
贯穿例子可以写成一个最小视图:按钮接收 control event,按钮所在容器接收 tap recognizer,视图本身还可重写 touch 方法。这个例子证明同一次物理接触会进入同一条系统事件路径,但 App 层可能有多个公开处理入口。
final class TouchDemoViewController: UIViewController {
private let button = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
button.setTitle("Tap", for: .normal)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
view.addSubview(button)
let tap = UITapGestureRecognizer(target: self, action: #selector(containerTapped(_:)))
view.addGestureRecognizer(tap)
}
@objc private func buttonTapped() {
button.isSelected.toggle()
}
@objc private func containerTapped(_ recognizer: UITapGestureRecognizer) {
guard recognizer.state == .ended else { return }
view.backgroundColor = .systemGray6
}
}
这段代码中的 touchUpInside 回调、UITapGestureRecognizer 回调和按钮高亮属于不同层面的结果。touchUpInside 是 UIControl 对 touch sequence 的控件语义封装;tap recognizer 是手势状态机对 touch sequence 的解释;背景色变化只有在主线程回到布局和显示提交路径后才成为用户可见结果。排查触摸问题时,把这三层混成“点击事件失效”会丢失责任边界。
173.2 Hardware Layer:Touch Controller、Display Hardware、Input Sampling
触摸硬件层的任务是把连续物理接触变成带时间和位置的采样数据。电容触摸屏通常由触摸控制器扫描传感矩阵,固件对噪声、误触、边缘接触和多点接触做初步处理,再把结果交给主 SoC 的输入路径。Apple 公开开发文档不暴露具体触摸控制器型号、扫描固件和驱动协议;App 可依赖的稳定事实是 UIKit 给出的 UITouch 坐标、phase、timestamp、view/window 归属和可选的 coalesced/predicted touch API。
输入采样和显示刷新是两条协同路径。触摸控制器按自己的采样节奏收集手指位置,显示硬件按面板刷新节奏扫描输出图像。系统为了降低手指移动到画面响应之间的时间差,会在输入路径保留 timestamp,并在 UIKit 层提供合并触摸和预测触摸能力。UIEvent 的 coalesced touches 让绘图类 App 读取一次回调之间的多个历史点,predicted touches 让 App 估计短时间后的轨迹;这两类数据支持低延迟绘制,但它们也要求 App 在下一帧到来前完成状态更新。
触摸 timestamp 是排查延迟的第一类证据。UITouch.timestamp 表示触摸事件在系统时间基准上的发生时刻,回调中读取当前时间与 timestamp 的差值,可以粗略估计事件进入 App 前后的延迟。这个差值无法拆出驱动、系统队列和 run loop 各自耗时,但它能判断问题是否已经在 App 回调之前积累。
final class LatencyProbeView: UIView {
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let inputTimestamp = touch.timestamp
let callbackTimestamp = ProcessInfo.processInfo.systemUptime
let appVisibleDelay = callbackTimestamp - inputTimestamp
print("app visible touch delay:", appVisibleDelay)
}
}
这个探针只说明 App 收到触摸时,触摸 timestamp 与回调时刻之间的间隔。它无法证明硬件采样慢,也无法证明系统事件服务拥塞;它能帮助读者先把问题分成两类:事件到达 App 前已经有明显延迟,或者事件到达后由 App 主线程、手势处理、布局和渲染继续放大延迟。
硬件层还决定触摸与显示的协同上限。高刷新率设备可以缩短显示反馈间隔,但输入链路仍要经过采样、事件投递、App 回调、布局提交和显示输出。Apple 对 ProMotion 和可变刷新率的公开说明集中在显示提交与刷新策略,例如 CADisplayLink 和 Optimizing ProMotion refresh rates 相关文档。对触摸手感来说,高刷新率提供更密集的反馈窗口;主线程阻塞或手势识别延迟仍会消耗这些窗口。
173.3 Kernel / Driver Layer:XNU、IOKit / Driver Boundary
Kernel / Driver Layer 的职责是把硬件输入纳入系统统一资源管理。公开的 Darwin / XNU 材料能说明 Apple 系统底层有 Mach、BSD 和 IOKit 这些基础结构;iOS 具体触摸驱动、输入 daemon 和硬件协议属于平台私有实现。写移动 OS 架构时,合理边界是把这一层描述为“驱动上报与系统输入标准化边界”,用它解释 App 无法直接控制触摸硬件,也无法绕过 UIKit 去读取原始触摸矩阵。
触摸驱动上报至少要解决三个问题。第一,硬件采样要转成系统可识别的输入事件,包含位置、阶段、时间戳和多点身份。第二,系统要把输入与当前前台会话、窗口层级、屏幕状态和安全策略关联起来。第三,事件要被安全地送到目标 App,后台 App、无前台窗口的进程和越权读取者不应获得任意触摸流。App 开发者看到的结果是触摸只投递到当前可交互 UI,且投递形式已经被 UIKit 封装。
这层的故障通常表现为系统级输入异常,影响范围会超过单个 view 回调。比如全系统触摸无响应、屏幕边缘大面积失灵、多指接触识别错误、锁屏和主屏也受影响,这类现象更接近硬件、固件、驱动或系统输入服务边界。相反,某个按钮没有响应、某个页面滑动卡顿、某个手势抢占另一个手势,通常应先回到 UIKit hit-testing、gesture recognizer 和 App 主线程检查。
Apple 平台的公开 API 还形成了一个重要工程约束:触摸能力按 UI 语义开放给 App,设备节点由系统侧控制。App 拿到的是 UITouch 对象,硬件控制入口不会暴露给普通 App;App 通过 UIView、UIControl、UIGestureRecognizer 或 SwiftUI 事件模型表达意图,不直接管理中断、DMA、驱动队列或固件参数。这种封装把硬件差异、隐私边界和输入一致性留在系统侧,也让 App 代码在不同设备和刷新率上维持稳定语义。
在贯穿例子里,驱动层对 App 的影响体现在 timestamp、坐标稳定性和 touch phase 上。按钮回调没有触发时,优先看 App 层是否有可交互 view、gesture 是否取消了 touches、run loop 是否被阻塞;只有当系统其它界面也出现相同输入异常时,才把判断上移到硬件或系统输入边界。
173.4 Event Delivery Layer:System Event Queue 与 RunLoop
Event Delivery Layer 把系统输入事件送入 App 的主线程事件循环。UIKit App 的 UI 事件处理依赖主线程,主线程运行 run loop,run loop 从输入源取事件并调用 UIKit 分发逻辑。Foundation 的 RunLoop 文档和 UIKit 行为共同说明了一个工程事实:触摸处理、定时器、source、layout 和显示提交都依赖主线程调度;主线程长时间执行同步任务会推迟事件处理和画面反馈。
RunLoop mode 是理解滚动和触摸跟踪的关键概念。Run loop mode 是一组 input source、timer 和 observer 的运行集合,UIKit 在触摸跟踪、滚动等交互期间会使用 tracking mode。开发者如果把定时器只挂在 default mode,它在滚动跟踪期间可能不触发;如果把耗时任务放在主线程,它会阻塞 touches、gesture 回调和下一帧提交。Apple 公开的 RunLoop.Mode.tracking 表达的是这种交互跟踪模式边界。
系统事件队列和 App 主线程之间的关系可以用四步判断。先看事件是否进入 App:touchesBegan 或 recognizer callback 是否出现。再看出现时机:timestamp 与回调时刻差值是否异常。接着看主线程是否正在执行同步 I/O、JSON 解析、大量 layout 或锁等待。最后看回调后是否马上触发 UI 状态变化。这个顺序能把“系统没有送事件”和“App 收到事件后处理慢”分开。
下面的例子说明主线程任务如何放大触摸延迟。示例用于构造一个可观察的阻塞点,帮助读者把延迟归因到 App 回调。
final class BlockingTapViewController: UIViewController {
@objc private func buttonTapped() {
let end = Date().addingTimeInterval(0.15)
while Date() < end {
_ = UUID().uuidString
}
view.backgroundColor = .systemBlue
}
}
这段回调运行期间,主线程无法继续处理后续触摸,也无法及时完成下一次布局和显示提交。用户感受到的是“点了以后反应慢”,系统路径上的真实责任点是 App 回调占用主线程。排查这类问题时,timestamp 可以说明事件已经到达,Time Profiler 或 Main Thread Checker 可以定位同步工作,Core Animation 帧时间可以说明画面反馈是否错过了刷新窗口。
Event Delivery Layer 还解释了 touchesCancelled 的来源。取消并不总是错误,它表示当前 touch sequence 被系统或上层识别策略终止,例如来电、系统手势、控件状态变化、手势识别器接管、窗口失去可交互状态等。App 收到取消后应把临时高亮、拖拽状态和未提交交互回滚到稳定状态;如果仍然把取消当成成功点击,界面状态会和真实手势结果分离。
173.5 Framework Layer:UIKit Event、Hit-Testing、Responder Chain
Framework Layer 把系统事件变成 UIKit 的对象模型。UIEvent 表示一次事件集合,UITouch 表示一根手指在一段 touch sequence 中的状态。UITouch.phase 从 began、moved、stationary、ended 到 cancelled 描述状态变化;view 和 window 记录 UIKit 关联的目标。App 一般无需自己创建这些对象,只需要在 UIKit 提供的 touch、control 或 gesture 入口中读取它们。
Hit-testing 决定一次触摸最初归属哪个 view。UIKit 从窗口开始,根据视图层级、可见性、交互开关、alpha、point(inside:with:) 和 hitTest(_:with:) 递归寻找最深的合格目标。目标 view 一旦在 began 阶段确定,后续 moved 和 ended 通常继续关联这个 touch sequence,即使手指移动到目标 view 外部,App 也应按同一序列完成状态处理。
自定义 hit-testing 解决的是“哪个 view 接收触摸”问题。下面的例子把按钮的可点击区域扩展到视觉边界外 12 点,用来说明 hit-test 修改的是命中范围,不改变驱动、系统事件队列或手势状态机。
final class ExpandedHitButton: UIButton {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let expandedBounds = bounds.insetBy(dx: -12, dy: -12)
return expandedBounds.contains(point)
}
}
这个例子对应的判断是:用户说“按钮边缘点不到”时,先看 view frame、父视图裁剪、isUserInteractionEnabled、alpha 和 hit-test 逻辑。只有目标 view 从未被命中,才谈不到 control event 或 gesture 回调。把 hit-test 问题误判成手势冲突,会把排查方向带到后续层级。
Responder Chain 决定事件在命中 view 无法处理时如何向上传递。UIResponder 是 UIKit 中处理事件的基础类,UIView、UIViewController、UIWindow 和 UIApplication 都在响应链上。view 可以重写 touchesBegan(_:with:) 等方法;没有处理的事件会沿 next responder 向上走。视图控制器常作为协调者参与响应链,但它通常不应承担所有低层触摸细节,否则 view 的局部交互、control 语义和控制器状态会耦合。
Framework Layer 的边界还体现在 UIControl。按钮、开关、滑块等控件把一段 touch sequence 翻译成更高层的 control event,例如 .touchDown、.touchDragInside、.touchUpInside、.valueChanged。开发者订阅的是控件语义,不需要自己判断手指是否在按钮内抬起。贯穿例子中的按钮点击优先属于 UIControl 语义;容器 tap recognizer 是同一触摸序列上的另一层解释。
173.6 Gesture Layer:UIGestureRecognizer 与 Touch Sequence
Gesture Layer 把低层 touch sequence 翻译成应用语义。UIGestureRecognizer 接收命中到关联 view 及其层级中的 touches,内部维护状态机,并在识别成功、变化、结束、失败或取消时调用 action。离散手势如 tap 只在识别完成后触发;连续手势如 pan、pinch、rotation 会经历 began、changed、ended 或 cancelled。这个状态机让 App 用“点击、拖拽、缩放、旋转”表达交互,而无需手写每一个 move 点的判断。
Recognizer state 是手势排查的中心证据。一个 recognizer 从 possible 开始,离散手势成功时进入 recognized,连续手势通常进入 began 后多次 changed,最后 ended。条件不满足会 failed;系统打断或竞争策略终止会 cancelled。App 代码应按 state 做分支,尤其是拖拽类交互要在 began 建立临时状态,在 changed 更新状态,在 ended 提交,在 cancelled 回滚。
final class DragHandler: NSObject {
@objc func handlePan(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
beginDragging()
case .changed:
updateDragging(with: recognizer.translation(in: recognizer.view))
case .ended:
commitDragging()
case .cancelled, .failed:
rollbackDragging()
default:
break
}
}
private func beginDragging() {}
private func updateDragging(with translation: CGPoint) {}
private func commitDragging() {}
private func rollbackDragging() {}
}
这段代码说明 gesture callback 是状态迁移入口。若只在 action 中处理“最终点击”,连续手势的中间反馈会丢失;若在 cancelled 分支不回滚,界面会留下半拖拽状态。手势层的稳定写法是把状态机和 UI 临时状态一一对应。
手势竞争由 recognizer、delegate 和 view 层级共同决定。常见竞争包括:父视图 tap recognizer 与子控件点击竞争,列表 pan recognizer 与单元格自定义拖拽竞争,系统边缘手势与 App 自定义手势竞争,双击与单击之间需要 failure dependency。require(toFail:) 表示一个 recognizer 等待另一个 recognizer 失败后再成功;delegate 可以允许 simultaneous recognition,也可以按条件阻止某个 recognizer 接收 touch。Apple 的 UIGestureRecognizerDelegate 文档给出了这些公开控制点。
cancelsTouchesInView、delaysTouchesBegan 和 delaysTouchesEnded 是手势层影响 view/control 回调的关键属性。默认情况下,一个手势识别成功后可能取消底层 view touch,使控件或自定义 touch handler 收到 touchesCancelled。延迟属性会推迟 view 接收 began 或 ended,让 recognizer 有时间判断序列是否属于某个手势。用户看到“按钮按下高亮慢”时,问题可能来自 recognizer 延迟策略,也可能来自主线程阻塞;判断时要看 timestamp、recognizer state 和 control event 的出现顺序。
贯穿例子中的容器 tap recognizer 会和按钮的 .touchUpInside 发生关系。若容器 recognizer 接收了同一次 touch,并在识别后取消 view touches,按钮的 control event 可能被打断;若 delegate 明确允许或过滤,按钮点击可以保留。正确排查顺序是先确认按钮是否被 hit-test 命中,再看 recognizer 是否接收了同一 touch,接着看 recognizer state 和 cancel 属性,最后看 control event 是否触发。
173.7 App Layer:UIView、UIControl、UIViewController
App Layer 面对同一条 touch path 有三种稳定入口。UIView 适合处理自定义绘图、拖拽、画布、游戏内对象等低层交互;UIControl 适合按钮、滑块、开关、文本输入这类控件语义;UIViewController 适合协调页面状态、导航、数据提交和跨 view 交互。把这三种入口分清,能减少“所有点击都写进控制器”的耦合,也能让触摸问题落在正确层级。
UIView touch handling 的优势是能直接接收 touch sequence。自定义画板需要读取每个 moved 点,结合 coalesced touches 绘制连续曲线;拖拽视图需要在 began 建立起点,在 moved 更新位置,在 ended 或 cancelled 收束状态。它的边界是语义较低,开发者要自己处理多指、取消、越界和状态回滚。
UIControl 的优势是把 touch sequence 翻译成控件语义。按钮的 .touchUpInside 表示“按下后在控件内抬起”,滑块的 .valueChanged 表示值变化,开关的 value change 表示状态确认。控件语义让业务代码不需要重复写命中、拖出、拖回和取消判断。它的边界是交互形态受控件模型约束,复杂组合手势通常应交给 gesture recognizer 或自定义 view。
UIViewController 的职责是协调,低层触摸细节应优先留给 view、control 或 gesture 对象。控制器可以响应按钮 action、gesture action 和 view delegate,更新模型、触发导航、提交请求或启动动画。控制器直接重写低层 touch 方法通常会让页面交互和 view 结构紧耦合;更稳定的做法是让 view 或 control 先把局部交互解释成明确事件,再让控制器处理页面级后果。
下面的例子把三种入口的责任分开。按钮处理确认动作,手势处理背景点击,控制器只接收已经命名的交互结果。
final class ClearTouchViewController: UIViewController {
private let cardView = UIView()
private let confirmButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
confirmButton.addTarget(self, action: #selector(confirmSelection), for: .touchUpInside)
let backgroundTap = UITapGestureRecognizer(target: self, action: #selector(closeKeyboard))
backgroundTap.cancelsTouchesInView = false
view.addGestureRecognizer(backgroundTap)
}
@objc private func confirmSelection() {
submitCurrentSelection()
}
@objc private func closeKeyboard() {
view.endEditing(true)
}
private func submitCurrentSelection() {}
}
这个示例中,cancelsTouchesInView = false 表达一个明确策略:背景 tap 用于收起键盘,同时保留子控件自己的 touch 处理机会。它不保证所有手势都可同时成功,因为 recognizer delegate、view 层级和控件状态仍会参与判断;它能把“背景点击”和“按钮点击”从默认取消关系中拆开,降低误伤控件回调的概率。
App 层还要承担可恢复状态。凡是 touch began 后改变了高亮、拖拽位置、临时选择、hover-like 状态或动画进度,都应在 ended 和 cancelled 中都有收束路径。移动系统会因系统手势、来电、应用切后台、多任务切换或窗口状态变化取消触摸序列;App 层只处理成功结束,会在真实设备上留下状态残留。
173.8 Touch Latency、Main Thread、Frame Pipeline 的协作关系
Touch latency 是一组分段指标,由输入延迟、事件分发延迟、App 处理延迟和显示反馈延迟共同组成。输入延迟来自硬件采样与系统输入路径,事件分发延迟来自 system event queue 与 main run loop,App 处理延迟来自回调、状态更新、布局和同步任务,显示反馈延迟来自 Core Animation transaction、GPU work、合成和面板刷新。用户说“触摸不跟手”,工程上要把这四段分开定位。
最小排查顺序可以固定为五步。第一,记录 UITouch.timestamp 与回调时刻,判断事件到 App 前是否已经有明显间隔。第二,观察回调是否马上执行,确认主线程是否被同步任务阻塞。第三,记录 recognizer state 和 control event 顺序,确认是否存在手势延迟、失败依赖或 cancel。第四,检查回调后是否触发布局、约束计算、图片解码、同步 I/O 或大量对象创建。第五,用帧率、卡顿 trace 或 CADisplayLink 观察显示反馈是否错过刷新窗口。
这条顺序可以应用到贯穿例子。按钮点击慢时,若 timestamp 到回调的差值很小,事件分发没有明显问题;若回调里执行网络同步等待或大量布局,责任点在 App 主线程;若容器 tap recognizer 先进入 recognized 并取消按钮 touches,责任点在 gesture 层;若按钮 action 很快完成但高亮或背景色下一帧才显示,责任点在布局和 frame pipeline。
触摸与帧管线之间的协作可以看成一个 deadline 问题。一次 touch 回调修改 UI 状态后,UIKit 和 Core Animation 需要在当前或下一次 run loop 周期内提交 layer transaction,GPU 和显示系统再完成合成与扫描输出。高刷新率缩短单帧预算,主线程上的 12 ms 同步工作在 60 Hz 下可能还能勉强赶上,在 120 Hz 下更容易错过一帧。这里的结论是:触摸手感不仅取决于输入硬件,还取决于 App 是否把回调、布局和渲染成本控制在帧预算内。
App 层可观测证据应覆盖事件、手势和帧。事件证据包括 UITouch.phase、timestamp、coalesced touches 数量和 callback 时间;手势证据包括 recognizer state、failure dependency、cancel 属性和 delegate 决策;帧证据包括 main thread 时间、layout 时间、Core Animation FPS、GPU work 和 displayed frame time。证据分层后,问题可以被定位成“事件到达慢”“手势等待慢”“回调执行慢”或“显示反馈慢”。
本章最终建立的理解是:Apple touch path 是一条受封装的跨层责任链。App 不需要也无法直接控制触摸硬件;App 能做的是正确选择 UIKit 入口,理解 hit-testing、responder chain 和 gesture state,保持主线程及时返回,并用 timestamp 与帧证据把触摸手感问题放回系统路径。
最小自检任务
用户反馈:一个 iOS 页面中,点击卡片内的按钮有时没有触发 .touchUpInside,同时页面背景挂了一个 UITapGestureRecognizer 用来收起键盘。滑动页面时偶尔感觉按钮高亮延迟。请按照本章路径写出排查顺序,说明每一步要确认的责任主体、证据和可能的用户可见结果。不得要求读取 Apple 私有实现或抓取系统驱动日志。
答案要点
先确认按钮是否被 hit-test 命中,检查按钮 frame、父视图裁剪、isUserInteractionEnabled、alpha、覆盖 view 和自定义 point(inside:with:)。如果按钮没有成为目标 view,.touchUpInside 自然不会出现,责任主体在 UIKit hit-testing 和视图层级。
再确认背景 UITapGestureRecognizer 是否接收同一 touch sequence。记录 recognizer state,检查 cancelsTouchesInView、failure dependency、delegate 的 simultaneous recognition 决策和是否过滤子控件区域。如果 recognizer 识别后取消了按钮 touches,按钮可能收到 cancel,.touchUpInside 不出现,责任主体在 Gesture Layer。
接着记录 UITouch.timestamp 与回调时刻,判断事件到达 App 前是否已经有明显延迟。若 timestamp 到回调差值小,主要瓶颈更可能在 App 处理或显示反馈;若差值大,同时其它页面也有触摸迟滞,可以把问题上移到系统事件队列或设备状态边界,结论仍应停留在公开证据支持的范围内。
随后检查主线程。用 Time Profiler、主线程日志或简化埋点确认按钮回调、手势回调、键盘收起、布局更新和同步任务是否占用主线程。如果主线程在触摸期间执行耗时任务,用户会看到高亮延迟、点击反馈慢或滚动掉帧,责任主体在 App 回调和布局成本。
最后把触摸反馈接到 frame pipeline。按钮 action 快速完成但高亮下一帧才出现时,要检查 layout、Core Animation transaction、图片解码、GPU work 和刷新窗口。结论应区分四类问题:按钮未命中、手势取消、主线程阻塞、显示反馈错过帧期限。
本章知识点总结
- 触摸路径:一次 Apple 触摸事件从硬件采样进入驱动边界,再经系统事件、UIKit 分发、命中测试、手势识别和 App 回调形成用户可见交互。
- 公开边界:App 可依赖
UIEvent、UITouch、UIView、UIControl和UIGestureRecognizer的公开语义,具体触摸驱动和系统输入 daemon 属于私有实现边界。 - 采样时间:
UITouch.timestamp是 App 可见的输入时间证据,可用于粗略区分事件到达前延迟和 App 处理后延迟。 - 显示协同:高刷新率缩短反馈窗口,但主线程、布局、Core Animation 和 GPU 成本仍决定触摸反馈能否按时显示。
- 驱动边界:Kernel / Driver Layer 负责硬件上报和输入标准化,单个按钮失效通常应先检查 UIKit 和 App 层证据。
- RunLoop 模式:触摸跟踪依赖主线程 run loop 和 tracking mode,主线程同步任务会推迟事件处理、手势回调和显示提交。
- 取消语义:
touchesCancelled表示 touch sequence 被系统、手势或窗口状态终止,App 应回滚临时交互状态。 - 命中测试:Hit-testing 从窗口沿视图层级寻找目标 view,按钮点击失效首先要确认 frame、交互开关、alpha、遮挡和自定义命中范围。
- 响应链:Responder Chain 让未处理事件沿 view、controller、window 和 application 传递,适合解释低层 touch 方法的归属。
- 控件语义:
UIControl把 touch sequence 翻译成.touchUpInside、.valueChanged等控件事件,业务代码应优先使用这些稳定语义。 - 手势状态:
UIGestureRecognizer用 possible、began、changed、ended、failed、cancelled 等状态解释 touch sequence,连续交互要覆盖提交和回滚。 - 手势竞争:Failure dependency、simultaneous recognition、cancel 属性和 delegate 决策共同决定多个 recognizer 与控件回调的关系。
- App 分层:
UIView处理局部低层交互,UIControl处理控件语义,UIViewController负责页面协调和业务后果。 - 延迟定位:触摸手感问题应按事件到达、手势识别、主线程处理和显示反馈四段收集证据。
- 最终判断:Apple touch path 的工程能力在于沿公开 API 追踪责任边界,用可观测证据定位输入、UIKit、App 或帧管线问题。