iOS UIGestureRecognizer与原生响应机制处理流程分析

之前在组内分享的时候,曾提到过在iOS事件响应机制中,当原生触摸事件与手势搭配时,相关方法的调用顺序。之前是将手势也理解至响应者链中,后来发现理解有误,所以在此进行一些总结。

UIGestureRecognizer与原生触摸事件均为处理用户点击事件,所以两者必然存在着紧密关联,所以在探究UIGestureRecognizer响应机制之前,我们先了解一下原生触摸事件的响应机制。

由于iOS系统的封闭性,我们在探究时可能需要做一些方法的重写、打点以及一些方法栈的打印。

以下分析是基于iOS 13.3模拟器进行的。

一、原生触摸事件响应机制

1、系统处理

点击事件是如何产生的?以及该事件是如何交给我们的App呢?

这些都属于系统处理,大致流程如下:

  1. 屏幕硬件捕获用户点击,由 IOKit封装为 IOHIDEvent对象,将该对象交由桌面 SpringBoard.app
  2. 桌面 SpringBoard.app获取当前处于前台的App,将收到的 IOHIDEvent对象发送给该App。

2、App处理

App处理过程其实网上已经做过很多次的讨论和总结了,大致过程如下:

  1. 当由 SpringBoard.app通知App时,本质上是由一个source1类型的事件唤起当前runloop,并触发 __IOHIDEventSystemClientQueueCallback回调。
  2. source1触发source0,在source0回调中,处理收到的 IOHIDEvent对象,将之封装为 UIEvent对象。
  3. App开始我们常说的寻找响应者过程,即 Hit-Testing,该过程会寻找到一系列可处理当前时间的控件。
  4. 由上一步获取到的响应者链开始依次处理事件,若事件在某个节点被处理了,那么整个过程就结束了;若事件一直未被处理,那么该事件就会被抛弃。

3. Hit-Testing

Hit-Testing目的为确定当前点击点所处视图链。

该过程主要依赖以下两个方法:

// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;

// default returns YES if point is in bounds
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

我们可以搭建以下结构的视图,并重写UIView的这两个方法,通过点击E视图来了解Hit-Testing的工作原理。

在这里插入图片描述

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"进入 View %@ 的 hitTest:withEvent:", self.name);
    UIView *view = [super hitTest:point withEvent:event];
    NSLog(@"离开 View %@ 的 hitTest:withEvent: %@", self.name, [view isKindOfClass:[CustomView class]] ? ((CustomView *)view).name : @"");
    return view;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"进入 View %@ 的 pointInside:withEvent:", self.name);
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"离开 View %@ 的 pointInside:withEvent: %@", self.name, @(isInside));
    return isInside;
}

输出如下:

2020-02-19 11:22:29.909200+0800 TouchDemo[22486:1782621] 进入 View A 的 hitTest:withEvent:
2020-02-19 11:22:29.909434+0800 TouchDemo[22486:1782621] 进入 View A 的 pointInside:withEvent:
2020-02-19 11:22:29.909626+0800 TouchDemo[22486:1782621] 离开 View A 的 pointInside:withEvent: 1
2020-02-19 11:22:29.909772+0800 TouchDemo[22486:1782621] 进入 View C 的 hitTest:withEvent:
2020-02-19 11:22:29.909986+0800 TouchDemo[22486:1782621] 进入 View C 的 pointInside:withEvent:
2020-02-19 11:22:29.910132+0800 TouchDemo[22486:1782621] 离开 View C 的 pointInside:withEvent: 1
2020-02-19 11:22:29.910271+0800 TouchDemo[22486:1782621] 进入 View E 的 hitTest:withEvent:
2020-02-19 11:22:29.910418+0800 TouchDemo[22486:1782621] 进入 View E 的 pointInside:withEvent:
2020-02-19 11:22:29.910561+0800 TouchDemo[22486:1782621] 离开 View E 的 pointInside:withEvent: 1
2020-02-19 11:22:29.910715+0800 TouchDemo[22486:1782621] 离开 View E 的 hitTest:withEvent: E
2020-02-19 11:22:29.910862+0800 TouchDemo[22486:1782621] 离开 View C 的 hitTest:withEvent: E
2020-02-19 11:22:29.911060+0800 TouchDemo[22486:1782621] 离开 View A 的 hitTest:withEvent: E

有输出不难分析出,系统会通过调用hitTest:withEvent来返回一个可以响应当前事件的控件,该控件可以为自己,也可以为子控件,而pointInside:withEvent:方法则返回当前控件是否可以响应事件。

另外,在绝大多数情况下,这一套流程会触发两次,官方给出的解释如下:

Yes, it’s normal. The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine.

即为了避免在正式处理事件之前做了对View的调整,会再进行确认,而该确认没有多余的副作用。

至此,可以处理事件的响应者链就确定了,该响应者链对应一个点击,理应应当被该点击所知,但是我们在这两个方法中打印一下UIEvent,发现其中并没有对应的UITouch,那么UITouch是在何时创建的,我们可以hook UITouch的init方法来确认一下。

在此之前,我们先获取一下在确定响应者链时的方法栈:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: 0x000000010feee454 TouchDemo`-[CustomView hitTest:withEvent:](self=0x00007ff79650a650, _cmd="hitTest:withEvent:", point=(x = 168, y = 783), event=0x00006000009e0000) at CustomView.m:30:53
    frame #1: 0x00007fff4855afbd UIKitCore`-[UIView(Geometry) _hitTest:withEvent:windowServerHitTestWindow:] + 87
    frame #2: 0x00007fff4855af0c UIKitCore`__38-[UIView(Geometry) hitTest:withEvent:]_block_invoke + 121
    frame #3: 0x00007fff23c3f1e7 CoreFoundation`__NSARRAY_IS_CALLING_OUT_TO_A_BLOCK__ + 7
    frame #4: 0x00007fff23b848fc CoreFoundation`-[__NSArrayM enumerateObjectsWithOptions:usingBlock:] + 524
    frame #5: 0x00007fff4855aaa3 UIKitCore`-[UIView(Geometry) hitTest:withEvent:] + 482
    frame #6: 0x00007fff4855afbd UIKitCore`-[UIView(Geometry) _hitTest:withEvent:windowServerHitTestWindow:] + 87
    frame #7: 0x00007fff4855af0c UIKitCore`__38-[UIView(Geometry) hitTest:withEvent:]_block_invoke + 121
    frame #8: 0x00007fff23c3f1e7 CoreFoundation`__NSARRAY_IS_CALLING_OUT_TO_A_BLOCK__ + 7
    frame #9: 0x00007fff23b848fc CoreFoundation`-[__NSArrayM enumerateObjectsWithOptions:usingBlock:] + 524
    frame #10: 0x00007fff4855aaa3 UIKitCore`-[UIView(Geometry) hitTest:withEvent:] + 482
    frame #11: 0x00007fff4851de63 UIKitCore`-[UIDropShadowView hitTest:withEvent:] + 242
    frame #12: 0x00007fff4855afbd UIKitCore`-[UIView(Geometry) _hitTest:withEvent:windowServerHitTestWindow:] + 87
    frame #13: 0x00007fff4855af0c UIKitCore`__38-[UIView(Geometry) hitTest:withEvent:]_block_invoke + 121
    frame #14: 0x00007fff23c3f1e7 CoreFoundation`__NSARRAY_IS_CALLING_OUT_TO_A_BLOCK__ + 7
    frame #15: 0x00007fff23b848fc CoreFoundation`-[__NSArrayM enumerateObjectsWithOptions:usingBlock:] + 524
    frame #16: 0x00007fff4855aaa3 UIKitCore`-[UIView(Geometry) hitTest:withEvent:] + 482
    frame #17: 0x00007fff48533c32 UIKitCore`-[UITransitionView hitTest:withEvent:] + 44
    frame #18: 0x00007fff4855afbd UIKitCore`-[UIView(Geometry) _hitTest:withEvent:windowServerHitTestWindow:] + 87
    frame #19: 0x00007fff4855af0c UIKitCore`__38-[UIView(Geometry) hitTest:withEvent:]_block_invoke + 121
    frame #20: 0x00007fff23c3f1e7 CoreFoundation`__NSARRAY_IS_CALLING_OUT_TO_A_BLOCK__ + 7
    frame #21: 0x00007fff23b848fc CoreFoundation`-[__NSArrayM enumerateObjectsWithOptions:usingBlock:] + 524
    fra
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值