美文网首页
事件传递

事件传递

作者: KB_MORE | 来源:发表于2020-08-12 15:36 被阅读0次
图片.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 响应链的组成

respondChain

还用上面那个栗子:
点击redView:
redview -> grayView -> viewController -> ...


image

因为只实现到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


image
  • 虽然点击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)

相关文章

网友评论

      本文标题:事件传递

      本文链接:https://www.haomeiwen.com/subject/xchfdktx.html