天天看点

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

调试iOS用户交互事件响应流程一、响应链1.1 Next Responder1.1.1 调试nextResponder1.2 Target-Action和响应链1.2.1 注册UIControlEvents1.2.2 调试UIControlEvents的传递结论一:Action不会在同级视图层级中传递结论二:Target为空时Action仍可以被响应结论三:Target为空时Action沿响应链传递1.3 手势识别和响应链1.4 修改响应链二、Touch事件传递2.1 碰撞检测2.2 调试Touch事件传递步骤零:准备工作步骤一:下断点步骤二:简单分析 touch 事件在 Window 层的分发步骤三:分析 Touch 事件的产生步骤四:分析 touch 事件开始后的传递情况一:点击 Button 控件时情况二:点击 Label 视图步骤五:分析 touch 事件结束后的传递三、RunLoop与事件(TODO)四、总结

调试iOS用户交互事件响应流程

2020-03-19

通常 iOS 界面开发中处理各种用户交互事件。其中,

UIControlEvent

以注册的 Target-Action 的方式绑定到控件;

UIGestureRecognizer

通过

addGestureRecognizer:

添加到

UIView

gestureRecognizers

属性中;

UIResponder

提供了

touchesBegin/Moved/Ended/Canceled/:withEvent:

motionsXXX:withEvent:

pressXX:withEvent:

系列接口,将用户设备的触摸、运动、按压事件通知到

UIResponder

对象等等。以上都是常用开发者处理用户交互事件的方式,那么隐藏在这些接口之下,从驱动层封装交互事件对象到 UI 控件接收到用户事件的流程是怎样的呢?本文主要探讨的就是这个问题。

一、响应链

Apple Documentation 官方文档Using Responders and the Responder Chain to Handle Events介绍了利用

UIResponder

的响应链来处理用户事件。

UIResponder

实现了

touchesXXX

pressXXX

motionXXX

分别用于响应用户的触摸、按压、运动(例如

UIEventSubtypeMotionShake

)交互事件。

UIResponder

包含

nextResponder

属性。

UIView

UIWindow

UIController

UIApplication

都是

UIResponder

的派生类,所以都能响应以上事件。

1.1 Next Responder

响应链结构如下图所示,基本上是通过

UIResponder

nextResponder

成员串联而成,基本上是按照 view 的层级,从前向后由子视图向父视图传递,且另外附加其他规则。总的响应链的规则如下:

  • View 的

    nextResponder

    是其父视图;
  • 当 View 为 Controller 的根视图时,

    nextResponder

    是 Controller;
  • Controller 的

    nextResponder

    是 present Controller 的控制器;
  • 当 Controller 为根控制器时,

    nextResponder

    是 Window;
  • Window 的

    nextResponder

    是 Application;
  • Application 的

    nextResponder

    是 App Delegate(仅当 App Delegate 为

    UIResponder

    类型);
android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

响应链

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。在后面的调试过程会有体现。

1.1.1 调试nextResponder

使用一个简单的 Demo 调试

nextResponder

。界面如下图所示,包含三个 Label,从颜色可以判断其层次从后往前的顺序是:A >> B >> C。下面两个按钮另做他用,先忽略。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

运行 Demo,查看各个元素的

nextResponder

,确实如前面所述。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

1.2 Target-Action和响应链

UIControl

控件与关联的 target 对象通信,直接通过向 target 对象发送 action 消息。虽然 Action 消息虽然不是事件,但是 Action 消息的传递是要经过响应链的。当接收到用户交互事件的控件的 target 为

nil

时,会沿着控件的响应链向下搜索,直到找到实现该 action 方法的对象为止。UIKit 的编辑菜单就是通过这个机制实现的,UIKit 会沿着控件的响应链搜索实现了

cut:

copy:

paste:

等方法的对象。

1.2.1 注册UIControlEvents

UIControl

控件调用

addTarget:action:forControlEvents:

方法注册事件时,会将构建

UIControlTargetAction

对象并将其添加到

UIControl

控件的

(NSMutableArray*)_targetActions

私有成员中,

addTarget:action:forControlEvents:

方法的 Apple Documentation 注释中有声明调用该方法时

UIControl

并不会持有 target 对象,因此无需考虑循环引用的问题。UIControl Events 注册过程的简单调试过程如下:

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

UIControl Target Action

附注: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.

1.2.2 调试UIControlEvents的传递

前面内容提到,控件的 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 实现):

结论一:Action不会在同级视图层级中传递

一切准备就绪,运行 Demo,点击“点我前Button”,抓取到了如下日志。注意框①中指定的 target 是

self

,也就是 Controller。可以发现点击事件产生,调用了若干次碰撞检测(框②),若干次

nextResponder

(框③),最终只调用了 Controller 中“点我前Button”的 action 方法。这是因为:

  • Target-Action 消息在传递时,永远不会在同级视图层级中传递;
  • Target 非空,则 UIKit 在确认控件响应某个事件后,会直接给控件的 target 对象发送 action 消息,这个过程不存在任何视图层级传递 或 响应链传递的过程;
android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程
结论二:Target为空时Action仍可以被响应

接下来将

addTarget:action:

中指定的 target 设为

nil

。然后在

TestEventsButton

中也添加 action 的响应代码,如下所示。

点击“点我前Button”,抓取到了如下日志。这次,由

TestEventsButton

处理了 action 消息。说明当控件注册 action 时指定的 target 为

nil

时,action 消息仍然可以被响应,且 action 只响应一次。请记住,此时

nextResponder

被调用了 5 次。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程
结论三:Target为空时Action沿响应链传递

再进一步修改代码,将结论二中

TestEventsButton

的新增代码删除,仍然将

addTarget:action:

中指定的 target 设为

nil

。点击“点我前Button”,抓取到了如下日志。这次,处理 action 消息的是 Controller。而且从日志中我们发现,这次

nextResponder

调用了 6 次,确切地说,是在 Button

touchBegin

之后,Controller 处理 action 消息之前(如图中红框所示)。这是因为,target 为

nil

时,action 消息会沿着响应链传递,直到找到可以响应 action 的对象为止。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

可以继续尝试给“点我后Button”,直接将

self.btnFront

的注册 Target-Action 的代码删掉。运行 Demo,再次点击“点我前Button”,此时

didClickBtnBack

仍然不触发。这其实只是进一步印证了“结论一”的结论,这里不再演示。

整个调试过程下来,可以发现,被 ButtonA 覆盖的 ButtonB,所有 action 都会被 ButtonA 拦截,被覆盖的 ButtonB 不会获得任何触发 action 的机会。

1.3 手势识别和响应链

Gesture Recognizer 会在 View 之前接收 Touch 和 Press 事件,当 Gesture Recognizer 对一连串的 Touch 事件手势识别失败时,UIKit 才将这些 Touch 事件发送给 View。若 View 不处理这些 Touch 事件,UIKit 将其递交到响应链。

1.4 修改响应链

响应链主要通过

nextResponder

方法串联,因此重新实现

UIResponder

派生类的

nextResponder

方法可以实现响应链修改的效果。

二、Touch事件传递

当 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

2.1 碰撞检测

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 的视图。

2.2 调试Touch事件传递

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”就直接被跳过了。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

为验证上面的推测。继续在 Demo 中引入继承自

UIView

TestEventsView

类型,套路和前面的 Button、Label 一致,就是为了打印关键日志。然后将 Controller 的根视图,也就是

self.view

的类型设置为

TestEventsView

。然后再在 Controller 的

viewDidLoad

中增加打印 Button 信息的代码以作对照。

准备就绪,运行 Demo,点击“点我前Button”,得到以下日志,干扰信息变多了,遮挡掉其中一部分。关注到红色框中的内容,发现

self.view

hitTest:forEvent:

返回的正是“点我前Button”,而且“点我前Button”的

hitTest:forEvent:

返回了自身。与前面的推测完全符合。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

步骤零:准备工作

前一小节的调试过程其实已经可以证明改结论,但是由于只是通过对有限的相关共有方法,譬如

hitTest:forEvent:

nextResponder

的调用次序的打印似乎还不够深入。接下来用 lldb 下断点的方式,进行调试。

在这之前需要做一些准备工作,这次是使用 lldb 调试主要通过查看函数调用栈、寄存器数据、内存数据等方式分析,因此不需要打印日志的操作,况且新增的

hitTest:withEvent

nextResponder

touchesXXX

方法会徒增调用栈的层数,因此将

TestEventsLabel

TestEventsButton

TestEventsView

ViewController

的这些方法悉数屏蔽。去掉一切不必要的日志打印逻辑。

准备就绪,运行 Demo,先不急着开始,首先查看 Demo 的视图层级,先记住这个

UIWindow

实例,它是应用的主窗口,它的内存地址是

0x7fa8f10036b0

,后面会用到。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程
注意:从 iOS 13 开始,引入了

UIWindowScene

统一管理应用的窗口和屏幕,

UIWindowScene

包含

windows

screen

属性。上图所展示

UIWindowScene

只包含了一个子 Window,实际真的如此吗?

步骤一:下断点

首先使用

break point -n

命令在四个关键方法处下断点:

  • hitTest:withEvent:

  • nextResponder

  • touchesBegan:withEvent:

  • touchesEnded:withEvent:

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程
注意:汇编代码中的函数通常以

pushq %rbp

movq %rsp, %rbp

开头,其中

bp

是基地址寄存器(base pointer),

sp

是堆栈寄存器(stack pointer),

bp

保存当前函数栈帧的基地址(栈底),

sp

保存当前函数栈帧的下一个可分配地址(栈顶),函数每分配一个单元的栈空间,

sp

自动递增,而

bp

保持不变。相应地,函数返回前都会有

popq %rbp

操作。

步骤二:简单分析 touch 事件在 Window 层的分发

点击“点我前Button”,很快触发了第一个

hitTest:withEvent:

的断点。先用

bt

命令查看当前调用栈,发现第 0 帧调用了

UIAutoRotatingWindow

hitTest:withEvent:

,打印寄存器数据获取到

r14

r15

都传递了

UIWindow

参数,但实际上调用该方法的是一个

UITextEffectsWindow

实例,

UITextEffectsWindow

UIAutoRotatingWindow

。它的内存地址是

0x00007fa8ebe05050

,显然不是 main window。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

r14

传递的地址是

0x00007fa8f10036b0

,正是 main window。之所以是

UITextEffectsWindow

接收到

hitTest:withEvent:

是因为Window 层中的碰撞检测是使用上图中红色框中的私有方法进行处理。接下来一步步弄清红框中的碰撞检测处理的 touch 事件的传递具体经由哪些 Window 实例。

frame select 8

跳到第 8 帧,跟踪到了一个

UIWindow

对象

0x7fa8f10036b0

。因此,Window 层级中最先接收到 touch 事件的确实是 main window。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

依次类推打印出所有栈帧的当前对象如下(有些层级到断点行时寄存器已经被修改,会找不到目标类型的实例,此时可以回到上一层打印需要传入下一层的所有寄存器的值即可):

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。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

图一:WindowScene与Window之间的关系

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

UITextEffectsWindow视图层级

图二:UITextEffectsWindow的视图层级

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

图三:Main Window的视图层级

为什么新版本 iOS 的 touch 事件传递过程,需要分离出 Window 层和 View 层阶段?是因为自 iOS 13 起引入

UIWindowScene

后,

UITextEffectsWindow

和 main window 有各自的视图层级,且两者都没有

superview

,因此必须修改 touch 的传递策略,让事件都能分发到两个 window 中。

注意:原本猜想,C 语言转化为汇编语言时,遵循声明一个局部变量就要分配一个栈空间的,调用函数时需要将形参和返回值地址推入堆栈,然而从调试过程中查看 Objective-C 的汇编代码,其实现并不是如此。由于现代处理器包含了大量的高效率存储器,因此 clang 编译时会最大限量地合理利用起这些寄存器(通常是通用寄存器)以提高程序执行效率。通常传递参数用到最多的是

r12

r13

r14

r15

寄存器,但绝不仅限于以上列举的几个。这给源代码调试增加了很大的难度。

步骤三:分析 Touch 事件的产生

注意这里的 touch 事件并不是指 UIKit 的 touch event,UIKit 的 touch event 在 UIKit 接收到来自驱动层的点击事件信号后就构建了 touch 事件的

UIEvent

对象。这里的 touch 事件是指经过碰撞检测确定了 touch event 的响应者从

touchesBegan:withEvent:

开始传递之前产生的

UITouch

对象。

1、现在正式开始追踪 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 仍然没有命中任何视图。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

2、继续运行收集断点信息:

  • {TestEventsLabel: 0x7fd8d48071a0; baseClass = UILabel; frame = (121 162; 250 166); text = 'C'; opaque = NO; autoresize = RM+BM; layer = <_uilabellayer:>}:(Hit-Test)(调用超类的实现)
  • {TestEventsLabel: 0x7fd8d4806df0; baseClass = UILabel; frame = (82 116; 250 166); text = 'B'; opaque = NO; autoresize = RM+BM; layer = <_uilabellayer:>}:(Hit-Test)(调用超类的实现)
  • {TestEventsLabel: 0x7fd8d4805aa0; baseClass = UILabel; frame = (44 75; 250 166); text = 'A'; opaque = NO; autoresize = RM+BM; layer = <_uilabellayer:>}:(Hit-Test)(调用超类的实现)
  • {TestEventsButton: 0x7fd8d48056c0; baseClass = UIButton; frame = (121 478; 173 79); opaque = NO; autoresize = RM+BM; layer =

    }:(Hit-Test)(调用 UIControl 的实现)

Hit-Test 断点终于命中了 Demo 的自定义 Label 和 Button 控件。根据收集的信息,命中顺序是 LabelC -> LabelB -> LabelA -> 点我前Button。此时,不急着继续,在调试窗口中使用

bt

指令,观察到调用栈深度已经来到了 43 层之多,如下图所示。但是注意到一点,以上每次断点命中,其调用栈深度都是 43 层,也就是说上面几个同层视图的碰撞检测过程是循环迭代,而不是递归,三个

TestEventsLabel

调用

hitTest:withEvent:

都可以直接返回

nil

不需要递归。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

3、继续运行收集断点信息:

  • TestEventsButton

    :(Hit-Test)(调用 UIView 的实现)
  • UIButtonLabel

    :(Hit-Test)(调用超类的实现)

调用栈到达了第一个高峰 49 层,如下图一所示。此时若点击继续,会发现调用栈回落到 13 层,如下图二所示。说明 Hit-Test 断点在命中

UIButtonLabel

后,本次 Hit-Test 递归就返回了。至于具体返回什么对象,实际上在 1.2.2 的调试日志中已经打印出来了,正是“点我前Button”。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

图一:Hit-Test调用栈到达顶峰

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

图二:Hit-Test调用栈回落

4、继续运行,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 层如下图所示。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

此时先不急着继续。因为以上是 Hit-Test 在本次调试中的最后一次断点命中,点击继续 Hit-Test 递归必然返回“点我前Button”,表示碰撞检测命中了该按钮控件。第二轮 Hit-Test 的调用栈明显浅许多,不难发现其原因是该轮碰撞检测没有经过

UITextEffectsWindow

而直接从

UIWindow

开始(个中原因不太确定)。

总结 Hit-Test 的处理过程的要点是:

  • 优先检测自己是否命中,不命中则直接忽略所有 subviews;
  • 若自己命中,则对所有子视图按同层级视图顺序从前向后的顺序依次进行碰撞检测,因此碰撞检测也是 superview 到 subview 的按视图层级从后向前递归的过程;
  • 若所有子视图均未命中,自己的碰撞检测才返回 nil。

文字表述似乎有点不太直观,还是用咱们程序员的语言吧,伪代码如下:

步骤四:分析 touch 事件开始后的传递

情况一:点击 Button 控件时

步骤三执行完成,UIKit 产生了

UITouch

事件并开始传递该事件。紧接在之前的基础上继续调试。再点击 continue,收集断点信息:

  • _UISystemGestureGateGestureRecognizer

    :(Touches-Began)
  • _UISystemGestureGateGestureRecognizer

    :(Touches-Began)
  • TestEventsButton

    :(Touches-Began)(调用 UIControl 的实现)

此时 Button 尝试触发 touchesBegan,开始

UITouch

事件传递。调用栈如下,是由 UIWindow 发送过来的 touch 事件。注意上面

TestEventsButton

调用的是UIControl 的实现,记住这个“猫腻”,后面的部分会再次提到。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程
  • TestEventsButton

    :(Next-Responder)(调用 UIView 的实现)

终于命中了 Next-Responder 断点,从上下两个调用栈可以发现,

nextResponder

是在

touchBegan

方法内调用的。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

再点击 continue,继续运行收集断点信息:

  • TestEventsView

    :(Next-Responder)(调用 UIView 的实现)

nextResponder

是在

touchBegan

方法内调用的,且增加了调用栈深度,说明

nextResponder

也触发了递归的过程。但是递归的不是

nextResponder

而是

UIResponder

里面的一个私有方法

_controlTouchBegan:withEvent:

。该方法似乎只简单遍历了一轮响应链,其他的什么都没做。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

再点击 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

层,调用栈达到顶峰,如下图所示。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

在调试过程中,发现响应链上除了第一响应者“点我前Button”外的所有对象都没有调用

touchesBegan:withEvent:

响应该 touch 事件。那么这就是对 touch 事件该有的处理么?其实不然,由于调试时点击的是 Button 控件,因此上述是对

UIControl

控件作为第一响应者的情况的,通过定制

UIControl

touchesBegan:withEvent:

方法实现的,特殊处理。上面提到的私有方法

_controlTouchBegan:withEvent:

就是为了告诉后面响应链后面的响应者这个 touch 事件已经被前面的 UIControl 处理了,请您不要处理该事件。

那么

UIResponder

原始的响应流程是怎样的呢?继续调试情况二。

情况二:点击 Label 视图

流程渐渐明朗的情况下,可以先

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:

也是递归的过程:

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

总结上面收集的信息,

UIResponder

作为第一响应者和

UIControl

作为第一响应者的区别已经相当明显了。当

UIResponder

作为第一响应者时,是沿着响应链传递,经过的每个对象都会触发

touchesBegan:withEvents:

方法。

步骤五:分析 touch 事件结束后的传递

Touch 事件事件结束会触发第一响应者的

touchesEnded:withEvent:

方法,具体传递过程和步骤四中一致。同样要区分

UIControl

UIResponder

的处理。

最后,无论是

UIControl

还是

UIResponder

,在完成所有

touchesEnded:withEvent:

处理后,都要额外再从第一响应者开始遍历一次响应链。从调用栈可以看到是为了传递

UIResponder

_completeForwardingTouches:phase:event

消息。具体原因不太清楚。

android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

三、RunLoop与事件(TODO)

行文至此,文章篇幅已经有点长,因此在下一篇文章中在调试这部分内容。

四、总结

  • 无论是使用

    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 消息;
  • 响应链是以 View 为起始,向 superview 延伸的一个反向树型结构,通过

    UIResponder

    nextResponder

    串联而成;
  • 当 View 作为 Controller 的根 view 时,

    nextResponder

    是 Controller;
  • 当 Controller 是由其他 Controller present 而来,则

    nextResponder

    是其 present controller;
  • 当 Controller 是 Window 的根 Controller,则

    nextResponder

    是 Window,注意调试中 Controller 的

    nextResponder

    是返回

    nil

    ,但实际上它们确实有这层关系;
  • Window 的

    nextResponder

    是 Window Scene;
  • Window Scene 的

    nextResponder

    是 Application;
  • Application 的

    nextResponder

    是 AppDelegate(当 AppDelegate 是

    UIResponder

    类型时);
  • Hit-Test 优先检测自己是否命中,不命中则直接忽略所有 subviews;
  • Hit-Test 若自己命中,则对所有子视图按同层级视图顺序从前向后的顺序依次进行碰撞检测,因此碰撞检测也是 superview 到 subview 的按视图层级从后向前递归的过程;
  • Hit-Test 若未命中任何子视图,自己的碰撞检测才返回 nil;
  • Hit-Test 命中目标后,产生

    UITouch

    事件,

    UITouch

    事件会沿着响应链传递到后面的所有响应者;
  • UIResponder

    作为第一响应者响应了 touch 事件,响应链后面的所有响应者也会触发

    touchesXXX

    系列方法;
  • UIControl

    控件作为第一响应者响应了 touch 事件,响应链后面的所有响应者均不再处理该 touch 事件;
android控件的touch事件_调试iOS用户交互事件响应流程调试iOS用户交互事件响应流程

继续阅读