·
·
文章目录
  1. 事件响应机制
    1. 事件类型
    2. 从一个触摸事件说起
    3. 事件传递机制
      1. 逆先序深度优先遍历算法
      2. hitTest:withEvent:
        1. 重写 hitTest:withEvent: 场景
    4. 事件响应链
      1. 处理事件
      2. 决定第一响应者
      3. 改变响应链
    5. 事件是怎么被接收的?
    6. 参考

事件响应机制

事件响应机制

上面我们提到 UIKit 主要是通过响应者(UIResponder)来响应用户事件,那么系统是如何来处理的呢?

事件类型

首先我们看下目前系统的定义的 UIEventType,我们主要通过 UIEventTypeTouches 来了解下事件响应:

1
2
3
4
5
6
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches, // 触摸事件
UIEventTypeMotion, // 晃动事件
UIEventTypeRemoteControl, // 远程控制事件,主要是外部辅助设备或者耳机的远程命令,例如控制音乐声音的大小,或者下一首歌。
UIEventTypePresses API_AVAILABLE(iOS(9.0)), // 物理按键事件
};

从一个触摸事件说起

我们为了搞清楚事件响应机制,我们用触摸事件来进行举例。当触摸事件发生的时候,一直到找到对应这个事件的响应,我把这个过程分为两步:

  • 通过事件传递机制找到 First Responder
  • First Responder 通过事件响应链找到 Touch Event

ps: 用触摸事件举例是因为有些事件是不需要通过事件传递和响应机制来寻找 First Responder,比如与加速计、陀螺仪、磁力仪相关的运动事件是直接通过 Core Motion 派发的。

事件传递机制

UIKit uses view-based hit-testing to determine where touch events occur. Specifically, UIKit compares the touch location to the bounds of view objects in the view hierarchy. The hitTest:withEvent: method of UIView traverses the view hierarchy, looking for the deepest subview that contains the specified touch, which becomes the first responder for the touch event.

当点击屏幕时,系统会产生一个触摸事件 UIEvent,系统会把这个 UIEvent 放到 Application 的事件队列中,Application 会把事件分发下去。首先响应的是 UIWindow,他会调用 hitTest:withEvent: ,找到能够响应事件的 UIView。UIView 会通过 hitTest:withEvent: 根据触摸事件的 location 在视图的层级结构中进行遍历,找到包含该触摸的层级最深的子视图,定义这个视图作为响应事件(touch event)的 first responder。

我们通过下面的例子,来详细说明两个原理:

  • 如何进行遍历?
  • hitTest 怎么工作的?

Example

逆先序深度优先遍历算法

如上图例子所示,当我们点击 View B.1 的时候,如何进行遍历来确认该 View 是 first responder?

Tree

系统会根据 view 添加的顺序,确定其在 subview 数组中的顺序。我们将视图的树形结构画出来,针对这个树,是按照逆先序深度优先遍历。根据图上绿色箭头所示的顺序,查找到 B.1 为响应最合适的 view。顺序如下:

UIWindow -> MainView -> View C -> View B -> View B.2 -> View B.1

hitTest:withEvent:

根据上面的遍历算法,我们聊一聊 hitTest:withEvent: 的实现原理。UIWindow 拿到事件后,首先会将事件传递给最靠近的 view,然后调用 hitTest:withEvent: 方法,并按照步骤遵循以下的原则:

  • 根据如下三个属性判断 view & subview 是否接受事件,若下面三个属性设置如下,则方法会自动忽略该 view & subview
    • isUserInteractionEnabled = false
    • isHidden = true
    • alpha <= 0.01
  • 根据 pointInside:withEvent: 判断点是否在当前 view 内
  • 如果在判断是否有 subview,没有 subview 返回自身;有子视图继续遍历

根据上面的原则,我们可以大概推断出 hitTest:withEvent: 的实现原理代码如下:

HitTest

重写 hitTest:withEvent: 场景

  • 改变 touch 区域大小,比较典型的例子就是 tabbar 中间按钮超出高度的情况下仍然需要点击;
  • 制定某一个 view 来响应事件,注意的是尽可能在父 view 中去返回要响应的 view,防止再向下遍历;

事件响应链

ResponsChain

Apps receive and handle events using responder objects. A responder object is any instance of the UIResponder class, and common subclasses include UIView, UIViewController, and UIApplication. Responders receive the raw event data and must either handle the event or forward it to another responder object. When your app receives an event, UIKit automatically directs that event to the most appropriate responder object, known as the first responder.
Unhandled events are passed from responder to responder in the active responder chain, which is the dynamic configuration of your app’s responder objects.

根据官方文档 Using Responders and the Responder Chain to Handle Events | Apple Developer Documentation ,我们发现事件响应链和事件传递完全是相反的过程。最有机会处理事件的就是通过事件传递找到的 first responder,如果没有进行处理,就会沿着事件响应链传递给下一个响应者 nextResponder,一直追溯到最上层 UIApplication。若都没有进行处理,就丢弃事件。

处理事件

1
2
3
4
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

对于触摸事件,系统提供了上面四种方法来处理。如果重写了上述方法,那么事件就会在此中断,并且不再沿着事件响应链进行传递;如果需要继续进行传递,则需要调用 super 方法。

决定第一响应者

我们上面通过事件传递机制寻找 first responder 主要是针对 UIEventTypeTouches 事件,但是 UIKit 决定 first responder 是根据事件类型来的。

| Event type | First responder |
| — | — |
| Touch events | 触摸发生的 view |
| Press events | 聚焦的对象 |
| Shake-motion events | 用户(UIKit)制定的对象 |
| Remote-control events | 用户(UIKit)制定的对象 |
| Editing menu messages | 用户(UIKit)制定的对象 |

与加速计、陀螺仪、磁力仪相关的运动事件,不遵循事件响应链。Core Motion 直接将这些事件传递给指定的对象。更多信息参照 Core Motion Framework

改变响应链

在 UIResponse 类中有一个 nextResponder 属性,可以通过重写该属性来改变事件响应链。很多的系统类就通过这个属性改变了 next responder:

  • UIView,如果 view 的 root view 是 UIViewController,则 next responder 是 UIViewController;否则,next responder 是 super view;
  • UIViewController
    • 如果 view controller 是 UIWindow 的 root view ,那么 next responder 是 UIWindow;
    • 如果 view controller 是被另一个 view controller presented 出来,那么 next responder 是 presenting view controller;
  • UIWindow,next responder 是 UIApplication;
  • UIApplication, next responder 是 app delegate;

事件是怎么被接收的?

我们上面讲了事件传递和事件响应,最后我们再来说说事件是怎么被接收的,接收指的是怎么由一个物理操作变成了可以传递的事件。

背后的力量是 Runloop 机制。具体的过程引用了 深入理解RunLoop | Garan no dou 中的段落:

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考 这里 。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

参考


版权声明



Ivan’s Blog by Ivan Ye is licensed under a Creative Commons BY-NC-ND 4.0 International License.
叶帆创作并维护的叶帆的博客博客采用创作共用保留署名-非商业-禁止演绎4.0国际许可证

本文首发于Ivan’s Blog | 叶帆的博客博客( http://yeziahehe.com ),版权所有,侵权必究。

本文链接:http://yeziahehe.com/2020/01/19/事件响应机制/
支持一下
扫一扫,支持yeziahehe
  • 微信扫一扫
  • 支付宝扫一扫