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
    frame #22: 0x00007fff4855aaa3 UIKitCore`-[UIView(Geometry) hitTest:withEvent:] + 482
    frame #23: 0x00007fff4855afbd UIKitCore`-[UIView(Geometry) _hitTest:withEvent:windowServerHitTestWindow:] + 87
    frame #24: 0x00007fff480e145e UIKitCore`-[UIWindow _hitTestLocation:inScene:withWindowServerHitTestWindow:event:] + 113
    frame #25: 0x00007fff480e128e UIKitCore`__63+[UIWindow _hitTestToPoint:forEvent:windowServerHitTestWindow:]_block_invoke + 84
    frame #26: 0x00007fff482e977b UIKitCore`__46-[UIWindowScene _topVisibleWindowPassingTest:]_block_invoke + 119
    frame #27: 0x00007fff482e9d05 UIKitCore`-[UIWindowScene _enumerateWindowsIncludingInternalWindows:onlyVisibleWindows:asCopy:stopped:withBlock:] + 257
    frame #28: 0x00007fff482e9670 UIKitCore`-[UIWindowScene _topVisibleWindowPassingTest:] + 498
    frame #29: 0x00007fff480e11e3 UIKitCore`+[UIWindow _hitTestToPoint:forEvent:windowServerHitTestWindow:] + 243
    frame #30: 0x00007fff480e154a UIKitCore`-[UIWindow _targetWindowForPathIndex:atPoint:forEvent:windowServerHitTestWindow:] + 181
    frame #31: 0x00007fff4812721d UIKitCore`____updateTouchesWithDigitizerEventAndDetermineIfShouldSend_block_invoke.52 + 851
    frame #32: 0x00007fff48137d4c UIKitCore`_UIEventHIDEnumerateChildren + 123
    frame #33: 0x00007fff4812b3e8 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 1933
    frame #34: 0x00007fff4812f1e0 UIKitCore`__handleEventQueueInternal + 5980
    frame #35: 0x00007fff23bd4471 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #36: 0x00007fff23bd439c CoreFoundation`__CFRunLoopDoSource0 + 76
    frame #37: 0x00007fff23bd3b74 CoreFoundation`__CFRunLoopDoSources0 + 180
    frame #38: 0x00007fff23bce87f CoreFoundation`__CFRunLoopRun + 1263
    frame #39: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438
    frame #40: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
    frame #41: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
    frame #42: 0x000000010fef28e4 TouchDemo`main(argc=1, argv=0x00007ffedfd11d08) at main.m:18:12
    frame #43: 0x00007fff5227ec25 libdyld.dylib`start + 1
    frame #44: 0x00007fff5227ec25 libdyld.dylib`start + 1

在点击之后,第一次调用UITouch的init方法时,我们获取一下此刻的方法栈:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x000000010fef2717 TouchDemo`-[UITouch(self=0x00007ff798904ca0, _cmd="init") sg_init] at UITouch+Custom.m:22:5
    frame #1: 0x00007fff48127463 UIKitCore`____updateTouchesWithDigitizerEventAndDetermineIfShouldSend_block_invoke.52 + 1433
    frame #2: 0x00007fff48137d4c UIKitCore`_UIEventHIDEnumerateChildren + 123
    frame #3: 0x00007fff4812b3e8 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 1933
    frame #4: 0x00007fff4812f1e0 UIKitCore`__handleEventQueueInternal + 5980
    frame #5: 0x00007fff23bd4471 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #6: 0x00007fff23bd439c CoreFoundation`__CFRunLoopDoSource0 + 76
    frame #7: 0x00007fff23bd3b74 CoreFoundation`__CFRunLoopDoSources0 + 180
    frame #8: 0x00007fff23bce87f CoreFoundation`__CFRunLoopRun + 1263
    frame #9: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438
    frame #10: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
    frame #11: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
    frame #12: 0x000000010fef28e4 TouchDemo`main(argc=1, argv=0x00007ffedfd11d08) at main.m:18:12
    frame #13: 0x00007fff5227ec25 libdyld.dylib`start + 1
    frame #14: 0x00007fff5227ec25 libdyld.dylib`start + 1

在两次方法栈中,从底层到上层是有一部分方法是一致的,说明确定响应者链与创建事件是在一个流程中确定的,而最后一个相同方法如下:

//获取响应者链
UIKitCore`____updateTouchesWithDigitizerEventAndDetermineIfShouldSend_block_invoke.52 + 851

//创建UITouch
UIKitCore`____updateTouchesWithDigitizerEventAndDetermineIfShouldSend_block_invoke.52 + 1433

由方法的偏移地址可知,系统是先确定响应者链,之后才会创建可响应的UITouch,并将响应者链告知给UITouch,进而进行之后的事件处理。

4.UIView处理事件

此时,系统已经探寻到响应者链,同时也创建了需要处理的UITouch,那么UITouch是如何交由文章开头提到的三种事件方式进行处理的,我们从简单的UIView开始看起。

我们重写touchesBegan:withEvent:等方法,并查看打印:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"调用 View %@ 的 touchesBegan:withEvent:", self.name);
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"调用 View %@ 的 touchesMoved:withEvent:", self.name);
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"调用 View %@ 的 touchesEnded:withEvent:", self.name);
    [super touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"调用 View %@ 的 touchesCancelled:withEvent:", self.name);
    [super touchesCancelled:touches withEvent:event];
}

打印如下:

2020-02-19 12:04:31.733527+0800 TouchDemo[22583:1800532] 调用 View E 的 touchesBegan:withEvent:
2020-02-19 12:04:31.733691+0800 TouchDemo[22583:1800532] 调用 View C 的 touchesBegan:withEvent:
2020-02-19 12:04:31.733823+0800 TouchDemo[22583:1800532] 调用 View A 的 touchesBegan:withEvent:
2020-02-19 12:04:31.764272+0800 TouchDemo[22583:1800532] 调用 View E 的 touchesEnded:withEvent:
2020-02-19 12:04:31.764587+0800 TouchDemo[22583:1800532] 调用 View C 的 touchesEnded:withEvent:
2020-02-19 12:04:31.764779+0800 TouchDemo[22583:1800532] 调用 View A 的 touchesEnded:withEvent:

UIView的处理遵循了响应者链的顺序,由最顶层到最底层去执行对应方法,同时在调用相关方法时,我们可以打印一下相应方法栈:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000108956106 TouchDemo`-[CustomView touchesBegan:withEvent:](self=0x00007fadd1e0ffc0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000039d8640) at CustomView.m:53:58
    frame #1: 0x00007fff480ce8de UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1867
    frame #2: 0x00007fff480d04c6 UIKitCore`-[UIWindow sendEvent:] + 4596
    frame #3: 0x00007fff480ab53b UIKitCore`-[UIApplication sendEvent:] + 356
    frame #4: 0x00007fff4812c71a UIKitCore`__dispatchPreprocessedEventFromEventQueue + 6847
    frame #5: 0x00007fff4812f1e0 UIKitCore`__handleEventQueueInternal + 5980
    frame #6: 0x00007fff23bd4471 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #7: 0x00007fff23bd439c CoreFoundation`__CFRunLoopDoSource0 + 76
    frame #8: 0x00007fff23bd3b74 CoreFoundation`__CFRunLoopDoSources0 + 180
    frame #9: 0x00007fff23bce87f CoreFoundation`__CFRunLoopRun + 1263
    frame #10: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438
    frame #11: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
    frame #12: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
    frame #13: 0x000000010895b0b4 TouchDemo`main(argc=1, argv=0x00007ffee72a9d08) at main.m:18:12
    frame #14: 0x00007fff5227ec25 libdyld.dylib`start + 1
    frame #15: 0x00007fff5227ec25 libdyld.dylib`start + 1

方法栈中同样有Hit-Testing过程中的方法:__dispatchPreprocessedEventFromEventQueue,由此可见,在确定了响应者链后,系统会将UIEvent交给UIApplication,UIApplication将UIEvent交给UIWindow,继而调用sendEvent:去处理其中的UITouch,处理每一个UITouch时,都能从UITouch中获取到它的响应者链,进而调用到原生触摸方法。

二、 UIGestureRecognizer

在上述Demo基础上,为View E添加一个UITapGestureRecognizer的子类手势实例,重写该子类中touchesBegan:withEvent:等方法,然后点击View E,查看输出日志:

2020-02-19 16:06:29.095422+0800 TouchDemo[23684:1914794] 进入 View A 的 hitTest:withEvent:
2020-02-19 16:06:29.095568+0800 TouchDemo[23684:1914794] 进入 View A 的 pointInside:withEvent:
2020-02-19 16:06:29.099405+0800 TouchDemo[23684:1914794] 离开 View A 的 pointInside:withEvent: 1
2020-02-19 16:06:29.099517+0800 TouchDemo[23684:1914794] 进入 View C 的 hitTest:withEvent:
2020-02-19 16:06:29.099635+0800 TouchDemo[23684:1914794] 进入 View C 的 pointInside:withEvent:
2020-02-19 16:06:29.099720+0800 TouchDemo[23684:1914794] 离开 View C 的 pointInside:withEvent: 1
2020-02-19 16:06:29.099803+0800 TouchDemo[23684:1914794] 进入 View E 的 hitTest:withEvent:
2020-02-19 16:06:29.099942+0800 TouchDemo[23684:1914794] 进入 View E 的 pointInside:withEvent:
2020-02-19 16:06:29.100057+0800 TouchDemo[23684:1914794] 离开 View E 的 pointInside:withEvent: 1
2020-02-19 16:06:29.100139+0800 TouchDemo[23684:1914794] 离开 View E 的 hitTest:withEvent: E
2020-02-19 16:06:29.100219+0800 TouchDemo[23684:1914794] 离开 View C 的 hitTest:withEvent: E
2020-02-19 16:06:29.100298+0800 TouchDemo[23684:1914794] 离开 View A 的 hitTest:withEvent: E
2020-02-19 16:06:29.101060+0800 TouchDemo[23684:1914794] 调用 gesture View E gr touchesBegan:withEvent:
2020-02-19 16:06:29.101224+0800 TouchDemo[23684:1914794] 离开 gesture View E gr touchesBegan:withEvent:
2020-02-19 16:06:29.101630+0800 TouchDemo[23684:1914794] 调用 View E 的 touchesBegan:withEvent:
2020-02-19 16:06:29.101759+0800 TouchDemo[23684:1914794] 调用 View C 的 touchesBegan:withEvent:
2020-02-19 16:06:29.101871+0800 TouchDemo[23684:1914794] 调用 View A 的 touchesBegan:withEvent:
2020-02-19 16:06:29.102400+0800 TouchDemo[23684:1914794] 调用 gesture View E gr touchesEnded:withEvent: UIGestureRecognizerStatePossible
2020-02-19 16:06:29.102536+0800 TouchDemo[23684:1914794] 离开 gesture View E gr touchesEnded:withEvent: UIGestureRecognizerStateEnded
2020-02-19 16:06:29.103273+0800 TouchDemo[23684:1914794] 调用 gesture 响应方法: View E gr 3
2020-02-19 16:06:29.103696+0800 TouchDemo[23684:1914794] 调用 View E 的 touchesCancelled:withEvent:
2020-02-19 16:06:29.104039+0800 TouchDemo[23684:1914794] 调用 View C 的 touchesCancelled:withEvent:
2020-02-19 16:06:29.104428+0800 TouchDemo[23684:1914794] 调用 View A 的 touchesCancelled:withEvent:

从打印日志中,可以发现以下几点:

  1. 加上UIGestureRecognizer之后,同样存在寻找响应者链的过程,且该过程没有任何变化。
  2. 加上UIGestureRecognizer之后,响应者链的所有原生触摸的touchesBegan:withEvent:方法会正常调用,但是在这之前会插入UIGestureRecognizer的touchesBegan:withEvent:
  3. 加上UIGestureRecognizer之后,响应者链的所有原生触摸的touchesEnd:withEvent:方法不会调用,而touchesCancelled:withEvent:方法会被调用,且在这之前会插入UIGestureRecognizer的touchesEnd:withEvent:方法及UIGestureRecognizer的响应方法。

我们从现象出发,来进行分析。

第一点我们无需关注,第二点,新增了UIGestureRecognizer的touchesBegan:withEvent:方法,我们可以看一下此刻的方法栈:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001078a2396 TouchDemo`-[CustomTapGestureRecognizer touchesBegan:withEvent:](self=0x0000600000905700, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000600000400960) at CustomTapGestureRecognizer.m:15:57
    frame #1: 0x00007fff47c3ccaa UIKitCore`-[UIGestureRecognizer _touchesBegan:withEvent:] + 198
    frame #2: 0x00007fff48118043 UIKitCore`-[UITouchesEvent _sendEventToGestureRecognizer:] + 302
    frame #3: 0x00007fff47c31200 UIKitCore`__47-[UIGestureEnvironment _updateForEvent:window:]_block_invoke + 70
    frame #4: 0x00007fff47c3133a UIKitCore`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 259
    frame #5: 0x00007fff47c3117f UIKitCore`-[UIGestureEnvironment _updateForEvent:window:] + 200
    frame #6: 0x00007fff480d04b0 UIKitCore`-[UIWindow sendEvent:] + 4574
    frame #7: 0x00007fff480ab53b UIKitCore`-[UIApplication sendEvent:] + 356
    frame #8: 0x00007fff4812c71a UIKitCore`__dispatchPreprocessedEventFromEventQueue + 6847
    frame #9: 0x00007fff4812f1e0 UIKitCore`__handleEventQueueInternal + 5980
    frame #10: 0x00007fff23bd4471 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #11: 0x00007fff23bd439c CoreFoundation`__CFRunLoopDoSource0 + 76
    frame #12: 0x00007fff23bd3b74 CoreFoundation`__CFRunLoopDoSources0 + 180
    frame #13: 0x00007fff23bce87f CoreFoundation`__CFRunLoopRun + 1263
    frame #14: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438
    frame #15: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
    frame #16: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
    frame #17: 0x00000001078a2fc4 TouchDemo`main(argc=1, argv=0x00007ffee8362d08) at main.m:18:12
    frame #18: 0x00007fff5227ec25 libdyld.dylib`start + 1
    frame #19: 0x00007fff5227ec25 libdyld.dylib`start + 1

从调用栈中,可以看到,到frame #7: 0x00007fff480ab53b UIKitCore`-[UIApplication sendEvent:] + 356时,UIGestureRecognizer的处理都与原生触摸方法一致,但进入[UIWindow senEvent:]后,从方法偏移来看,首先使用UIGestureEnvironment来处理UIGestureRecognizer,然后才使用[UIWindow _sendTouchesForEvent:]处理原生手势,至此,可以确定UIGestureRecognizer和原生触摸方法是在事件正式处理时才发生交集,而交集的关键点就在UIGestureEnvironment这个类上。

UIGestureEnvironment是一个私有类,我们可以将它的类信息导出,其中有用的信息如下:

@interface UIGestureEnvironment : NSObject {

	CFRunLoopObserverRef _gestureEnvironmentUpdateObserver;
	NSMutableSet* _gestureRecognizersNeedingUpdate;
	NSMutableSet* _gestureRecognizersNeedingReset;
	NSMutableSet* _gestureRecognizersNeedingRemoval;
	NSMutableArray* _dirtyGestureRecognizers;
	NSMutableArray* _delayedTouches;
	NSMutableArray* _delayedTouchesToSend;
	NSMutableArray* _delayedPresses;
	NSMutableArray* _delayedPressesToSend;
	NSMutableArray* _preUpdateActions;
	bool _dirtyGestureRecognizersUnsorted;
	bool _updateExclusivity;
	UIGestureGraph* _dependencyGraph;
	NSMapTable* _nodesByGestureRecognizer;

}

- (void)_cancelTouches:(id)arg1 event:(id)arg2;
- (void)_updateForEvent:(id)arg1 window:(id)arg2;
- (void)addGestureRecognizer:(id)arg1;
- (void)removeGestureRecognizer:(id)arg1;

从类名看,该类是一个手势管理类,而且也提供了对UIGestureRecognizer添加和删除的API,我们就先对该类的init方法和addGestureRecognizer:方法进行hook,看看他们的调用时机:

init方法:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: 0x00000001061afd37 TouchDemo`-[HookTool UIGestureEnvironment_init](self=0x0000600002a64af0, _cmd="init") at HookTool.m:39:5
    frame #1: 0x00007fff48086ad7 UIKitCore`-[UIApplication init] + 417
    frame #2: 0x00007fff480924fb UIKitCore`UIApplicationInstantiateSingleton + 93
    frame #3: 0x00007fff48092aca UIKitCore`UIApplicationMain + 978
    frame #4: 0x00000001061affc4 TouchDemo`main(argc=1, argv=0x00007ffee9a55d08) at main.m:18:12
    frame #5: 0x00007fff5227ec25 libdyld.dylib`start + 1
    frame #6: 0x00007fff5227ec25 libdyld.dylib`start + 1

addGestureRecognizer:方法:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
  * frame #0: 0x00000001061afcc4 TouchDemo`-[HookTool UIGestureEnvironment_addGestureRecognizer:](self=0x0000600002a64af0, _cmd="addGestureRecognizer:", gr=0x0000600003d64600) at HookTool.m:34:6
    frame #1: 0x00007fff48572f87 UIKitCore`-[UIView(Internal) _addGestureRecognizer:atEnd:] + 377
    frame #2: 0x00000001061aabc7 TouchDemo`-[CustomView addGesture](self=0x00007fd86d707d50, _cmd="addGesture") at CustomView.m:36:5
    frame #3: 0x00000001061aa8af TouchDemo`-[ViewController viewDidLoad](self=0x00007fd86d4090f0, _cmd="viewDidLoad") at ViewController.m:36:5
    frame #4: 0x00007fff47a0ef01 UIKitCore`-[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 83
    frame #5: 0x00007fff47a13e5a UIKitCore`-[UIViewController loadViewIfRequired] + 1084
    frame #6: 0x00007fff47a14277 UIKitCore`-[UIViewController view] + 27
    frame #7: 0x00007fff480ca3cf UIKitCore`-[UIWindow addRootViewControllerViewIfPossible] + 150
    frame #8: 0x00007fff480c9ac0 UIKitCore`-[UIWindow _updateLayerOrderingAndSetLayerHidden:actionBlock:] + 232
    frame #9: 0x00007fff480cab43 UIKitCore`-[UIWindow _setHidden:forced:] + 362
    frame #10: 0x00007fff480ddef1 UIKitCore`-[UIWindow _mainQueue_makeKeyAndVisible] + 42
    frame #11: 0x00007fff4808b53b UIKitCore`-[UIApplication _callInitializationDelegatesWithActions:forCanvas:payload:fromOriginatingProcess:] + 4199
    frame #12: 0x00007fff48090f05 UIKitCore`-[UIApplication _runWithMainScene:transitionContext:completion:] + 1226
    frame #13: 0x00007fff477c576d UIKitCore`-[_UISceneLifecycleMultiplexer completeApplicationLaunchWithFBSScene:transitionContext:] + 122
    frame #14: 0x00007fff47cb44c1 UIKitCore`_UIScenePerformActionsWithLifecycleActionMask + 83
    frame #15: 0x00007fff477c627f UIKitCore`__101-[_UISceneLifecycleMultiplexer _evalTransitionToSettings:fromSettings:forceExit:withTransitionStore:]_block_invoke + 198
    frame #16: 0x00007fff477c5c8e UIKitCore`-[_UISceneLifecycleMultiplexer _performBlock:withApplicationOfDeactivationReasons:fromReasons:] + 296
    frame #17: 0x00007fff477c60ac UIKitCore`-[_UISceneLifecycleMultiplexer _evalTransitionToSettings:fromSettings:forceExit:withTransitionStore:] + 818
    frame #18: 0x00007fff477c5941 UIKitCore`-[_UISceneLifecycleMultiplexer uiScene:transitionedFromState:withTransitionContext:] + 345
    frame #19: 0x00007fff477c9f3f UIKitCore`__186-[_UIWindowSceneFBSSceneTransitionContextDrivenLifecycleSettingsDiffAction _performActionsForUIScene:withUpdatedFBSScene:settingsDiff:fromSettings:transitionContext:lifecycleActionType:]_block_invoke_2 + 178
    frame #20: 0x00007fff47bd8c83 UIKitCore`+[BSAnimationSettings(UIKit) tryAnimatingWithSettings:actions:completion:] + 865
    frame #21: 0x00007fff47cd2dff UIKitCore`_UISceneSettingsDiffActionPerformChangesWithTransitionContext + 240
    frame #22: 0x00007fff477c9c5a UIKitCore`__186-[_UIWindowSceneFBSSceneTransitionContextDrivenLifecycleSettingsDiffAction _performActionsForUIScene:withUpdatedFBSScene:settingsDiff:fromSettings:transitionContext:lifecycleActionType:]_block_invoke + 153
    frame #23: 0x00007fff47cd2d02 UIKitCore`_UISceneSettingsDiffActionPerformActionsWithDelayForTransitionContext + 84
    frame #24: 0x00007fff477c9ac8 UIKitCore`-[_UIWindowSceneFBSSceneTransitionContextDrivenLifecycleSettingsDiffAction _performActionsForUIScene:withUpdatedFBSScene:settingsDiff:fromSettings:transitionContext:lifecycleActionType:] + 381
    frame #25: 0x00007fff476206e7 UIKitCore`__64-[UIScene scene:didUpdateWithDiff:transitionContext:completion:]_block_invoke + 657
    frame #26: 0x00007fff4761f26c UIKitCore`-[UIScene _emitSceneSettingsUpdateResponseForCompletion:afterSceneUpdateWork:] + 248
    frame #27: 0x00007fff47620411 UIKitCore`-[UIScene scene:didUpdateWithDiff:transitionContext:completion:] + 210
    frame #28: 0x00007fff4808f599 UIKitCore`-[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 535
    frame #29: 0x00007fff47bfa7f5 UIKitCore`-[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 361
    frame #30: 0x00007fff365d6165 FrontBoardServices`-[FBSSceneImpl _callOutQueue_agent_didCreateWithTransitionContext:completion:] + 442
    frame #31: 0x00007fff365fc4d8 FrontBoardServices`__86-[FBSWorkspaceScenesClient sceneID:createWithParameters:transitionContext:completion:]_block_invoke.154 + 102
    frame #32: 0x00007fff365e0c45 FrontBoardServices`-[FBSWorkspace _calloutQueue_executeCalloutFromSource:withBlock:] + 220
    frame #33: 0x00007fff365fc169 FrontBoardServices`__86-[FBSWorkspaceScenesClient sceneID:createWithParameters:transitionContext:completion:]_block_invoke + 355
    frame #34: 0x00000001064d8d48 libdispatch.dylib`_dispatch_client_callout + 8
    frame #35: 0x00000001064dbcb9 libdispatch.dylib`_dispatch_block_invoke_direct + 300
    frame #36: 0x00007fff3662237e FrontBoardServices`__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 30
    frame #37: 0x00007fff3662206c FrontBoardServices`-[FBSSerialQueue _queue_performNextIfPossible] + 441
    frame #38: 0x00007fff3662257b FrontBoardServices`-[FBSSerialQueue _performNextFromRunLoopSource] + 22
    frame #39: 0x00007fff23bd4471 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #40: 0x00007fff23bd439c CoreFoundation`__CFRunLoopDoSource0 + 76
    frame #41: 0x00007fff23bd3b74 CoreFoundation`__CFRunLoopDoSources0 + 180
    frame #42: 0x00007fff23bce87f CoreFoundation`__CFRunLoopRun + 1263
    frame #43: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438
    frame #44: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
    frame #45: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
    frame #46: 0x00000001061affc4 TouchDemo`main(argc=1, argv=0x00007ffee9a55d08) at main.m:18:12
    frame #47: 0x00007fff5227ec25 libdyld.dylib`start + 1
    frame #48: 0x00007fff5227ec25 libdyld.dylib`start + 1

通过方法栈,可以得知UIGestureEnvironment在UIApplication创建时就被创建了,即该类本质上就是一个单例,而在UIView的addGestureRecognizer:方法中,会获取该单例,并同时把UIGestureRecognizer添加到UIView和UIGestureEnvironment中,同时UIGestureRecognizer中也会记录它所属的UIGestureEnvironment。

至此我们可以理解上述不同点中的第二点现象:系统在开始处理事件时,会先调用UIGestureEnvironment来调用当前可响应事件的UIGestureRecognizer的对应方法,之后才会将UITouch按照响应者链去交给原生触摸方法处理。

那既然这样,之后的情况应当是先调用UIGestureRecognizer的touchesEnd:withEvent:方法,然后再调用UIView的touchesEnd:withEvent:方法,但情况并非如此,我们先确认一下UIGestureRecognizer的touchesEnd:withEvent:方法是否和我们想象的一样:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.1
  * frame #0: 0x00000001061af5f6 TouchDemo`-[CustomTapGestureRecognizer touchesEnded:withEvent:](self=0x0000600003d64600, _cmd="touchesEnded:withEvent:", touches=1 element, event=0x0000600003074460) at CustomTapGestureRecognizer.m:29:60
    frame #1: 0x00007fff47c3ce18 UIKitCore`-[UIGestureRecognizer _touchesEnded:withEvent:] + 142
    frame #2: 0x00007fff481181b6 UIKitCore`-[UITouchesEvent _sendEventToGestureRecognizer:] + 673
    frame #3: 0x00007fff47c31200 UIKitCore`__47-[UIGestureEnvironment _updateForEvent:window:]_block_invoke + 70
    frame #4: 0x00007fff47c3133a UIKitCore`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 259
    frame #5: 0x00007fff47c3117f UIKitCore`-[UIGestureEnvironment _updateForEvent:window:] + 200
    frame #6: 0x00007fff480d04b0 UIKitCore`-[UIWindow sendEvent:] + 4574
    frame #7: 0x00007fff480ab53b UIKitCore`-[UIApplication sendEvent:] + 356
    frame #8: 0x00007fff4812c71a UIKitCore`__dispatchPreprocessedEventFromEventQueue + 6847
    frame #9: 0x00007fff4812f1e0 UIKitCore`__handleEventQueueInternal + 5980
    frame #10: 0x00007fff4812f5d3 UIKitCore`__handleEventQueueInternal + 6991
    frame #11: 0x00007fff23bd4471 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #12: 0x00007fff23bd439c CoreFoundation`__CFRunLoopDoSource0 + 76
    frame #13: 0x00007fff23bd3bcc CoreFoundation`__CFRunLoopDoSources0 + 268
    frame #14: 0x00007fff23bce87f CoreFoundation`__CFRunLoopRun + 1263
    frame #15: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438
    frame #16: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
    frame #17: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
    frame #18: 0x00000001061affc4 TouchDemo`main(argc=1, argv=0x00007ffee9a55d08) at main.m:18:12
    frame #19: 0x00007fff5227ec25 libdyld.dylib`start + 1
    frame #20: 0x00007fff5227ec25 libdyld.dylib`start + 1

由方法栈可知,UIGestureRecognizer的touchesEnd:withEvent:方法是由UIGestureEnvironment调起的,这与我们的想法一致,那接下来看一下UIView的touchesCancelled:withEvent:方法的调用情况:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 7.1
  * frame #0: 0x00000001061ab246 TouchDemo`-[CustomView touchesCancelled:withEvent:](self=0x00007fd86d707d50, _cmd="touchesCancelled:withEvent:", touches=1 element, event=0x0000600003074460) at CustomView.m:76:62
    frame #1: 0x00007fff480a4a43 UIKitCore`__106-[UIApplication _cancelViewProcessingOfTouchesOrPresses:withEvent:sendingCancelToViewsOfTouchesOrPresses:]_block_invoke + 609
    frame #2: 0x00007fff480a429e UIKitCore`-[UIApplication _cancelTouchesOrPresses:withEvent:includingGestures:notificationBlock:] + 1163
    frame #3: 0x00007fff480a47ac UIKitCore`-[UIApplication _cancelViewProcessingOfTouchesOrPresses:withEvent:sendingCancelToViewsOfTouchesOrPresses:] + 158
    frame #4: 0x00007fff47c37f2f UIKitCore`-[UIGestureEnvironment _cancelTouches:event:] + 707
    frame #5: 0x00007fff47c40115 UIKitCore`-[UIGestureRecognizer _updateGestureForActiveEvents] + 1779
    frame #6: 0x00007fff47c31eda UIKitCore`_UIGestureEnvironmentUpdate + 2706
    frame #7: 0x00007fff47c3140a UIKitCore`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 467
    frame #8: 0x00007fff47c3117f UIKitCore`-[UIGestureEnvironment _updateForEvent:window:] + 200
    frame #9: 0x00007fff480d04b0 UIKitCore`-[UIWindow sendEvent:] + 4574
    frame #10: 0x00007fff480ab53b UIKitCore`-[UIApplication sendEvent:] + 356
    frame #11: 0x00007fff4812c71a UIKitCore`__dispatchPreprocessedEventFromEventQueue + 6847
    frame #12: 0x00007fff4812f1e0 UIKitCore`__handleEventQueueInternal + 5980
    frame #13: 0x00007fff4812f5d3 UIKitCore`__handleEventQueueInternal + 6991
    frame #14: 0x00007fff23bd4471 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #15: 0x00007fff23bd439c CoreFoundation`__CFRunLoopDoSource0 + 76
    frame #16: 0x00007fff23bd3bcc CoreFoundation`__CFRunLoopDoSources0 + 268
    frame #17: 0x00007fff23bce87f CoreFoundation`__CFRunLoopRun + 1263
    frame #18: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438
    frame #19: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
    frame #20: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
    frame #21: 0x00000001061affc4 TouchDemo`main(argc=1, argv=0x00007ffee9a55d08) at main.m:18:12
    frame #22: 0x00007fff5227ec25 libdyld.dylib`start + 1
    frame #23: 0x00007fff5227ec25 libdyld.dylib`start + 1

从方法栈看出,在UIGestureEnvironment将UIEvent交由某个UIGestureRecognizer处理完后,会调用_cancelTouches:event:方法,而该方法会调用UIApplication,并把处理后的UITouch传给UIApplication,而UIApplication则可以通过记录在UITouch中的对应响应者链,依次调用响应者链中的touchesCancelled:withEvent:,这就造成了上述不同点中第三点,此刻UIView的touchesCancelled:withEvent:方法并不是由UIWindow发出的,而是由UIGestureEnvironment发出的。

至此,UIGestureRecognizer与原生响应机制处理流程就有了很好的理解:

  1. Hit-Testing获取响应者链的过程还是点击事件处理的基础。
  2. 手势是通过一个全局的UIGestureEnvironment来处理的。
  3. 手势的处理是要早于响应者链的。
  4. touchesBegan:withEvent:方法是不受影响调用的。
  5. 当某一个UITouch的End事件被手势截获并处理后,由UIGestureEnvironment负责通知该UITouch响应者链上的所有UIView调用touchesCancelled:withEvent:方法。

流程图如下:

在这里插入图片描述

发布了74 篇原创文章 · 获赞 34 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/104397536
今日推荐