1.IOHIDEvent事件的传递
1当发生触摸屏幕等硬件事件的时候,会通过 IOKit.framework 产生一个 IOHIDEvent 对象
aIOKit.framework 是一个系统框架的集合,用来驱动一些系统事件。IOHIDEvent 中的 HID 代表 Human Interface Device,即人机交互驱动。
1然后系统通过 mach port(IPC 进程间通信) 将 IOHIDEvent 对象转发给 SpringBoard.app。
2SpringBoard.app 是 iOS 系统桌面 App,它只接收按键、触摸、加速、接近传感器等几种 Event。SpringBoard.app 会找到可以响应这个事件的 App,并通过 mach port(IPC 进程间通信) 将 IOHIDEvent 对象转发给这个 App。
3前台 App 主线程 Runloop 接收到 SpringBoard.app 转发过来的消息之后,触发对应的 mach port 的 Source1 回调 __IOHIDEventSystemClientQueueCallback()。
4Source1 回调内部将 IOHIDEvent 对象转化为 UIEvent, 触发了 Source0 回调__UIApplicationHandleEventQueue()。
5Soucre0 回调内部调用 UIApplication 的 +[sendEvent:] 方法,将 UIEvent 传给UIWindow。
传递流程图如下:
传递堆栈信息如下:
找到UIWindow后,接下来需要做的就是传递UIEvent事件。
2.UIEvent事件传递
UIEvent事件传递大致可以分为三个阶段:
1Hit-Testing(寻找合适的 view)
2Recognize Gesture(响应手势)
3Response Chain(touch 事件传递, 响应事件)
2.1 Hit-Testing 碰撞检测(父视图到子视图)
1是否可响应事件
a是否开启用户交互
b是否 hidden
c是否 alph < 0.01
2手势发生的点是否在当前 view 内
3如果均为是,则倒着遍历子视图,递归判断子视图是否满足1、2两点。如果子视图均不满足,则表示自己则为最合适的响应者。
a倒着遍历子视图数组,是因为,最后添加的视图在最上方,最有可能是响应者,这样可以减少遍历次数
b如果最佳响应者是自己,但是自己不想处理,可以返回nil, 表示未找到最佳处理者,交给父视图处理。
c如果想不管查找结果如何,就是指定最佳响应者为某个视图,那么就直接返回那个视图。
d如果想增加视图的响应区域,可以获取当前视图的size,宽高增加后,再通过CGRectContainsPoint(touchRect, point)判断点是否在当前视图上。
碰撞检测的代码逻辑示例如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 是否响应 touch 事件
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;
// 点是否在 view 内
if (![self pointInside:point withEvent:event]) return nil;
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
// point 进行坐标转化,递归调用,寻找自视图,直到返回 nil 或者 self
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
2.3 Gesture Recognizer
Gesture Recognizer(手势识别器)是系统封装的一些类,用来识别一系列的常见手势,例如点击、长按等。
在上一步中确定了合适的 View 之后,UIWindow 会首先将 touches 事件先传递给 Gesture Recognizer,再传递给视图自己的touch事件。这一点可以通过自定义一个手势,并将手势添加到 View 上来验证。你会发现会先调用自定义手势中的一系列 touches 方法,再调用视图自己的一系列 touches 方法。
如果是UIControl,同时添加了手势和target-action事件,会先响应手势,并且发送一个touchCancelled事件,中断事件响应的传递,也就是target-action事件不会执行。但是对于常见的一些UIControl的子类,苹果内部做了处理,只响应target-action事件,不会触发手势。
2.2 响应者链 touch 事件传递(子视图到父视图)
当确定哪个视图是最佳响应者后,系统会自动触发视图的一系列的 touch 事件。
- touch 事件如果没有被视图重写,默认操作是将事件顺着响应者链条向上传递,将事件传递给上一个响应者进行处理。这个过程与碰撞检测相反,是由子视图传递给父视图。
- 如果重写,要注意写[super touch*],不然就会使响应者链就此终止,不会再向上传递
响应者链的事件传递过程:
- 如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;
- 如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
- 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
- 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
- 如果UIApplication也不能处理该事件或消息,则将其丢弃
- (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;
/**
iOS 9.1 增加的 API,当无法获取真实的 touches 时,UIKit 会提供一个预估值,并设置到 UITouch 对应的 estimatedProperties 中监测更新。当收到新的属性更新时,会通过调用此方法来传递这些更新值。
eg: 当使用 Apple Pencil 靠近屏幕边缘时,传感器无法感应到准确的值,此时会获取一个预估值赋给 estimatedProperties 属性。不断去更新数据,直到获取到准确的值
*/
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);