iOS 事件响应链

iOS 的事件响应链(Responder Chain)就是当 UI 收到某个信号的响应后这种控件间自上到下消息传递的链路。其中最重要的就是事件传递流程以及如何找到第一响应者。

什么是 iOS 的事件响应链机制?

iOS 的事件响应链(Responder Chain)就是当 UI 收到某个信号的响应后这种控件间自上到下消息传递的链路。其中最重要的就是事件传递流程以及如何找到第一响应者

注意和事件传递是倆概念!!!

事件传递

可以使得一个触摸事件选择多个对象来处理,简单来说系统是通过 hitTest:withEvent: 方法找到此次触摸事件的响应视图,然后调用视图的 touchesBegan:withEvent: 方法来处理事件。

事件产生之后,会被加入到由 UIApplication 管理的事件队列里,接下来开始自 UIApplication 往下传递,首先会传递给主 window,然后按照 view 的层级结构一层层往下传递,一直找到最合适的 view(发生 touch 的那个 view)来处理事件。查找最合适的 view 的过程是一个递归的过程,其中涉及到两个重要的方法 hitTest:withEvent:pointInside:withEvent:

当我们点击屏幕时候的事件传递

从逻辑上来说,探测链是最先发生的机制,当触摸事件发生后,iOS 系统根据 Hit-Testing 来确定触摸事件发生在哪个视图对象上。其中主要用到了两个 UIView 中的方法:

UIApplication -> UIWindow -> hitTest:withEvent:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

http://sylarimage.oss-cn-shenzhen.aliyuncs.com/2019-03-22-024827.png

  • ~首先判断当前视图 !hidden && userInteractionEnable && alpha > 0.01 条件通过的时候,到下一步. 否则返回 nil,找不到当前视图~
  • ~通过 pointInside 判断点击的点是否在当前范围内,为 YES 直接下一步. 不在则直接返回 nil。~
  • ~倒序遍历所有子视图,同时调用 hitTest 方法,如果某一个子视图返回了对应的响应视图,这个子视图会直接作为最终的响应视图给响应方,如果为 nil 则继续遍历下一个子视图。如果全部遍历结束都返回 nil,那会返回当前点击位置在当前的视图范围内的视图作为最终响应视图~

更好的原理解析如下:

http://sylarimage.oss-cn-shenzhen.aliyuncs.com/2020-05-12-143231.jpg
Example:

点击 View D

http://sylarimage.oss-cn-shenzhen.aliyuncs.com/2020-05-12-144602.jpg

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"进入A_View---hitTest withEvent ---");
    UIView * view = [super hitTest:point withEvent:event];
    NSLog(@"离开A_View--- hitTest withEvent ---hitTestView:%@",view);
    return view;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event
{
    NSLog(@"A_view--- pointInside withEvent ---");
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"A_view--- pointInside withEvent --- isInside:%d",isInside);
    return isInside;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"A_touchesBegan");
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
    NSLog(@"A_touchesMoved");
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
    NSLog(@"A_touchesEnded");
    [super touchesEnded:touches withEvent:event];
}

-(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"A_touchesCancelled");
    [super touchesCancelled:touches withEvent:event];
}



进入A_View---hitTest withEvent ---
A_view--- pointInside withEvent ---
A_view--- pointInside withEvent --- isInside:1
进入C_View---hitTest withEvent ---
C_view---pointInside withEvent ---
C_view---pointInside withEvent --- isInside:1
进入E_View---hitTest withEvent ---
E_view---pointInside withEvent ---
E_view---pointInside withEvent --- isInside:0
离开E_View---hitTest withEvent ---hitTestView:(null)
进入D_View---hitTest withEvent ---
D_view---pointInside withEvent ---
D_view---pointInside withEvent --- isInside:1
离开D_View---hitTest withEvent ---hitTestView:<DView: 0x12dd11e50; frame = (0 37; 240 61); autoresize = RM+BM; layer = <CALayer: 0x283f87b40>>
离开C_View---hitTest withEvent ---hitTestView:<DView: 0x12dd11e50; frame = (0 37; 240 61); autoresize = RM+BM; layer = <CALayer: 0x283f87b40>>
离开A_View--- hitTest withEvent ---hitTestView:<DView: 0x12dd11e50; frame = (0 37; 240 61); autoresize = RM+BM; layer = <CALayer: 0x283f87b40>>


如上图, 最底层有一个 AView, 按顺序添加 A 的子 View B C, CView 按顺序添加 D E

如 Log, 从底到高传递事件 (addSubView 顺序倒序遍历 Subviews)

递归执行hitTest withEventpointInside withEvent

如果在 hitTest 后的 pointInside检测到该 View 不是触点 View, 则 pointInside返回 NO,hitTest 返回 nil , 继续遍历 Subviews 倒序下一个, 如此反复,直到遍历到最后

要么至死也没能找到能够响应的对象,最终释放。

1. 系统通过 hitTest:withEvent: 方法沿视图层级树从底向上(从根视图开始)从后向前(从逻辑上更靠近屏幕的视图开始)进行遍历,最终返回一个适合响应触摸事件的 View。

2. 原生触摸事件从 Hit-Testing 返回的 View 开始,沿着响应链从上向下进行传递。

详细触摸事件

以下的触摸事件更底层的解释:

事件的生命周期

当指尖触碰屏幕的那一刻,一个触摸事件就在系统中生成了。经过 IPC 进程间通信,事件最终被传递到了合适的应用。在应用内历经峰回路转的奇幻之旅后,最终被释放。大致经过如下图:

http://sylarimage.oss-cn-shenzhen.aliyuncs.com/2020-03-01-132050.jpg

系统响应阶段

  1. 手指触碰屏幕,屏幕感应到触碰后,将事件交由 IOKit 处理。
  2. IOKit 将触摸事件封装成一个 IOHIDEvent 对象,并通过 mach port 传递给 SpringBoard 进程。
  3. SpringBoard 进程因接收到触摸事件,触发了主线程 runloop 的 source1 事件源的回调。

此时 SpringBoard 会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无 APP 运行),则触发 SpringBoard 本身主线程 runloop 的 source0 事件源的回调,将事件交由桌面系统去消耗;若是后者(即有 app 正在前台运行),则将触摸事件通过 IPC 传递给前台 APP 进程,接下来的事情便是 APP 内部对于触摸事件的响应了。

mach port 进程端口,各进程之间通过它进行通信。 SpringBoard.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。

APP 响应阶段

1.APP 进程的 mach port 接受到 SpringBoard 进程传递来的触摸事件,主线程的 runloop 被唤醒,触发了 source1 回调。

2.source1 回调又触发了一个 source0 回调,将接收到的 IOHIDEvent 对象封装成 UIEvent 对象,此时 APP 将正式开始对于触摸事件的响应。

3.source0 回调内部将触摸事件添加到 UIApplication 对象的事件队列中。事件出队后,UIApplication 开始一个寻找最佳响应者的过程,这个过程又称 hit-testing 。接下来如上面 事件传递 的解释

  1. 寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了,关于响应链相关的内容详见 [事件的响应及在响应链中的传递] 一节。事实上,事件除了被响应者消耗,还能被手势识别器或是 target-action 模式捕捉并消耗掉。其中涉及对触摸事件的响应优先级

  2. 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop 若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。

Reference

iOS 触摸事件全家桶

深入理解 iOS 事件机制

iOS 事件处理,看我就够了~

简悦

Page

Index<

Next>