iOS的事件响应链
好的,iOS 的事件响应链是面试中的高频考点,它考察的是对 UIKit 事件传递机制的理解。下面我将为你详细解析。
---
### 一、什么是事件响应链?
**事件响应链** 是 iOS 中用来寻找和处理**触摸事件、晃动事件、远程控制事件**等的一个机制。它是由一系列**响应者对象**连接起来的链条。
#### 核心概念:响应者对象
- 任何继承自 `UIResponder` 的对象都是响应者
- 常见的响应者:
- `UIApplication`
- `UIWindow`
- `UIViewController`
- `UIView`(及其子类,如 `UILabel`, `UIButton` 等)
---
### 二、响应链的建立:视图层级
响应链是基于应用的视图层级建立的,遵循 **从下到上** 的规则:
```
UIApplication → UIWindow → Root ViewController → 父视图 → ... → 子视图
```
**查找下一个响应者的规则**:
1. **UIView**:
- 如果它有视图控制器,下一个响应者是它的视图控制器
- 否则,下一个响应者是它的父视图
2. **UIViewController**:
- 如果它的视图是 window 的根视图,下一个响应者是 window
- 否则,下一个响应者是它的父视图控制器(如果有)或父视图
3. **UIWindow**:下一个响应者是 `UIApplication`
4. **UIApplication**:下一个响应者是 `AppDelegate`(如果它继承自 `UIResponder`)
---
### 三、事件传递的完整流程
事件处理分为两个主要阶段:
#### 阶段 1:事件传递(Hit-Testing) - 寻找第一响应者
**目标**:找到最合适处理触摸事件的视图(第一响应者)。
**过程**:
1. 系统从 `UIWindow` 开始,调用 `hitTest:withEvent:` 方法
2. `hitTest:withEvent:` 内部会调用 `pointInside:withEvent:` 判断触摸点是否在自己范围内
3. 如果在范围内,它会**从后往前**遍历自己的子视图(后添加的视图在顶层):
- 对每个子视图递归调用 `hitTest:withEvent:`
4. 最终返回最深层的、包含触摸点、且 `userInteractionEnabled = YES`、`alpha > 0.01` 的视图
**示例代码理解**:
```objectivec
// 伪代码逻辑
- (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] == NO) {
return nil;
}
// 3. 从后往前遍历子视图
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [self convertPoint:point toView:subview];
UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
if (hitView) {
return hitView; // 找到了合适的子视图
}
}
// 4. 没有合适的子视图,自己就是第一响应者
return self;
}
```
#### 阶段 2:事件响应 - 沿着响应链传递
**目标**:如果第一响应者不处理事件,就将事件传递给下一个响应者。
**过程**:
1. 事件首先发送给**第一响应者**
2. 如果第一响应者不处理,就传递给它的**下一个响应者**
3. 继续传递,直到有响应者处理事件,或者到达 `UIApplication` 仍不处理,事件被丢弃
**响应者处理方法**:
```objectivec
// 触摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
// 其他事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event; // 晃动
- (void)remoteControlReceivedWithEvent:(UIEvent *)event; // 远程控制
```
---
### 四、实际场景示例
假设有这样的视图层级:
```
UIWindow
└── RootViewController.view (红色)
├── View A (蓝色, 200x200)
│ └── Button B (黄色, 100x100)
└── View C (绿色, 200x200)
```
**场景 1:点击 Button B**
1. **Hit-Testing**:
- `UIWindow` → `RootViewController.view` → `View A` → `Button B`
- 第一响应者:`Button B`
2. **事件响应**:
- 如果 `Button B` 处理了点击(有 target-action),事件处理结束
- 如果 `Button B` 不处理,传递给 `View A` → `RootViewController` → `UIWindow` → `UIApplication`
**场景 2:点击 View C 的空白区域**
1. **Hit-Testing**:
- `UIWindow` → `RootViewController.view` → `View C`
- 第一响应者:`View C`
2. **事件响应**:
- `View C` 的 `touchesBegan:` 被调用
---
### 五、常见面试问题与答案
#### Q1:如果子视图超出了父视图的 bounds,还能接收到事件吗?
**A**:默认情况下**不能**。因为 `pointInside:withEvent:` 在判断时,如果触摸点在父视图的 bounds 之外,会直接返回 NO,就不会继续向子视图进行 Hit-Testing。
**解决方案**:重写父视图的 `pointInside:withEvent:` 方法
```objectivec
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// 扩大点击区域,包含所有子视图
CGRect expandedBounds = CGRectInset(self.bounds, -50, -50);
return CGRectContainsPoint(expandedBounds, point);
}
```
#### Q2:如何阻止某个视图接收触摸事件?
**A**:有几种方式:
- 设置 `userInteractionEnabled = NO`
- 设置 `hidden = YES`
- 设置 `alpha <= 0.01`
- 重写 `hitTest:withEvent:` 返回 nil
#### Q3:`touchesBegan` 和 `UIControl` 的 target-action 哪个先执行?
**A**:`UIControl` 的 target-action 机制是基于响应链的更高级封装。当点击 `UIButton` 时:
1. 先调用 `touchesBegan` 等方法
2. 然后 `UIButton` 内部处理会触发 target-action
3. 如果重写了 `touchesBegan` 但不调用 `super`,可能会阻止 target-action 的执行
---
### 六、响应链的实际应用
1. **自定义事件传递**:重写 `hitTest:withEvent:` 可以实现不规则形状点击
2. **事件拦截**:在父视图层拦截所有子视图的事件
3. **全局手势处理**:在 `UIWindow` 或根视图控制器处理特定事件
4. **调试事件问题**:理解响应链可以帮助快速定位事件不响应的问题
掌握事件响应链机制,对于处理复杂的 UI 交互、自定义控件和调试触摸相关问题都非常有帮助。
