[TOC]程序员
2020-03-19windows
一般 iOS 界面开发中处理各类用户交互事件。其中,UIControlEvent
以注册的 Target-Action 的方式绑定到控件;UIGestureRecognizer
经过addGestureRecognizer:
添加到UIView
的gestureRecognizers
属性中;UIResponder
提供了touchesBegin/Moved/Ended/Canceled/:withEvent:
、motionsXXX:withEvent:
、pressXX:withEvent:
系列接口,将用户设备的触摸、运动、按压事件通知到UIResponder
对象等等。以上都是经常使用开发者处理用户交互事件的方式,那么隐藏在这些接口之下,从驱动层封装交互事件对象到 UI 控件接收到用户事件的流程是怎样的呢?本文主要探讨的就是这个问题。app
Apple Documentation 官方文档Using Responders and the Responder Chain to Handle Events介绍了利用UIResponder
的响应链来处理用户事件。UIResponder
实现了touchesXXX
、pressXXX
、motionXXX
分别用于响应用户的触摸、按压、运动(例如UIEventSubtypeMotionShake
)交互事件。UIResponder
包含nextResponder
属性。UIView
、UIWindow
、UIController
、UIApplication
都是UIResponder
的派生类,因此都能响应以上事件。ide
响应链结构以下图所示,基本上是经过UIResponder
的nextResponder
成员串联而成,基本上是按照 view 的层级,从前向后由子视图向父视图传递,且另外附加其余规则。总的响应链的规则以下:函数
nextResponder
是其父视图;nextResponder
是 Controller;nextResponder
是 present Controller 的控制器;nextResponder
是 Window;nextResponder
是 Application;nextResponder
是 App Delegate(仅当 App Delegate 为UIResponder
类型);UIResponder
响应touchesXXX
、pressXXX
、motionXXX
事件不须要指定userInteractionEnabled
为YES
。可是对于UIView
则须要指定userInteractionEnabled
,缘由是UIView
从新实现了这些方法。响应UIGesture
则须要指定userInteractionEnabled
,addGestureRecognizer:
是UIView
类的接口。工具
注意:新版本中,分离了 Window 和 View 的响应链。当 Controller 为根控制器时,
nextResponder
其实是nil
;Windows 的nextResponder
是 Window Scene;Window Scene 的nextResponder
是 Application。在后面的调试过程会有体现。oop
使用一个简单的 Demo 调试nextResponder
。界面以下图所示,包含三个 Label,从颜色能够判断其层次从后往前的顺序是:A >> B >> C。下面两个按钮另作他用,先忽略。ui
运行 Demo,查看各个元素的nextResponder
,确实如前面所述。spa
UIControl
控件与关联的 target 对象通讯,直接经过向 target 对象发送 action 消息。虽然 Action 消息虽然不是事件,可是 Action 消息的传递是要通过响应链的。当接收到用户交互事件的控件的 target 为nil
时,会沿着控件的响应链向下搜索,直到找到实现该 action 方法的对象为止。UIKit 的编辑菜单就是经过这个机制实现的,UIKit 会沿着控件的响应链搜索实现了cut:
、copy:
、paste:
等方法的对象。3d
当UIControl
控件调用addTarget:action:forControlEvents:
方法注册事件时,会将构建UIControlTargetAction
对象并将其添加到UIControl
控件的(NSMutableArray*)_targetActions
私有成员中,addTarget:action:forControlEvents:
方法的 Apple Documentation 注释中有声明调用该方法时UIControl
并不会持有 target 对象,所以无需考虑循环引用的问题。UIControl Events 注册过程的简单调试过程以下:
附注:The control does not retain the object in the target parameter. It is your responsibility to maintain a strong reference to the target object while it is attached to a control.
前面内容提到,控件的 action 是沿着响应链传递的,那么,当两个控件在界面上存在重合的区域,那么在重合区域触发用户事件时,action 消息会在哪一个控件上产生呢?在 1.1.1 中的两个重合的按钮就是为了验证这个问题。
稍微改造一下 1.1.1 的 Demo 程序,将 Label A、B、C 指定为自定义的继承自UILabel
的类型TestEventsLabel
,将两个 Button 指定为继承自UIButton
的TestEventsButton
类型。而后在TestEventsLabel
、TestEventsButton
、ViewController
中,为touchesXXX:
系列方法、nextResponder
方法、hitTest:withEvent:
方法添加打印日志的代码,以TestEventsButton
的实现为例(固然也能够用 AOP 实现):
@implementation TestEventsButton
-(UIResponder *)nextResponder{
UIResponder* responder = [super nextResponder];
NSLog(@"Next Responder Button %@ - return responder: %@", [self titleForState:UIControlStateNormal], responder);
return responder;
}
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView* view = [super hitTest:point withEvent:event];
NSLog(@"Hit Test Button %@ - return view: %@", [self titleForState:UIControlStateNormal], view);
return view;
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
NSLog(@"Button %@ - %s", [self titleForState:UIControlStateNormal], __func__);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesEnded:touches withEvent:event];
NSLog(@"Button %@ - %s", [self titleForState:UIControlStateNormal], __func__);
}
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesMoved:touches withEvent:event];
NSLog(@"Button %@ - %s", [self titleForState:UIControlStateNormal], __func__);
}
-(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesCancelled:touches withEvent:event];
NSLog(@"Button %@ - %s", [self titleForState:UIControlStateNormal], __func__);
}
@end
复制代码
一切准备就绪,运行 Demo,点击“点我前Button”,抓取到了以下日志。注意框①中指定的 target 是self
,也就是 Controller。能够发现点击事件产生,调用了若干次碰撞检测(框②),若干次nextResponder
(框③),最终只调用了 Controller 中“点我前Button”的 action 方法。这是由于:
接下来将addTarget:action:
中指定的 target 设为nil
。而后在TestEventsButton
中也添加 action 的响应代码,以下所示。
-(void)didClickBtnFront:(id)sender{
NSLog(@"In Button 点我前Button Did Click Action %s", __func__);
}
-(void)didClickBtnBack:(id)sender{
NSLog(@"In Button 点我后Button Did Click Action %s", __func__);
}
复制代码
点击“点我前Button”,抓取到了以下日志。此次,由TestEventsButton
处理了 action 消息。说明当控件注册 action 时指定的 target 为nil
时,action 消息仍然能够被响应,且 action 只响应一次。请记住,此时nextResponder
被调用了 5 次。
再进一步修改代码,将结论二中TestEventsButton
的新增代码删除,仍然将addTarget:action:
中指定的 target 设为nil
。点击“点我前Button”,抓取到了以下日志。此次,处理 action 消息的是 Controller。并且从日志中咱们发现,此次nextResponder
调用了 6 次,确切地说,是在 Button touchBegin
以后,Controller 处理 action 消息以前(如图中红框所示)。这是由于,target 为nil
时,action 消息会沿着响应链传递,直到找到能够响应 action 的对象为止。
能够继续尝试给“点我后Button”,直接将self.btnFront
的注册 Target-Action 的代码删掉。运行 Demo,再次点击“点我前Button”,此时didClickBtnBack
仍然不触发。这其实只是进一步印证了“结论一”的结论,这里再也不演示。
整个调试过程下来,能够发现,被 ButtonA 覆盖的 ButtonB,全部 action 都会被 ButtonA 拦截,被覆盖的 ButtonB 不会得到任何触发 action 的机会。
Gesture Recognizer 会在 View 以前接收 Touch 和 Press 事件,当 Gesture Recognizer 对一连串的 Touch 事件手势识别失败时,UIKit 才将这些 Touch 事件发送给 View。若 View 不处理这些 Touch 事件,UIKit 将其递交到响应链。
响应链主要经过nextResponder
方法串联,所以从新实现UIResponder
派生类的nextResponder
方法能够实现响应链修改的效果。
当 touch 事件发生时,UIKit 会构建一个与 view 关联的UITouch
实例,当 touch 位置变化时,仅改变 touch 的属性值,但不包括其view
属性。即便 touch 移出了 view 的范围,view
属性仍然是不变的。UITouch
的gestureRecognizers
属性表示正在处理该 touch 事件的全部 gesture recognizer。UITouch
的timestamp
属性表示 touch 事件的发生时间或者上一次修改的时间。UITouch
的phase
属性,表示 touch 事件当前所在的生命周期阶段,包括UITouchPhaseMoved
、UITouchPhaseBegan
、UITouchPhaseStationary
、UITouchPhaseEnded
、UITouchPhaseCanceled
。
UIKit 经过 hit-test 碰撞检测肯定哪些 View 须要响应 touch 事件,hit-test 经过比较 touch 的位置与 View 的 bounds 判断 touch 是否与 View 相交。Hit-test 是在 View 的视图层级中,取层级最深的子视图,做为 touch 事件的 first responder,而后从前向后递归地对每一个子视图进行 Hit-test,直到子视图命中,直接返回命中的子视图。
Hit-test 经过UIView
的hitTest:withEvent:
方法实现,若 touch 的位置超出了 view 的 bounds 范围,则hitTest:withEvent:
会忽略该 view 及其全部子视图。因此,当 view 的maskToBounds
为NO
时,即便 touch 看起来落在了某个视图上,但只要 touch 位置超出了 view 或者其 super view 的 bounds 范围,则该 view 仍然会接收不到 touch 事件。
碰撞检测方法- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
中,point
参数是碰撞检测点在事件发生的 view 的坐标系中的坐标;event
参数是使用本次碰撞检测的UIEvent
事件。当目标检测点不在当前 view 的范围内时,该方法返回nil
,反之则返回 view 自己。hitTest:withEvent:
方法是经过调用UIView
的- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
方法实现的,该方法忽略userInteractionEnabled
为NO
或者 alpha 值小于 0.01 的视图。
Touch 事件传递过程主要调用了hitTest:withEvent:
方法,Touch 事件若未被 gesture recognizer 捕捉则最终会去到touchesXXX:
系列方法。在响应链的调试时,已经见到很多hitTest:withEvent:
调用的痕迹。
在第一章“结论一”的运行日志中,发现点击“点我前Button”时,也对 Label A、B、C 作了碰撞检测,且并无对“点我后Button”作碰撞检测。注意到 Label 和 Button 都是self.view
的子视图,且 Label A、B、C 在“点我前Button”以前,“点我后Button”以后。前面提到过:Hit-test 是在 View 的视图层级中,取层级最深的子视图,做为 touch 事件的 first responder,而后从前向后递归地对每一个子视图进行 Hit-test。所以,self.view
调用 Hit-Test 时,首先找到的是 Label C。而后,从前向后递归调用hitTest:withEvent:
,所以才会有C >> B >> A >> 点我前Button
的顺序。为何到“点我后Button”没有递归到呢?这是由于self.view
的hitTest:withEvent:
在迭代到“点我前Button”时命中了目标,所以直接返回“点我前Button”。而更后面的“点我前Button”就直接被跳过了。
为验证上面的推测。继续在 Demo 中引入继承自UIView
的TestEventsView
类型,套路和前面的 Button、Label 一致,就是为了打印关键日志。而后将 Controller 的根视图,也就是self.view
的类型设置为TestEventsView
。而后再在 Controller 的viewDidLoad
中增长打印 Button 信息的代码以做对照。
准备就绪,运行 Demo,点击“点我前Button”,获得如下日志,干扰信息变多了,遮挡掉其中一部分。关注到红色框中的内容,发现self.view
的hitTest:forEvent:
返回的正是“点我前Button”,并且“点我前Button”的hitTest:forEvent:
返回了自身。与前面的推测彻底符合。
前一小节的调试过程其实已经能够证实改结论,可是因为只是经过对有限的相关共有方法,譬如hitTest:forEvent:
、nextResponder
的调用次序的打印彷佛还不够深刻。接下来用 lldb 下断点的方式,进行调试。
在这以前须要作一些准备工做,此次是使用 lldb 调试主要经过查看函数调用栈、寄存器数据、内存数据等方式分析,所以不须要打印日志的操做,何况新增的hitTest:withEvent
、nextResponder
、touchesXXX
方法会徒增调用栈的层数,所以将TestEventsLabel
、TestEventsButton
、TestEventsView
、ViewController
的这些方法悉数屏蔽。去掉一切没必要要的日志打印逻辑。
准备就绪,运行 Demo,先不急着开始,首先查看 Demo 的视图层级,先记住这个UIWindow
实例,它是应用的主窗口,它的内存地址是0x7fa8f10036b0
,后面会用到。
注意:从 iOS 13 开始,引入了
UIWindowScene
统一管理应用的窗口和屏幕,UIWindowScene
包含windows
和screen
属性。上图所展现UIWindowScene
只包含了一个子 Window,实际真的如此吗?
首先使用break point -n
命令在四个关键方法处下断点:
hitTest:withEvent:
nextResponder
touchesBegan:withEvent:
touchesEnded:withEvent:
注意:汇编代码中的函数一般以
pushq %rbp
、movq %rsp, %rbp
开头,其中bp
是基地址寄存器(base pointer),sp
是堆栈寄存器(stack pointer),bp
保存当前函数栈帧的基地址(栈底),sp
保存当前函数栈帧的下一个可分配地址(栈顶),函数每分配一个单元的栈空间,sp
自动递增,而bp
保持不变。相应地,函数返回前都会有popq %rbp
操做。
点击“点我前Button”,很快触发了第一个hitTest:withEvent:
的断点。先用bt
命令查看当前调用栈,发现第 0 帧调用了UIAutoRotatingWindow
的hitTest:withEvent:
,打印寄存器数据获取到r14
、r15
都传递了UIWindow
参数,但实际上调用该方法的是一个UITextEffectsWindow
实例,UITextEffectsWindow
是UIAutoRotatingWindow
。它的内存地址是0x00007fa8ebe05050
,显然不是 main window。
而r14
传递的地址是0x00007fa8f10036b0
,正是 main window。之因此是UITextEffectsWindow
接收到hitTest:withEvent:
是由于Window 层中的碰撞检测是使用上图中红色框中的私有方法进行处理。接下来一步步弄清红框中的碰撞检测处理的 touch 事件的传递具体经由哪些 Window 实例。frame select 8
跳到第 8 帧,跟踪到了一个UIWindow
对象0x7fa8f10036b0
。所以,Window 层级中最早接收到 touch 事件的确实是 main window。
依次类推打印出全部栈帧的当前对象以下(有些层级到断点行时寄存器已经被修改,会找不到目标类型的实例,此时能够回到上一层打印须要传入下一层的全部寄存器的值便可):
frame 0: UITextEffectsWindow 0x00007fa8ebe05050 frame 1: UITextEffectsWindow 0x00007fa8ebe05050 frame 2: UITextEffectsWindow 0x00007fa8ebe05050 frame 3: UIWindow +(类方法) frame 4: UIWindowScene -(nil不须要使用self) frame 5: UIWindowScene 0x00007fa8ebd06c50 frame 6: UIWindowScene 0x00007fa8ebd06c50 frame 7: UIWindow +(类方法) frame 8: UIWindow 0x00007fa8f10036b0
能够进一步使用 lldb 调试命令理清上面几个对象之间的关系。首先是图一中 window scene 与 window 之间的关系。图二则打印出了UITextEffectsWindow
的视图层级。图三是 main window 的视图层级,注意到红框中的对象,是否似曾相识?没错,到这里追踪到 Controller 的TestEventsView
类型的根 view。
为何新版本 iOS 的 touch 事件传递过程,须要分离出 Window 层和 View 层阶段?是由于自 iOS 13 起引入UIWindowScene
后,UITextEffectsWindow
和 main window 有各自的视图层级,且二者都没有superview
,所以必须修改 touch 的传递策略,让事件都能分发到两个 window 中。
注意:本来猜测,C 语言转化为汇编语言时,遵循声明一个局部变量就要分配一个栈空间的,调用函数时须要将形参和返回值地址推入堆栈,然而从调试过程当中查看 Objective-C 的汇编代码,其实现并非如此。因为现代处理器包含了大量的高效率存储器,所以 clang 编译时会最大限量地合理利用起这些寄存器(一般是通用寄存器)以提升程序执行效率。一般传递参数用到最多的是
r12
、r13
、r14
、r15
寄存器,但毫不仅限于以上列举的几个。这给源代码调试增长了很大的难度。
注意这里的 touch 事件并非指 UIKit 的 touch event,UIKit 的 touch event 在 UIKit 接收到来自驱动层的点击事件信号后就构建了 touch 事件的UIEvent
对象。这里的 touch 事件是指通过碰撞检测肯定了 touch event 的响应者从touchesBegan:withEvent:
开始传递以前产生的UITouch
对象。
一、如今正式开始追踪 touch 事件。已知,步骤二中打断的第一次hitTest:withEvent:
命中,其调用对象是UITextEffectsWindow
实例。此时点击调试工具栏中的“continue”按钮,继续执行。
注意:因为调试过程比较长,致使继续运行时 lldb 被打断须要从新运行。不过问题不大,由于前面的工做已经肯定了须要追踪的关键对象。所以从新运行后,从新下断点,再记录一次关键对象的地址便可。
开始收集断点命中(包括第一次命中):
UITextEffectsWindow
:(Hit-Test)UITextEffectsWindow
:(Hit-Test)(调用 UIView 的实现)UIInputSetContainerView
:(Hit-Test)UIInputSetContainerView
:(Hit-Test)(调用 UIView 的实现)UIEditingOverlayGestureView
:(Hit-Test)UIEditingOverlayGestureView
:(Hit-Test)(调用 UIView 的实现)UIInputSetHostView
:(Hit-Test)UIInputSetHostView
:(Hit-Test)(调用 UIView 的实现)UIWindow
:(Hit-Test)(调用 UIView 的实现)UITransitionView
:(Hit-Test)UITransitionView
:(Hit-Test)(调用 UIView 的实现)UIDropShadowView
:(Hit-Test)UIDropShadowView
:(Hit-Test)(调用 UIView 的实现)TestEventsView
:(Hit-Test)(调用 UIView 的实现)至此 Hit-Test 断点命中了以前自定义的 Controller 的TestEventsView
类型的根类,在这里打印一下调用栈。调用栈增长至 38 层以下图。并且上面的层次都是在调用hitTest:withEvents
方法,这是个明显的递归调用的表现。并且到此为止,Hit-Test 仍然没有命中任何视图。
二、继续运行收集断点信息:
Hit-Test 断点终于命中了 Demo 的自定义 Label 和 Button 控件。根据收集的信息,命中顺序是 LabelC -> LabelB -> LabelA -> 点我前Button。此时,不急着继续,在调试窗口中使用bt
指令,观察到调用栈深度已经来到了 43 层之多,以下图所示。可是注意到一点,以上每次断点命中,其调用栈深度都是 43 层,也就是说上面几个同层视图的碰撞检测过程是循环迭代,而不是递归,三个TestEventsLabel
调用hitTest:withEvent:
均可以直接返回nil
不须要递归。
三、继续运行收集断点信息:
TestEventsButton
:(Hit-Test)(调用 UIView 的实现)UIButtonLabel
:(Hit-Test)(调用超类的实现)调用栈到达了第一个高峰 49 层,以下图一所示。此时若点击继续,会发现调用栈回落到 13 层,以下图二所示。说明 Hit-Test 断点在命中UIButtonLabel
后,本次 Hit-Test 递归就返回了。至于具体返回什么对象,实际上在 1.2.2 的调试日志中已经打印出来了,正是“点我前Button”。
四、继续运行,Demo 会进入第二次 Hit-Test 递归,之因此一次点击事件引起了两轮递归,是由于 touch 事件在开始和结束时,各进行了一轮碰撞检测。继续收集断点信息:
UIWindow
:(Hit-Test)(调用 UIView 的实现)UITransitionView
:(Hit-Test)UITransitionView
:(Hit-Test)(调用 UIView 的实现)UIDropShadowView
:(Hit-Test)UIDropShadowView
:(Hit-Test)(调用 UIView 的实现)TestEventsView
:(Hit-Test)(调用 UIView 的实现)TestEventsLabel
:(Hit-Test)(调用 UIView 的实现)TestEventsLabel
:(Hit-Test)(调用 UIView 的实现)TestEventsLabel
:(Hit-Test)(调用 UIView 的实现)TestEventsButton
:(Hit-Test)(调用 UIControl 的实现)TestEventsButton
:(Hit-Test)(调用 UIView 的实现)UIButtonLabel
:(Hit-Test)(调用 UIView 的实现)调用栈再次到达了高峰 41 层以下图所示。
此时先不急着继续。由于以上是 Hit-Test 在本次调试中的最后一次断点命中,点击继续 Hit-Test 递归必然返回“点我前Button”,表示碰撞检测命中了该按钮控件。第二轮 Hit-Test 的调用栈明显浅许多,不难发现其缘由是该轮碰撞检测没有通过UITextEffectsWindow
而直接从UIWindow
开始(个中缘由不太肯定)。
总结 Hit-Test 的处理过程的要点是:
文字表述彷佛有点不太直观,仍是用我们程序员的语言吧,伪代码以下:
- (UIView * _Nullable)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1. 优先检测本身,不命中则马上排除
BOOL isHit = [self pointInside:point withEvent:event];
if(!isHit){
return nil;
}
// 2. 从前向后循环迭代全部子视图
for(UIView* subviews in subviews){
// 跨视图层级从 superview 向 subview 递归
UIView* hitView = [subviews hitTest:point withEvent:event];
if(hitView)
return hitView;
}
// 3. 全部子视图未命中返回nil
return nil;
}
复制代码
步骤三执行完成,UIKit 产生了UITouch
事件并开始传递该事件。紧接在以前的基础上继续调试。再点击 continue,收集断点信息:
_UISystemGestureGateGestureRecognizer
:(Touches-Began)_UISystemGestureGateGestureRecognizer
:(Touches-Began)TestEventsButton
:(Touches-Began)(调用 UIControl 的实现)此时 Button 尝试触发 touchesBegan,开始UITouch
事件传递。调用栈以下,是由 UIWindow 发送过来的 touch 事件。注意上面TestEventsButton
调用的是UIControl 的实现,记住这个“猫腻”,后面的部分会再次提到。
TestEventsButton
:(Next-Responder)(调用 UIView 的实现)终于命中了 Next-Responder 断点,从上下两个调用栈能够发现,nextResponder
是在touchBegan
方法内调用的。
再点击 continue,继续运行收集断点信息:
TestEventsView
:(Next-Responder)(调用 UIView 的实现)nextResponder
是在touchBegan
方法内调用的,且增长了调用栈深度,说明nextResponder
也触发了递归的过程。可是递归的不是nextResponder
而是UIResponder
里面的一个私有方法_controlTouchBegan:withEvent:
。该方法彷佛只简单遍历了一轮响应链,其余的什么都没作。
再点击 continue,继续运行收集断点信息:
UIViewController
:(Next-Responder)(调用 UIViewController 的实现)UIDropShadowView
:(Next-Responder)(调用 UIView 的实现)UITransitionView
:(Next-Responder)(调用 UIView 的实现)UIWindow
:(Next-Responder)UIWindowScene
:(Next-Responder)(调用 UIScene 的实现)UIApplication
:(Next-Responder)AppDelegate
:(Next-Responder)(调用 UIResponder 的实现)在AppDelegate
层,调用栈达到顶峰,以下图所示。
在调试过程当中,发现响应链上除了第一响应者“点我前Button”外的全部对象都没有调用touchesBegan:withEvent:
响应该 touch 事件。那么这就是对 touch 事件该有的处理么?其实否则,因为调试时点击的是 Button 控件,所以上述是对UIControl
控件做为第一响应者的状况的,经过定制UIControl
类touchesBegan:withEvent:
方法实现的,特殊处理。上面提到的私有方法_controlTouchBegan:withEvent:
就是为了告诉后面响应链后面的响应者这个 touch 事件已经被前面的 UIControl 处理了,请您不要处理该事件。
那么UIResponder
原始的响应流程是怎样的呢?继续调试状况二。
流程渐渐明朗的状况下,能够先breakpoint disable
终止上面的断点,而后breakpoint delete XXX
删除掉hitTest:withEvent:
断点,以减小打断次数。解屏蔽掉以前屏蔽的打印日志的代码,由于当断点命中 Demo 中的自定义类时,能够直接判定nextResponder
的触发类。
点击界面中的 Label C。开始收集信息(省略自定义日志打印方法只保留原始方法):
_UISystemGestureGateGestureRecognizer
:(Touches-Began)_UISystemGestureGateGestureRecognizer
:(Touches-Began)TestEventsLabel
:(Touches-Began)(调用 UIResponder 的实现)TestEventsLabel
:(Next-Responder)(调用 UIView 的实现)TestEventsView
:(Touch-Began)(调用 UIResponder 的实现)TestEventsView
:(Next-Responder)(调用 UIView 的实现)UIViewController
:(Touch-Began)(调用 UIResponder 的实现)UIViewController
:(Next-Responder)(调用 UIViewController 的实现)UIDropShadowView
:(Touch-Began)(调用 UIResponder 的实现)UIDropShadowView
:(Next-Responder)(调用 UIView 的实现)UITransitionView
:(Touch-Began)(调用 UIResponder 的实现)UITransitionView
:(Next-Responder)(调用 UIView 的实现)UIWindow
:(Touch-Began)(调用 UIResponder 的实现)UIWindow
:(Next-Responder)UIWindowScene
:(Touch-Began)(调用 UIResponder 的实现)UIWindowScene
:(Next-Responder)(调用 UIScene 的实现)UIApplication
:(Touch-Began)(调用 UIResponder 的实现)UIApplication
:(Next-Responder)AppDelegate
:(Touch-Began)(调用 UIResponder 的实现)AppDelegate
:(Next-Responder)(调用 UIResponder 的实现)至此先看一下调用栈,显然touchesBegan:withEvent:
也是递归的过程:
总结上面收集的信息,UIResponder
做为第一响应者和UIControl
做为第一响应者的区别已经至关明显了。当UIResponder
做为第一响应者时,是沿着响应链传递,通过的每一个对象都会触发touchesBegan:withEvents:
方法。
Touch 事件事件结束会触发第一响应者的touchesEnded:withEvent:
方法,具体传递过程和步骤四中一致。一样要区分UIControl
和UIResponder
的处理。
最后,不管是UIControl
仍是UIResponder
,在完成全部touchesEnded:withEvent:
处理后,都要额外再从第一响应者开始遍历一次响应链。从调用栈能够看到是为了传递UIResponder
的_completeForwardingTouches:phase:event
消息。具体缘由不太清楚。
行文至此,文章篇幅已经有点长,所以在下一篇文章中在调试这部份内容。
UIControl
的 Target-Action 方式仍是UIResponder
的touchesXXX
方式处理用户事件,都涉及到 Hit-Test 和 响应链的内容;UIControl
使用 Target-Action 注册用户事件,当后面的控件被前面的控件覆盖时,若用户事件(UIEvent
)被前面的控件拦截(不管前面的控件有没有注册 Target-Action),则后面的控件永远得不处处理事件的机会,即便前面的控件未注册 Target-Action;UIControl
使用 Target-Action 注册用户事件,指定 Target 为空时,Action 消息会沿着响应链传递,直到找到能响应 Action 的 Responder 为止,Action 一旦被其中一个 Responder 响应,响应链后面的对象就再也不处理该 Action 消息;UIResponder
的nextResponder
串联而成;nextResponder
是 Controller;nextResponder
是其 present controller;nextResponder
是 Window,注意调试中 Controller 的nextResponder
是返回nil
,但实际上它们确实有这层关系;nextResponder
是 Window Scene;nextResponder
是 Application;nextResponder
是 AppDelegate(当 AppDelegate 是UIResponder
类型时);UITouch
事件,UITouch
事件会沿着响应链传递到后面的全部响应者;UIResponder
做为第一响应者响应了 touch 事件,响应链后面的全部响应者也会触发touchesXXX
系列方法;UIControl
控件做为第一响应者响应了 touch 事件,响应链后面的全部响应者均再也不处理该 touch 事件;