![](https://img.haomeiwen.com/i6526232/b8c10cd2e88a21ff.png)
- IOKit.framework 为系统内核的库
- SpringBoard.app 相当于手机的桌面
- Source1 主要接收系统的消息
- Source0 - UIApplication - UIWindow
-
从window开始系统会调用hitTest:withEvent:和pointInside来找到最优响应者,具体过程可参考下图
图片.png
比如我们在self.view 上依次添加view1、view2、view3(3个view是同级关系),那么系统用hitTest以及pointInside时会先从view3开始便利,如果pointInside返回YES就继续遍历view3的subviews(如果view3没有子视图,那么会返回view3),如果pointInside返回NO就开始遍历view2。反序遍历,最后一个添加的subview开始。也算是一种算法优化。后面会具体介绍hitTest的内部实现和具体使用场景。
- UITouch会给gestureRecognizers和最优响应者也就是hitTestView发送消息
默认view会走其touchBegan:withEvent:等方法,当gestureRecognizers找到识别的gestureRecognizer后,将会独自占有该touch,即会调用其他gestureRecognizer和hitTest view的touchCancelled:withEvent:方法,并且它们不再收到该touche事件,也就不会走响应链流程。下面会具体阐述UIContol和UIScrollView和其子类与手势之间的冲突和关系。
hitTest:withEnvent-->内部调用pointInside:withEvent查找最顶层view--->view调用UITouch一系列方法--->gestureRecognizers找到合适Recognizer----->调用其他Recognizer和veiew的touchCancelled:withEvent方法 同时触发合适Recognizer的selector
当该事件响应完毕,主线程的Runloop开始睡眠,等待下一个事件。
普通button点击时间响应堆栈调用
frame #0: 0x000000010db26367 demo-事件传递响应链`-[ViewController recClick](self=0x00007fbe3e406310, _cmd="recClick") at ViewController.m:57:5
frame #1: 0x00007fff47c3a347 UIKitCore`-[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 44
frame #2: 0x00007fff47c4333d UIKitCore`_UIGestureRecognizerSendTargetActions + 109
frame #3: 0x00007fff47c409ea UIKitCore`_UIGestureRecognizerSendActions + 298
frame #4: 0x00007fff47c3fd17 UIKitCore`-[UIGestureRecognizer _updateGestureForActiveEvents] + 757
frame #5: 0x00007fff47c31eda UIKitCore`_UIGestureEnvironmentUpdate + 2706
frame #6: 0x00007fff47c3140a UIKitCore`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 467
frame #7: 0x00007fff47c3117f UIKitCore`-[UIGestureEnvironment _updateForEvent:window:] + 200
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
---------------------------------------------------------------------------------------------------
frame #8: 0x00007fff480d04b0 UIKitCore`-[UIWindow sendEvent:] + 4574
frame #9: 0x00007fff480ab53b UIKitCore`-[UIApplication sendEvent:] + 356
frame #10: 0x00007fff4812c71a UIKitCore`__dispatchPreprocessedEventFromEventQueue + 6847
frame #11: 0x00007fff4812f1e0 UIKitCore`__handleEventQueueInternal + 5980
frame #12: 0x00007fff23bd4471 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
frame #13: 0x00007fff23bd439c CoreFoundation`__CFRunLoopDoSource0 + 76
frame #14: 0x00007fff23bd3b74 CoreFoundation`__CFRunLoopDoSources0 + 180
frame #15: 0x00007fff23bce87f CoreFoundation`__CFRunLoopRun + 1263
frame #16: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438
frame #17: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
frame #18: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
frame #19: 0x000000010db2740d demo-事件传递响应链`main(argc=1, argv=0x00007ffee20d8c90) at main.m:18:12
添加手势点击事件响应堆栈调用
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
* frame #0: 0x00000001079563b7 demo-事件传递响应链`-[ViewController click:](self=0x00007fedf3d09a40, _cmd="click:", ben=0x00007fedf3c08b20) at ViewController.m:68:18
frame #1: 0x00007fff48093fff UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
frame #2: 0x00007fff47a6c00e UIKitCore`-[UIControl sendAction:to:forEvent:] + 223
frame #3: 0x00007fff47a6c358 UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 398
frame #4: 0x00007fff47a6b2b7 UIKitCore`-[UIControl touchesEnded:withEvent:] + 481
frame #5: 0x00007fff480cebbf UIKitCore`-[UIWindow _sendTouchesForEvent:] + 2604
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
---------------------------------------------------------------------------------------------------
frame #6: 0x00007fff480d04c6 UIKitCore`-[UIWindow sendEvent:] + 4596
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: 0x000000010795741d demo-事件传递响应链`main(argc=1, argv=0x00007ffee82a8c90) at main.m:18:12
frame #18: 0x00007fff5227ec25 libdyld.dylib`start + 1
1.hitTest:withEvent:和pointInside
1.1 hitTest:withEvent:和pointInside 演练
-
测试hitTest和pointInside执行过程
GSGrayView *grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)]; [self.view addSubview:grayView]; GSRedView *redView = [[GSRedView alloc] initWithFrame:CGRectMake(0, 0, grayView.bounds.size.width / 2, grayView.bounds.size.height / 3)]; [grayView addSubview:redView]; GSBlueView *blueView = [[GSBlueView alloc] initWithFrame:CGRectMake(grayView.bounds.size.width/2, grayView.bounds.size.height * 2/3, grayView.bounds.size.width/2, grayView.bounds.size.height/3)]; // blueView.userInteractionEnabled = NO; // blueView.hidden = YES; // blueView.alpha = 0.1;//0.0; [grayView addSubview:blueView]; GSYellowView *yellowView = [[GSYellowView alloc] initWithFrame:CGRectMake(CGRectGetMinX(grayView.frame), CGRectGetMaxY(grayView.frame) + 20, grayView.bounds.size.width, 100)]; [self.view addSubview:yellowView];
hitTest测试
点击redView:
yellowView -> grayView -> blueView -> redView
image
-
当点击redView时,因为yellowView和grayView同级,yellowView比grayView后添加,所以先打印yellowView,由于触摸点不在yellowView中因此打印grayView,然后遍历grayView的subViews分别打印blueView和redView。
-
当hitTest返回nil时,也不会打印pointInside。因此可以得出pointInside是在hitTest后面执行的。
-
当view的userInteractionEnabled为NO、hidden为YES或alpha<=0.1时,也不会打印pointInside方法。因此可以推断出在hitTest方法内部会判断如果这些条件一个成立则会返回nil,也不会调用pointInside方法。
-
如果在grayView的hitTest返回[super hitTest:point event:event],则会执行gery.subviews的遍历(subviews 的 hitTest 与 pointInside),grayView的pointInside是判断触摸点是否在grayView的bounds内,grayView的hitTest是判断是否需要遍历他的subviews.
-
pointInside只是在执行hitTest时,会在hitTest内部调用的一个方法。也就是说pointInside是hitTest的辅助方法。
-
hitTest是一个递归函数
1.2 hitTest:withEvent:内部实现代码还原
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"-----%@",self.nextResponder.class);
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;
//判断点在不在这个视图里
if ([self pointInside:point withEvent:event]) {
//在这个视图 遍历该视图的子视图
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
//转换坐标到子视图
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
//递归调用hitTest:withEvent继续判断
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
//在这里打印self.class可以看到递归返回的顺序。
return hitTestView;
}
}
//这里就是该视图没有子视图了 点在该视图中,所以直接返回本身,上面的hitTestView就是这个。
NSLog(@"命中的view:%@",self.class);
return self;
}
//不在这个视图直接返回nil
return nil;
}
1.3 pointInside运用:增大热区范围
在开发过程中难免会遇到需要增大UIButton等的热区范围,假如UIButton的布局不允许修改,那么就需要用到pointInside来增大UIButton的点击热区范围。具体实现代码如下:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ -- pointInside",self.class);
CGRect bounds = self.bounds;
//若原热区小于200x200,则放大热区,否则保持原大小不变
//一般热区范围为40x40 ,此处200是为了便于检测
CGFloat widthDelta = MAX(200 - bounds.size.width, 0);
CGFloat heightDelta = MAX(200 - bounds.size.height, 0);
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
return CGRectContainsPoint(bounds, point);
}
也就是说如果button的size小于200*200,则点击button相对中心位置上下左右各100的范围内即使超出button,也可以响应点击事件
2.响应链
2.1 响应链的组成
![](https://img.haomeiwen.com/i6526232/fc27403cad7dcb28.png)
还用上面那个栗子:
点击redView:
redview -> grayView -> viewController -> ...
![](https://img.haomeiwen.com/i6526232/b021649375b59a44.png)
因为只实现到controller的touches事件方法因此只打印到Controller。
- 响应链是通过nextResponder属性组成的一个链表。
- 点击的view有 superView,nextResponder就是superView;
- view.nextResponder.nextResponder是viewController 或者是 view.superView. view
- view.nextResponder.nextResponder.nextResponder是 UIWindow (非严谨,便于理解)
- view.nextResponder.nextResponder.nextResponder. nextResponder是UIApplication、UIAppdelate、直到nil (非严谨,便于理解)
- touch事件就是根据响应链的关系来层层调用(我们重写touch 要记得 super 调用,不然响应链会中断)。
- 比如我们监听self.view的touch事件,也是因为subviews的touch都在同一个响应链里。
2.2 UIControl阻断响应链
把上面栗子中的grayView替换成一个Button:
GSExpandButton *expandButton = [[GSExpandButton alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)];
expandButton.backgroundColor = [UIColor lightGrayColor];
[expandButton setTitle:@"点我啊" forState:UIControlStateNormal];
[expandButton addTarget:self action:@selector(expandButtonClick:) forControlEvents:UIControlEventTouchDown];
[self.view addSubview:expandButton];
self.redView = [[GSRedView alloc] initWithFrame:CGRectMake(0, 0, expandButton.bounds.size.width / 2, expandButton.bounds.size.height / 3)];
[expandButton addSubview:self.redView];
self.blueView = [[GSBlueView alloc] initWithFrame:CGRectMake(expandButton.bounds.size.width/2, expandButton.bounds.size.height * 2/3, expandButton.bounds.size.width/2, expandButton.bounds.size.height/3)];
// blueView.userInteractionEnabled = NO;
// blueView.hidden = YES;
// blueView.alpha = 0.1;//0.0;
[expandButton addSubview:self.blueView];
self.yellowView = [[GSYellowView alloc] initWithFrame:CGRectMake(CGRectGetMinX(expandButton.frame), CGRectGetMaxY(expandButton.frame) + 20, expandButton.bounds.size.width, 100)];
[self.view addSubview:self.yellowView];
点击redView:
redview -> expandButton
![](https://img.haomeiwen.com/i6526232/f7cfc6b6621523b6.png)
- 虽然点击redView,虽然button的touches事件方法也走了但是依然不会响应button的target的action方法,只是会传递到button而已,因为最佳响应着依然是redView。
- 从上面测试结果可以看出,UIControl会阻断响应链的传递,也就是说在响应UIContol的touches事件时并不会调用nextResponder的对应的方法。
- 通过在Button子类中重写touches的方法,发现如果不调用super的touches对应的方法则不会响应点击事件。由此可以大致推断出UIControl其子类响应点击原理大致为:根据添加target:action:时设置的UIControlEvents,在touches的合适方法调用target的action方法。(即button能够响应方法,就不向下查找nextResponser了, 但是我们设置button的触发事件是 使用的 addTarget:action: forControlEvents: 没有达到controlEvents的触发条件, 所以不执行action)
网友评论