iOS编程中的快递小哥-Responder Chain(响应链)

deliver.jpeg

今天我们来聊下iOS编程中常见点击事件从分发传递到响应的完整流程😎

1.事件类别

  • Touch events
    UIView上的常见点击事件
  • Press events
    AppleTV遥控器或者游戏控制器或其他带有实体物理键所触发的事件
  • Shake-motion events
    由加速计、陀螺仪、磁力仪触发的事件
  • Remote-control events
    额外配件如耳机上的音视频播放按键所触发的事件(视频播放、下一首)

今天我们只讲Touch events相关事件的传递响应

2.响应链工作原理

从你手指触到到屏幕中某一控件到其响应相关事件其实是分为两步:事件的传递事件的响应

事件的传递涉及到了UIView中的两个方法

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
//判断当前点击事件是否存在最优响应者(First Responder)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
//判断当前点击是否在控件的Bounds之内

事件的传递其实就是在事件产生与分发之后如何寻找最优响应视图的一个过程

2.1事件的传递流程

1.触碰屏幕产生事件UIEvent并存入UIApplication中的事件队列中, 并且在整个视图结构中自上而下的进行分发
2.UIWindow接受到事件开始进行最优响应视图查询的过程(逆序遍历subviews)
3.当到UIViewController这一层时同样对其根视图(self.view及其上subviews)开始最优响应视图查询。该查询会调用上述提及到两个于UIView的方法,之所以采用逆序查询也是为了优化查找速度,毕竟后addSubview的视图在上易于命中

事件分发与传递流程

Note:
如果在hitTest & pointInside过程中查询到最优响应视图则后续对于其他subviews遍历查询则会停止

2.1.1视图命中查找流程

1.调用hitTest方法进行最优响应视图查询

  • hidden = YES
  • userInteractionEnabled = NO
  • alpha < 0.01
    以上三种情况会使该方法返回nil,即当前视图下无最优响应视图

2.hitTest方法内部会调用pointInside方法对点击点进行是否在当前视图bounds内进行判断,如果超出boundshitTest则返回nil,未超出范围则进行步骤3

3.对当前视图下的subviews逆序采取上述1 2步骤以查询最优响应视图。如果hitTest返回了对应视图则说明在当前视图层级下有最优响应视图,可能为self或者其subview,这个要看具体返回。

下面是最优命响应图查询代码示例

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    if (self.alpha < 0.01 || !self.userInteractionEnabled || self.hidden) {
        
        return nil;
    }
    
    if (![self pointInside:point withEvent:event]) {
        
        return nil;
    }
    
    __block UIView *hitView = nil;
    [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
        
        CGPoint convertPoint = [self convertPoint:point toView:subview];
        hitView = [subview hitTest:convertPoint withEvent:event];
        if (hitView) {
            
            *stop = YES;
        }
    }];
    
    return hitView ? : self;
}

好了, 事件的分发与传递流程我们已经讲完了,那我们该如果进行相关的验证呢?首先我们要明确相关要确认的点:

  • UIApplication开始自上而下的进行事件分发
  • UIView内部开始反向遍历查找最优视图

UIApplication 开始自上而下的进行事件分发

这个我们可以打开Instrument中的TimeProfiler进行一个整体的函数调用查看

在使用Instrument之前记得为其配置相对应的dSYM文件,否则到时候TimeProfiler中看到的将是调用函数的16进制地址,这不便于我们对问题的定位

屏幕快照 2017-10-14 下午10.18.07.png

然后我们在ViewController中添加一个Button和对应按钮事件就可以开始运行TimeProfiler了(Command + i

buttonActionTimeProfiler

从图中我们可以看到分别一次调用了[UIApplication endEvent:][UIWindow sendEvent:]

这里可能会有同学注意到上面所提及到流程图中UIWindow是进行最优响应视图查询的,为什么TimeProfiler中显示了其调用了一次事件分发。这里让我们来看下Xcode文档中对于UIWindowsendEvent方法的注释

called by UIApplication to dispatch events to views inside the window

所以博主认为这里的调用是没问题的

UIView 内部开始反向遍历查找最优视图

首先我们可以利用Method Swizzling交换下我们需要监测的 hitTest方法

#import "UIView+WCQHitTest.h"
#import <objc/runtime.h>

@implementation UIView (WCQHitTest)

+ (void)load {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Class class = [self class];
        
        SEL oriSEL = @selector(hitTest:withEvent:);
        SEL swiSEL = @selector(wcq_hitTest:withEvent:);
        
        Method oriMethod = class_getInstanceMethod(class, oriSEL);
        Method swiMethod = class_getInstanceMethod(class, swiSEL);
        
        BOOL didAddMethod = class_addMethod(class, oriSEL,
                                            method_getImplementation(swiMethod),
                                            method_getTypeEncoding(swiMethod));
        
        if (didAddMethod) {
         
            class_replaceMethod(class,
                                swiSEL,
                                method_getImplementation(oriMethod),
                                method_getTypeEncoding(oriMethod));
        }else {
            
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });
}

- (UIView *)wcq_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    NSLog(@"%@ %s",[self class], __PRETTY_FUNCTION__);
    return [self wcq_hitTest:point withEvent:event];
}

然后我们分别新建三个UIView的子类: AViewBViewCView并依次按顺序添加到ViewController

F5B949E2-B466-4304-9299-BA646B981DB7.png

然后我们依次点击AB视图看下hitTes调用顺序是否和预期一致

点击AView.png
点击BView.png
bingo.jpeg

2.2事件的响应流程

这里引用下苹果官方文档中的一张图

屏幕快照 2017-10-15 下午3.21.02.png

响应链 其实是由一个个UIResponder的子类构成的,UIResponder是系统一个负责接受和处理事件的类。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

而以上这几个响应触碰的方法其实也是出自于UIResponder类,
UIView作为UIResponder的子类能够处理点击事件也就无可厚非了

现在讲讲事件的响应流程:

1.首先已确定最优响应视图
2.判断最优响应视图能否响应事件,如果视图能进行响应则事件在响应链中的传递终止。如果视图不能响应则将事件传递给 nextResponder也就是通常的superview进行事件响应
3.如果事件继续上报至UIWindow并且无法响应,它将会把事件继续上报给UIApplication
4.如果事件继续上报至UIApplication并且也无法响应,它将会将事件上报给其Delegate,但前提下这个Delegate不属于 响应链 并且是UIResponder的子类
5.如果最终事件依旧未被响应则会被系统抛弃

Note:
也并非所有的nextResponder即是superview,比如UIViewController的根视图self.viewnextResponder是其所在UIViewController。而如果UIViewController如果是UIWindow的根控制器,那么它的nextResponder就是UIWindow,但如果UIViewController是另外一个 UIViewController present出来的话,那么它的nextResponder就是之前所执行present操作的那个UIViewController

流程讲完了,还是那句话: 设法证实其关键节点

  • 事件响应自下而上进行上报

我们这次可以利用该方法进行验证:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

同时我们也看看该方法在文档中的描述,看能否查找到一些有用的信息节点
果然~

UIKit calls this method when a new touch is detected in a view or window. Many UIKit classes override this method and use it to handle the corresponding touch events. The default implementation of this method forwards the message up the responder chain. When creating your own subclasses, call super to forward any events that you do not handle yourself.

根据文档所述,该方法默认实现就是将事件沿 响应链 进行自下而上的上报。现在我们同样可以利用Method Swizzling再次对touchesBegan方法进行监测,这里有一个要注意的地方:由于这次置换的方法中调用到super方法,所以我们置换的时候置换的是UIView中的touchesBegan方法而没去置换UIResponder中的touchesBegan方法

- (void)wcq_touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    NSLog(@"%@ %s",[self class], __PRETTY_FUNCTION__);
    [super touchesBegan:touches withEvent:event];
}

同时这次我们为了验证事件的响应是自下而上,我们调整下UI的结构:

B1BB7DFA-42DA-4243-A3AD-CBB57DAF04DF.png

运行模拟器点击CView

点击CView.png

3.总结

  • 事件分发与传递:自上而下
  • 事件响应:自下而上

当然这仅仅只是 Touch eventResponder Chain 中的传递与响应流程。不同类型的 UIEvent 分发与响应原理还不一致。

4.最后

你的点赞与指正都是我继续创作的动力,感谢你长的那么帅(漂亮)还来看我的文章😊

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容