iOS之事件的传递和响应机制

一、 基础概念:响应者对象 (UIResponder)
学习触摸事件首先要理解 UIResponder。在 iOS 中,不是任何对象都能处理事件,只有继承了 UIResponder 的对象才能接收并处理事件,我们称之为“响应者对象”。
常见子类包括:
-
UIView -
UIViewController -
UIApplication -
UIWindow(继承自 UIView)
二、 事件的产生与底层源头(RunLoop 视角)

这是面试中区分初级和资深开发的一个点:事件是如何从硬件传到 App 的?
-
Source1 (Mach Port): 当用户触摸屏幕,系统(IOKit.framework)将触摸事件封装成
IOHIDEvent对象,通过 IPC(进程间通信)机制发送给 App 进程。App 的 RunLoop 中的Source1回调接收到该消息,并唤醒 RunLoop。 -
Source0: RunLoop 被唤醒后,
Source1会触发Source0回调,将IOHIDEvent转化为UIEvent,并由UIApplication进行分发。 -
事件队列:
UIApplication会管理一个事件队列。- 为什么是队列? 队列遵循 FIFO(先进先出)原则,保证用户的操作按顺序被处理(例如先点确定再点取消,不能反过来)。
三、 事件的传递(Hit-Testing)—— 寻找最合适的 View
这是事件处理的第一步:自上而下(UIApplication -> Window -> View -> Subview)寻找“靶心”。
3.1 传递流程
-
UIApplication从事件队列取出事件,发送给keyWindow。 -
keyWindow判断自己能否响应,并判断点是否在自己身上。 -
倒序遍历子控件(从后往前,即
subviews.lastObject开始),重复上述步骤。- 原因:后添加的 View 在视觉上覆盖在先添加的 View 之上,优先响应最上面的 View 符合视觉逻辑,同时能减少循环次数。
-
一旦找到符合条件的子 View,就将其作为
fitView返回,停止遍历。 -
如果没有符合条件的子控件,但自己满足条件,则自己就是最合适的 View。
3.2 核心方法与源码模拟
寻找过程由两个核心方法实现:
-
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; -
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
hitTest:withEvent: 的伪代码实现:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1. 判断是否允许交互:不允许交互、被隐藏、透明度极低均不能响应
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha < 0.01) {
return nil;
}
// 2. 判断触摸点是否在当前 View 内部
if ([self pointInside:point withEvent:event] == NO) {
return nil;
}
// 3. 从后往前遍历子控件(深度优先,视觉顶层优先)
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
// 关键:坐标系转换,将当前点的坐标转换到子控件的坐标系上
CGPoint childPoint = [self convertPoint:point toView:childView];
// 递归调用子控件的 hitTest
UIView *fitView = [childView hitTest:childPoint withEvent:event];
// 如果子控件找到了最合适的 View,直接返回,不再继续遍历
if (fitView) {
return fitView;
}
}
// 4. 如果子控件都没有返回,且通过了步骤1和2,则自己就是最合适的 View
return self;
}
3.3 拦截与Hack技巧
通过重写 hitTest:withEvent: 可以实现特殊需求:
-
扩大点击区域:重写
pointInside,判断点在 bounds 向外延伸的范围内即返回 YES。 -
穿透点击:让下层的 View 响应事件。在顶层 View 的
hitTest中返回nil,事件就会自动传递给被遮挡的 View(前提是父 View 继续寻找)。 -
子视图超出父视图范围响应:默认情况下,子视图超出父视图部分无效(因为父视图
pointInside返回 NO,根本不会遍历子视图)。解决方案是重写父视图的hitTest或pointInside。
四、 事件的响应(The Responder Chain)
找到最合适的 View(Initial View)后,如果没有手势拦截,系统会调用该 View 的 touches 系列方法。
4.1 四大核心方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
注意
touchesCancelled:不仅仅是电话呼入,最常见的情况是手势识别器(UIGestureRecognizer)识别成功后,会取消触摸事件,导致 View 收到此回调。
4.2 响应者链条传递规则
响应链是自下而上(View -> Superview -> Controller -> Window -> App)传递的。
-
Initial View 处理事件(调用 touches 方法)。
-
如果 View 没有重写 touches 方法,或者在 touches 方法中调用了
[super touchesBegan/Moved/Ended...],事件会传递给nextResponder。 -
Next Responder 查找规则:
-
UIView: 若是 VC 的 Root View,下一个是 VC;否则是
superview。 -
UIViewController: 下一个是其 View 的
superview(通常是 Window 或其他 VC 的 View)。 -
UIWindow: 下一个是
UIApplication。 -
UIApplication: 下一个是
AppDelegate(如果它是 UIResponder)。
-
-
如果传递到最后都没人处理,事件被丢弃。!
五、 高阶难点:事件响应的冲突与特殊处理
这部分内容是资深工程师必须掌握的细节。
5.1 手势识别器 (Gesture Recognizer) vs 触摸事件 (Touches)
这是最容易混淆的地方。UIGestureRecognizer 也是通过 Hit-Testing 绑定到 View 上的,但它的优先级通常高于 View 自身的 touches 方法。
-
默认行为:
-
当触摸发生,系统同时将事件发送给 View (touchesBegan) 和 绑定在 View (及父视图) 上的 GestureRecognizer。
-
如果 GestureRecognizer 识别失败,View 继续接收 touchesEnded。
-
如果 GestureRecognizer 识别成功:
-
它会独占该事件。
-
系统会向 View 发送
touchesCancelled,终止 View 的事件处理。 -
View 不会再收到 touchesEnded。
-
-
-
关键属性:
-
cancelsTouchesInView(默认 YES):识别成功后取消 View 的触摸。设为 NO 则两者共存(View 能收到 touchesEnded)。 -
delaysTouchesBegan(默认 NO):只有手势识别失败后,才把 touchesBegan 发送给 View。用于解决点击态闪烁问题。
-
5.2 UIControl (Target-Action) 的特殊性
UIButton、UISlider 等继承自 UIControl。
-
现象:点击 Button,其父 View 的
touchesBegan不会被触发。 -
原因:
UIControl内部重写了 touches 方法来处理 Target-Action 逻辑。它默认阻断了响应链的向上传递(没有调用 super)。 -
区别:
-
UILabel/UIView:默认不处理,透传给父控件。 -
UIButton:处理并吞掉事件,父控件收不到。
-
5.3 总结:事件处理的优先级排序
在大多数情况下,事件响应的优先级如下:
-
手势识别器 (UIGestureRecognizer):优先级最高,识别成功会 Cancel 其他。
-
UIControl (Target-Action):次之,通常会阻断响应链。
-
UIResponder (Touches):优先级最低,通过响应链传递。
六、 总结
iOS 的事件机制是一个精密设计的流程:
-
硬件层:Source1 -> RunLoop -> Source0 -> UIApplication。
-
寻找层 (Hit-Test):自上而下,倒序遍历,利用
pointInside和坐标转换找到最合适的fitView。 -
响应层 (Responder Chain):自下而上,
nextResponder传递。 -
干扰项:注意手势识别器对标准响应链的“拦截”和“取消”机制。
掌握这套逻辑,不仅能应对面试中的“如何扩大按钮点击范围”、“父视图如何拦截子视图事件”、“手势冲突解决”等问题,也能在实际开发中处理复杂的交互场景。