iOS 响应者链

iOS 响应者链,第1张

1.源起

最近在面试,好基友池子跑过来对我说:响应者链这是个必考点,一般会这么问:响应者事件传递顺序是什么, 响应者的响应顺序是什么?

池子认为事件传递的过程是自上而下的,事件响应是自下而上而上的。为此和池子争论了一番。争议点在事件传递上,就此达成一致的是响应者链的顺序是自上而下的。Jeverson认为响应者链寻找最合适的(第一响应者)响应者调用HitTest的过程–事件响应,找到第一响应者发现没有相应的处理函数,向上传递事件的过程–事件传递 池子则认为相反。

1.1RunLoop 是如何响应事件的?

Apple注册了一个 Source1(基于mach_port 的)用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallBack().

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由IOKit.framework生成一个IOHIDEvent 事件并由SrpingBoard 接收

SpringBoard 只接收按键(锁屏/静音)、触摸,加速,接近传感器等几种Event,随后用mach_port 转发给需要的 App进程。随后苹果注册的那个 Source1就会触发触发回调,并调用_UIApplicationHandleEventQueue() 进行应用的内部分发。

_UIApplicationHandleEventQueue() 会把IOHIDEvent 处理并包装成UIEvent进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow等。通常事件比如UIButton点击 、touchesBegin/move/end/cancel 事件都是在这个回调中完成的。

1.2 说明

Jeverson在面试中遇到过两位面试官,两个人对事件传递就如何池子Jevreson一样。而本人呢却刚好巧妙的避开两位面试官的理解。故本文就抛开歧义,全面理解HitTest,响应者链。并参照池子对两争议名词的理解开展。

2.响应者(Responder)

上面讲述到_UIApplicationHandleEventQueue()UIEvent进行处理或分发给window,接着是如何找出最佳响应者(responder), 这里就要引入UIview 的两个方法:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
方法一:返回视图层级结构,并显示第一响应者
方法二:返回点击是否在该视图中 2.1 响应者一瞥

上面讲到过,UIView的两个重要方法;那我们来定义一个嵌套涂层来一探这两个方法的调用过程。我们的示例参照stackoverflow

+----------------------------+
|A                           |
|+--------+   +------------+ |
||B       |   |C           | |
||        |   |+----------+| |
|+--------+   ||D         || |
|             |+----------+| |
|             +------------+ |
+----------------------------+

手指在D上点击究竟发生了什么,hitTest:withEvent:, pointInside:withEvent:发生了什么,让我们来一探究竟究竟。

2.2 通过Category交换方法的方式一探究竟
+ (void)load {
    Method originHitTest = class_getInstanceMethod([UIView class], @selector(hitTest:withEvent:));
    Method customHitTest = class_getInstanceMethod([UIView class], @selector(jj_hitTest:withEvent:));
    method_exchangeImplementations(originHitTest, customHitTest);
    
    Method originPointInside = class_getInstanceMethod([UIView class], @selector(pointInside:withEvent:));
    Method customPointInside = class_getInstanceMethod([UIView class], @selector(jj_pointInside:withEvent:));
    method_exchangeImplementations(originPointInside, customPointInside);
}

- (UIView *)jj_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"%@ hitTest", NSStringFromClass([self class]));
    UIView *view = [self jj_hitTest:point withEvent:event];
    NSLog(@"%@ hitTest return %@", NSStringFromClass([self class]), NSStringFromClass([view class]));
    return view;
}

- (BOOL)jj_pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"%@ pointInside", NSStringFromClass([self class]));
    BOOL result = [self jj_pointInside:point withEvent:event];
    NSLog(@"%@ pointInside return %@", NSStringFromClass([self class]), (result?@"true":@"false"));
    return result;
}
2.3 log
2022-02-25 17:13:12.187599+0800 ResponserChain[16154:291200] UIWindow hitTest
2022-02-25 17:13:12.187789+0800 ResponserChain[16154:291200] UIWindow pointInside
2022-02-25 17:13:12.187892+0800 ResponserChain[16154:291200] UIWindow pointInside return true
2022-02-25 17:13:12.187998+0800 ResponserChain[16154:291200] UITransitionView hitTest
2022-02-25 17:13:12.188105+0800 ResponserChain[16154:291200] UITransitionView pointInside
2022-02-25 17:13:12.188200+0800 ResponserChain[16154:291200] UITransitionView pointInside return true
2022-02-25 17:13:12.188290+0800 ResponserChain[16154:291200] UIDropShadowView hitTest
2022-02-25 17:13:12.188399+0800 ResponserChain[16154:291200] UIDropShadowView pointInside
2022-02-25 17:13:12.188712+0800 ResponserChain[16154:291200] UIDropShadowView pointInside return true
2022-02-25 17:13:12.189025+0800 ResponserChain[16154:291200] AView hitTest
2022-02-25 17:13:12.189276+0800 ResponserChain[16154:291200] AView pointInside
2022-02-25 17:13:12.189609+0800 ResponserChain[16154:291200] AView pointInside return true
2022-02-25 17:13:12.189910+0800 ResponserChain[16154:291200] CView hitTest
2022-02-25 17:13:12.190180+0800 ResponserChain[16154:291200] CView pointInside
2022-02-25 17:13:12.190461+0800 ResponserChain[16154:291200] CView pointInside return true
2022-02-25 17:13:12.190727+0800 ResponserChain[16154:291200] DView hitTest
2022-02-25 17:13:12.190984+0800 ResponserChain[16154:291200] DView pointInside
2022-02-25 17:13:12.191231+0800 ResponserChain[16154:291200] DView pointInside return true
2022-02-25 17:13:12.191497+0800 ResponserChain[16154:291200] UILabel hitTest
2022-02-25 17:13:12.191759+0800 ResponserChain[16154:291200] UILabel hitTest return (null)
2022-02-25 17:13:12.192088+0800 ResponserChain[16154:291200] DView hitTest return DView
2022-02-25 17:13:12.193328+0800 ResponserChain[16154:291200] CView hitTest return DView
2022-02-25 17:13:12.193609+0800 ResponserChain[16154:291200] AView hitTest return DView
2022-02-25 17:13:12.193836+0800 ResponserChain[16154:291200] UIDropShadowView hitTest return DView
2022-02-25 17:13:12.194166+0800 ResponserChain[16154:291200] UITransitionView hitTest return DView
2022-02-25 17:13:12.194396+0800 ResponserChain[16154:291200] UIWindow hitTest return DView
2022-02-25 17:13:12.194743+0800 ResponserChain[16154:291200] UIWindow hitTest
2022-02-25 17:13:12.195022+0800 ResponserChain[16154:291200] UIWindow pointInside
2022-02-25 17:13:12.195276+0800 ResponserChain[16154:291200] UIWindow pointInside return true
2022-02-25 17:13:12.195509+0800 ResponserChain[16154:291200] UITransitionView hitTest
2022-02-25 17:13:12.195889+0800 ResponserChain[16154:291200] UITransitionView pointInside
2022-02-25 17:13:12.196137+0800 ResponserChain[16154:291200] UITransitionView pointInside return true
2022-02-25 17:13:12.201222+0800 ResponserChain[16154:291200] UIDropShadowView hitTest
2022-02-25 17:13:12.201357+0800 ResponserChain[16154:291200] UIDropShadowView pointInside
2022-02-25 17:13:12.201451+0800 ResponserChain[16154:291200] UIDropShadowView pointInside return true
2022-02-25 17:13:12.201550+0800 ResponserChain[16154:291200] AView hitTest
2022-02-25 17:13:12.201648+0800 ResponserChain[16154:291200] AView pointInside
2022-02-25 17:13:12.201740+0800 ResponserChain[16154:291200] AView pointInside return true
2022-02-25 17:13:12.201830+0800 ResponserChain[16154:291200] CView hitTest
2022-02-25 17:13:12.202077+0800 ResponserChain[16154:291200] CView pointInside
2022-02-25 17:13:12.202335+0800 ResponserChain[16154:291200] CView pointInside return true
2022-02-25 17:13:12.202581+0800 ResponserChain[16154:291200] DView hitTest
2022-02-25 17:13:12.202853+0800 ResponserChain[16154:291200] DView pointInside
2022-02-25 17:13:12.203083+0800 ResponserChain[16154:291200] DView pointInside return true
2022-02-25 17:13:12.203344+0800 ResponserChain[16154:291200] UILabel hitTest
2022-02-25 17:13:12.203607+0800 ResponserChain[16154:291200] UILabel hitTest return (null)
2022-02-25 17:13:12.203868+0800 ResponserChain[16154:291200] DView hitTest return DView
2022-02-25 17:13:12.204110+0800 ResponserChain[16154:291200] CView hitTest return DView
2022-02-25 17:13:12.204367+0800 ResponserChain[16154:291200] AView hitTest return DView
2022-02-25 17:13:12.204641+0800 ResponserChain[16154:291200] UIDropShadowView hitTest return DView
2022-02-25 17:13:12.204916+0800 ResponserChain[16154:291200] UITransitionView hitTest return DView
2022-02-25 17:13:12.205192+0800 ResponserChain[16154:291200] UIWindow hitTest return DView

日志中不难看出hitTest:withEventpointInside:withEvent的调用是从window开始的;值得注意的是,A执行完成之后的调用,并没有调用与C同级的B的方法,主要是因为子视图的调用顺序是从后向前的 而C是在B之后并且调用C之后返回的是true故这里并没有再去遍历B。这里引用Apple的解释hitTest:withEvent 返回包含指定点的视图层次结构(包括其自身)中接收器的最远后代。然后找到最佳响应者DView,值得注意的是此处的Label 的userInteraction没有打开。因为Label是View的子试图,所以还有一次递归调用,返回是Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.

2.4总结

1、最佳响应者寻找的方式是通过hitTest:withEventpointInside 来完成的
2、hitTest的调用是从window开始的,子视图的调用方式是从后向前
3、递归调用找到最合适的响应者,并返回包含指定点的视图层次结构(包括其自身)中接收器的最远后代

3.处理者

找到了最佳响应者,那么手势识别的过程是怎样的呢?

3.1 手势识别

当上面的_UIApplicationHandleEventQueue()识别了一个手势,其首先会调用cancel 将当前的touchesBegin/move/end 系列回调打断。随后系统将对应的UIGestureRecoginizer 标记为待处理

苹果注册了一个observer 监测 beforeWaiting(Loop即将进入休眠)事件,这个observer的回调函数是_UIGestureRecognizerUpdateObserver(),其内部会获取刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。当有UIGestureRecognizer的变化(创建/销毁/改变状态)是,这个回调都会进行相应的处理。

3.2 事件传递

我们在ViewDtouch 事件,ViewD作为最佳响应者,理应处理事件。但是我们这边并没有处理(重写touchesBegin/move/end 的方法)。那么事件将会被传递,那么D的下一个响应者是谁,谁来处理,事件在传递的过程中若响应者链中没有处理那么终将被废弃掉。
那么我们来一探事件传递的过程。

3.3 D 的响应者链是什么 3.3.1代码探究nextResponder
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (2 * NSEC_PER_SEC));
    dispatch_after(time, dispatch_get_main_queue(), ^{
        UIResponder *responder = self.dView.nextResponder;
        NSMutableString *pre = [NSMutableString stringWithString:@"--"];
        NSLog(@"%@", NSStringFromClass([self.dView class]));
        while (responder) {
            NSLog(@"%@%@", pre, NSStringFromClass([responder class]));
            [pre appendString:@"--"];
            responder =  responder.nextResponder;
        }
    });
3.3.2 logs
2022-02-27 10:00:54.761528+0800 ResponserChain[4933:159322] DView
2022-02-27 10:00:54.761673+0800 ResponserChain[4933:159322] --CView
2022-02-27 10:00:54.761815+0800 ResponserChain[4933:159322] ----AView
2022-02-27 10:00:54.761925+0800 ResponserChain[4933:159322] ------ViewController
2022-02-27 10:00:54.762091+0800 ResponserChain[4933:159322] --------UIDropShadowView
2022-02-27 10:00:54.762209+0800 ResponserChain[4933:159322] ----------UITransitionView
2022-02-27 10:00:54.762328+0800 ResponserChain[4933:159322] ------------UIWindow
2022-02-27 10:00:54.762424+0800 ResponserChain[4933:159322] --------------UIWindowScene
2022-02-27 10:00:54.762700+0800 ResponserChain[4933:159322] ----------------UIApplication
2022-02-27 10:00:54.763049+0800 ResponserChain[4933:159322] ------------------AppDelegate
3.3.3 注意点 我们在3.3.1查找nextResnponder时,将方法延时执行的,若直接在ViewControllerviewDidLoad中,nextResponder将在ViewController 中结束。
2022-02-27 10:10:45.560602+0800 ResponserChain[5275:168169] DView
2022-02-27 10:10:45.560739+0800 ResponserChain[5275:168169] --CView
2022-02-27 10:10:45.560852+0800 ResponserChain[5275:168169] ----AView
2022-02-27 10:10:45.560952+0800 ResponserChain[5275:168169] ------ViewController

从这里推理出我们响应者树的构造过程是在ViewDidLoad周期中来完成的,这个函数会将当前实例的构成的响应者子树合并到我们整个根树中

根据logs我们得出,ResponderChain自上而下的即AppDelegate->UIApplication->UIWindowSece->UIWindow->ViewController->AView->CView->DView.事件处理的顺序将是自下而上而上,由ResponderChain的逆向传递去处理的。因为想看到事件传递的全过程,我们这边并没有去处理touch事件,下面我们将处理事件来验证事件处理的过程。

3.3.4 事件处理

我们在AView上来处理touch/(begain/cancel/move/end)事件,overwrite前面的方法。

@implementation AView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%@ touchBegin", NSStringFromClass([self  class]));
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%@ toucheModed", NSStringFromClass([self class]));
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%@  touchEnded", NSStringFromClass([self class]));
    [super touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%@ touchCanclled", NSStringFromClass([self class]));
    [super touchesCancelled:touches withEvent:event];
}

@end

我们得到下面的log

2022-02-27 10:42:08.346133+0800 ResponserChain[6196:190756] UIWindow hitTest return DView
2022-02-27 10:42:08.347626+0800 ResponserChain[6196:190756] AView touchBegin
2022-02-27 10:42:08.439185+0800 ResponserChain[6196:190756] AView  touchEnded

若我们在DView中添加手势,并且不去实现touch的方法,那又会发生什么呢

- (void)awakeFromNib {
    [super awakeFromNib];
    [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)]];
}

- (void)tapAction:(UITapGestureRecognizer *)tapReg {
    NSLog(@"%@ DView taped", tapReg.self);
}
2022-02-27 11:06:06.307171+0800 ResponserChain[6932:210072] UIWindow hitTest return DView
2022-02-27 11:06:06.308727+0800 ResponserChain[6932:210072] AView touchBegin
2022-02-27 11:06:06.377668+0800 ResponserChain[6932:210072] ; target= <(action=tapAction:, target=)>> DView taped
2022-02-27 11:06:06.377883+0800 ResponserChain[6932:210072] AView touchCanclled
3.3.5 总结

我们得到下面结论
1、响应者链找出最佳响应者firstResopnder->DView
2、DView顺着响应者链,找到合适的处理者AView(实现了touch)方法。 (DView有视图,会根据nextResponder 寻找到CView,发现C没有处理,C会根据nextResponder找到A,结果发现A处理了那那么事件传递到此结束)
3、若本例中的A没有处理,会向上找到ViewController,若还是找不到会沿着响应者链一直向上传递,知道AppDelegate也不处理。那么这个事件将被废弃掉。

4.应用场景 4.1 无法响应

无法响应即不调用hitTest的状况,主要有以下几点

子视图超越父视图视图不透明度小于0.01userInteraction 为false视图被隐藏 4.2 自定义tabbar的button超过tabbar 的视图,扩大button的点击范围 4.3 圆形背景图button缩小点击范围 4.4 点击穿透

篇幅有限,关于案例中问题;可以自行百度。考查的知识点也可以从上述的篇幅中找到答案。

5.参考文献

iOS响应者链彻底掌握
Apple_Event Handling Guide for UIKit Apps
Responder一点也不神秘————iOS用户响应者链完全剖析

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/996906.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存