iOS基础补完计划--透过堆栈看事件响应机制

目录

  • 前言
  • TouchEvent 响应链
  • UIControl
  • UIGestureRecognizer
  • 结论

前言

众所周知、手势识别器的权限比响应链更高。
而UIControl的响应机制不涉及响应链、由UIAPPlication直接分发。

于是、会出现一些冲突的问题。
比如给self.view添加手势之后、cell不可点击、但UIButton不受影响。
可以看看《iOS点击事件和手势冲突》

而在网上找了找、但总觉得不那么直观。


TouchEvent 响应链

点击一个View

frame #20: 0x000000010ce0e4fd NSObject`-[View1 touchesBegan:withEvent:](self=0x00007f9c19e20a10, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x000060400010d410) at View1.m:107
frame #21: 0x000000010ec87e1a UIKit`-[UIWindow _sendTouchesForEvent:] + 2052
frame #22: 0x000000010ec897c1 UIKit`-[UIWindow sendEvent:] + 4086
frame #23: 0x000000010ec2d310 UIKit`-[UIApplication sendEvent:] + 352
  • 结论
  1. 响应链由UIWindow_sendTouchesForEvent方法调起。
  2. _sendTouchesForEventsendEvent:方法调起。

UIControl

点击一个UIButton

  • UIButton响应线
frame #0: 0x000000010ce0d2a7 NSObject`-[ViewController btnClick](self=0x00007f9c19f0f080, _cmd="btnClick") at ViewController.m:55
frame #1: 0x000000010ec133e8 UIKit`-[UIApplication sendAction:to:from:forEvent:] + 83
frame #2: 0x000000010ed8e7a4 UIKit`-[UIControl sendAction:to:forEvent:] + 67
frame #3: 0x000000010ed8eac1 UIKit`-[UIControl _sendActionsForEvents:withEvent:] + 450
frame #4: 0x000000010ed8da09 UIKit`-[UIControl touchesEnded:withEvent:] + 580
frame #5: 0x000000010ec880bf UIKit`-[UIWindow _sendTouchesForEvent:] + 2729
frame #6: 0x000000010ec897c1 UIKit`-[UIWindow sendEvent:] + 4086
frame #7: 0x000000010ec2d310 UIKit`-[UIApplication sendEvent:] + 352
  • 响应链开始线

frame #0: 0x000000010c9db48b NSObject`-[View1 touchesBegan:withEvent:](self=0x00007f8a15405cc0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000001044a0) at View1.m:109
frame #1: 0x000000010e854e1a UIKit`-[UIWindow _sendTouchesForEvent:] + 2052
frame #2: 0x000000010e8567c1 UIKit`-[UIWindow sendEvent:] + 4086
frame #3: 0x000000010e7fa310 UIKit`-[UIApplication sendEvent:] + 352
  • 响应链结束线
frame #0: 0x000000010c9db60b NSObject`-[View1 touchesEnded:withEvent:](self=0x00007f8a15405cc0, _cmd="touchesEnded:withEvent:", touches=1 element, event=0x00006000001044a0) at View1.m:120
frame #1: 0x000000010e8550bf UIKit`-[UIWindow _sendTouchesForEvent:] + 2729
frame #2: 0x000000010e8567c1 UIKit`-[UIWindow sendEvent:] + 4086
frame #3: 0x000000010e7fa310 UIKit`-[UIApplication sendEvent:] + 352
  • 结论
  1. 会由UIWindow_sendTouchesForEvent:调起响应链。
  2. touchesBegan:withEvent判断如果为UIControl、则截断响应链(不向next reponder传递)、并且尝试识别。
  3. 识别成功则直接由UIApplicationtarget发送Action事件。

UIGestureRecognizer

点击一个添加了手势的View或者SubView

  • 手势响应线
frame #0: 0x00000001034ea447 NSObject`-[ViewController doTapChange](self=0x00007fbb31607910, _cmd="doTapChange") at ViewController.m:101
frame #1: 0x00000001058e754f UIKit`-[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 57
frame #2: 0x00000001058f0324 UIKit`_UIGestureRecognizerSendTargetActions + 109
frame #3: 0x00000001058edb6c UIKit`_UIGestureRecognizerSendActions + 307
frame #4: 0x00000001058ecdc0 UIKit`-[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 859
frame #5: 0x00000001058d1e24 UIKit`_UIGestureEnvironmentUpdate + 1329
frame #6: 0x00000001058d18a7 UIKit`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 484
frame #7: 0x00000001058d09a9 UIKit`-[UIGestureEnvironment _updateGesturesForEvent:window:] + 281
frame #8: 0x00000001053667ab UIKit`-[UIWindow sendEvent:] + 4064
frame #9: 0x000000010530a310 UIKit`-[UIApplication sendEvent:] + 352
  • 响应链开始线
[ViewController touchesBegan:withEvent:](self=0x00007f8a15426010, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000001044a0) at ViewController.m:97
frame #1: 0x000000010ea0c767 UIKit`forwardTouchMethod + 340
frame #2: 0x000000010ea0c602 UIKit`-[UIResponder touchesBegan:withEvent:] + 49
frame #3: 0x000000010e854e1a UIKit`-[UIWindow _sendTouchesForEvent:] + 2052
frame #4: 0x000000010e8567c1 UIKit`-[UIWindow sendEvent:] + 4086
frame #5: 0x000000010e7fa310 UIKit`-[UIApplication sendEvent:] + 352
  • 响应链取消线
frame #0: 0x00000001034eb5ed NSObject`-[View1 touchesCancelled:withEvent:](self=0x00007fbb314061e0, _cmd="touchesCancelled:withEvent:", touches=0x000060400025a1f0, event=0x000060000011d0a0) at View1.m:120
frame #1: 0x0000000105303434 UIKit`__98-[UIApplication _cancelViewProcessingOfTouches:withEvent:sendingTouchesCancelledToViewsOfTouches:]_block_invoke + 663
frame #2: 0x0000000105302c22 UIKit`-[UIApplication _cancelTouches:withEvent:includingGestures:notificationBlock:] + 1091
frame #3: 0x0000000105303167 UIKit`-[UIApplication _cancelViewProcessingOfTouches:withEvent:sendingTouchesCancelledToViewsOfTouches:] + 158
frame #4: 0x00000001058d41e8 UIKit`_UIGestureEnvironmentCancelTouches + 687
frame #5: 0x00000001058d3f26 UIKit`-[UIGestureEnvironment _cancelTouches:event:] + 42
frame #6: 0x00000001058ed2d0 UIKit`-[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 2155
frame #7: 0x00000001058d1e24 UIKit`_UIGestureEnvironmentUpdate + 1329
frame #8: 0x00000001058d18a7 UIKit`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 484
frame #9: 0x00000001058d09a9 UIKit`-[UIGestureEnvironment _updateGesturesForEvent:window:] + 281
frame #10: 0x00000001053667ab UIKit`-[UIWindow sendEvent:] + 4064
frame #11: 0x000000010530a310 UIKit`-[UIApplication sendEvent:] + 352
  • [KTUITapGestureRecognizer touchesBegan:withEvent:]线:
-[KTUITapGestureRecognizer touchesBegan:withEvent:](self=0x00006000003d6700, _cmd="touchesBegan:withEvent:", 0x0000600000dd1d40) ITapGestureRecognizer.m:14
-[UIGestureRecognizer _touchesBegan:withEvent:] + 240
-[UITouchesEvent _sendEventToGestureRecognizer:] + 287
-[UIGestureEnvironment _updateForEvent:window:]_block_invoke + 64
-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 276
-[UIGestureEnvironment _updateForEvent:window:] + 200
-[UIWindow sendEvent:] + 4058
-[UIApplication sendEvent:] + 352
  • 然后来科普一下手势的识别过程

Window在将事件传递给hit-tested view之前,会先将事件传递给相关的手势识别器并由手势识别器优先识别。若手势识别器成功识别了事件,就会取消hit-tested view对事件的响应;若手势识别器没能识别事件,hit-tested view才完全接手事件的响应权。

关于谁先谁后。佐证的话、你可以自定义一个子类并且重载一些方法、这里直接贴结果:

大致理解是,Window在将事件传递给hit-tested view之前,会先将事件传递给相关的手势识别器并由手势识别器优先识别。若手势识别器成功识别了事件,就会取消hit-tested view对事件的响应;若手势识别器没能识别事件,hit-tested view才完全接手事件的响应权。
佐证的话、你可以自定义一个子类并且重载一些方法、这里直接贴结果
先用一个离散型手势做实验:

14:20:17-[KTUITapGestureRecognizer touchesBegan:withEvent:]
14:20:17-[KTUITapGestureRecognizer2 touchesBegan:withEvent:]
14:20:17-[View2 touchesBegan:withEvent:]
14:20:17-[View touchesBegan:withEvent:]
14:20:18-[KTUITapGestureRecognizer touchesEnded:withEvent:]
14:20:18-[KTUITapGestureRecognizer2 touchesEnded:withEvent:]
14:20:18-[View2 tapAction]
14:20:18-[View2 touchesCancelled:withEvent:]
14:20:18-[View touchesCancelled:withEvent:]
而对于持续型手势

在一开始滑动的过程中,手势识别器处在识别手势阶段,滑动产生的连续事件既会传递给手势识别器又会传递给View,因此ViewtouchesMoved:withEvent:在开始一段时间内会持续调用;
当手势识别器成功识别了该滑动手势时,手势识别器的action开始调用,同时通知Application取消View对事件的响应。之后仅由滑动手势识别器接收事件并响应,View不再接收事件。

这里有几个点可以说说:

  1. 可以看到右侧有一秒的时间差
    也就是说View的后续动作会等待GestureRecognizer的识别结果。
  2. KTUITapGestureRecognizer以及KTUITapGestureRecognizer2的方法都被触发了
    也就是说是hit-tested列表中所有View上的手势识别器都会得到机会去识别事件。(依赖UITouch中的gestureRecognizers属性)
    至于最后触发谁、取决于代理中的设置。默认按照响应链的顺序。

并且手势在触摸事件处理流程中,处于观察者的角色,其不是view层级结构的一部分,所以也不参与或者依赖responder chain(你把上层View的touch不调用super也影响不了)。

其流程大概如下图所示:

注:图中view与手势的关系是,手势关联在view或view的superview(可能多级)上。

  • 结论
  1. 手势的ActionUIGestureRecognizer直接调用。
  2. 响应链的开始依旧由UIWindow_sendTouchesForEvent触发。
  3. 响应链的cancelUIGestureEnvironment发起
  4. 手势的识别(KTUITapGestureRecognizertouchesBeagn)并不依赖响应链(ViewtouchesBeagn)、而是由[UIWindow sendEvent:]方法中被UIGestureEnvironment直接调起。

结论

有几个地方可以提一下:

  1. 响应链本身什么都不做
    所以手势以及UIControl的处理本身并不依赖响应链。以下是勘误:
    对于UIView是这样。但当我看过UIControl后有了新的理解~
    但对于UIControl。如果注册了Action、那么UIContrl便会在自己的位置上截断响应链、并且在TouchBegan等方法内部处理TouchDown等等状态以及对应的Action
    此外需要注意的是:只要注册了Action、就会截断响应链(但并不一定会触发- sendAction:to:forEvent:)。哪怕是向一个UIButton注册EditingDidEnd
    由于UIControl依赖touchBegan并且具有截断的特性、所以位于下方的另一个UIButton(如果你没有搞什么特殊的事情)才永远不会被触发。
  1. 响应链必然会触发、只是后期处理不一样。
    如果是手势、则被取消。如果是UIControl则被结束。
    如果设置tap.delaysTouchesBegan = YES;在识别结果出来之前则不会触发View的touchesBegan
  • 关于事件处理的优先级。

这个光看堆栈应该是看不出来的。
但结合一些结论、可以试着猜测:

  1. 目标View上有手势
    手势优先识别
  2. 目标View上没有手势但是归属系统UIControl
    UIControl优先处理
  3. 目标View就是个普通View(或者自定义的Control)
    下层手势优先识别、如果没有手势/识别失败则交给响应链
  • 如何将事件发送给指定的View/手势

全部依赖UITouch的属性

  1. Window
@property(nullable,nonatomic,readonly,strong) UIWindow *window;
  1. 响应链/UIControl
@property(nullable,nonatomic,readonly,strong) UIView *view;
  1. 手势
@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);

最后

本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果愿意补充以及不吝赐教小弟会更加感激。

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