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。


传递流程图如下:

iOS事件的传递与响应_IOHIDEventSystemCl


传递堆栈信息如下:

iOS事件的传递与响应_IOHIDEventSystemCl_02


找到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事件,不会触发手势。

iOS事件的传递与响应_CGRectContainsPoint_03

2.2 响应者链 touch 事件传递(子视图到父视图)

当确定哪个视图是最佳响应者后,系统会自动触发视图的一系列的 touch 事件。

  • touch 事件如果没有被视图重写,默认操作是将事件顺着响应者链条向上传递,将事件传递给上一个响应者进行处理。这个过程与碰撞检测相反,是由子视图传递给父视图。
  • 如果重写,要注意写[super touch*],不然就会使响应者链就此终止,不会再向上传递

响应者链的事件传递过程:

  1. 如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;
  2. 如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
  3. 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
  4. 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
  5. 如果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);