一、本节只记录问题,答案和细节从大神的文章中寻找
二、描述触摸事件的生命周期(从用户触碰屏幕开始)
1. 系统响应阶段
1.1 手指触碰屏幕,屏幕感受到触碰后,将事件交由 IOKit
处理。
1.2 IOKit 将触摸事件封装成IOHIDEvent
对象,并通过 mach port
传递给 SpringBoard
进程。
mach port
是 macOS 的IPC 进程通信
方式,各个进程之间通过它通信。
SpringBoard.app
是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统收到的触摸事件。
1.3 SpringBoard
进程因接收到触摸事件,触发了主线程的 RunLoop
的 source1
事件源回调,会把事件传递给当前屏幕运行的 APP
。(暂不讨论当前屏幕进程就是 Springboard 的情况)
2. APP响应阶段
2.1 Springboard
进程将事件通过 mach port
传递给当前 APP
,会唤醒当前 APP 的 RunLoop
并且触发 source1
的回调。
2.2 source1
的回调又会触发 source0
的回调,source0
将事件封装成 UIEvent 对象
,并且将触摸事件添加到 UIApplication
对象的事件队列
中。
2.3 事件出队列后,开始了寻找最佳响应者
的过程,这个过程又称为 hitTest
过程。
2.4 寻找到最佳响应者
后,接下来 UIApplication
会调用 sendEvent:
将事件分发给最佳响应者
。
2.5 最佳响应者
拿到事件后,可以决定对事件进行独自消化
,也可以选择让事件在响应者链条
中继续传递
。
2.6 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够相应的对象,最终释放。至此,这个触摸事件的使命就算终结了,RunLoop 若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。
三、UITouch 、 UIEvent、UIResponder 分别是什么?
1. UITouch
触摸的起源
- 一个手指一次触摸屏幕,就对应生成一个 UITouch 对象。多个手指同时触摸,生成多个 UITouch 对象。
- 多个手指先后触摸,系统会根据触摸的位置判断是否更新同一个 UITouch 对象。
- 每个 UITouch 对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的视图、窗口等信息。
- 手指离开屏幕一段时间后,确定该 UITouch 对象不会再被更新将被释放。
2. UIEvent
事件的真身
-
触摸
的目的是生成触摸事件
供响应者响应
,一个触摸事件
对于一个 UIEvent 对象,其中 type 属性标识了事件的类型。 -
UIEvent
对象中包含了触发该事件的触摸对象的集合
,因为一个触摸事件
可能是由多个手指同时触摸
产生的。触摸对象集合通过allTouches
属性获取。
3. UIResponder
一切都是为了满足它的野心
每个响应者都是一个 UIResponder 对象,即所有派生自 UIResponder 的对象,本身都具备响应事件的能力。因此以下类的实例都是响应者:
UIView
、UIViewController
、UIWindow
、AppDelegate
响应者之所以能响应事件,因为其提供了 4 个处理触摸事件的方法:
//手指触碰屏幕,触摸开始
- (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;
四、寻找事件的最佳响应者(hit-tested View)
1. 如何寻找最佳响应者?
-
自上而下
:(父视图 → 子视图) -
从后往前
(同级视图,优先询问后添加的视图) - 若视图没有能响应的子视图了,则自身就是最合适的响应者。
2. 如何判断视图能否响应?
通过
hitTest:withEvent:
方法的返回值来判断:
- 若当前视图无法响应事件,则返回 nil
- 若当前视图可以响应事件,当无子视图可以响应事件,则返回自身作为当前视图层中的事件响应者
- 若当前视图可以响应事件,同时有子视图可以响应,继续调用子视图的
hitTest:withEvent:
,直到找到最佳响应者
。
3. hitTest:withEvent:
的内部实现?
- 先判断视图
userInteractionEnabled
hidden
alpha
三个属性是否符合 - 再判断点击事件的触发位置是否在视图之内
- 再从后往前调用子视图的
hitTest:withEvent:
方法 - 最后返回找到的
最佳响应者
五、事件的响应以及在响应链中的传递
1. 找到最佳响应者之后,事件是如何传递给最佳响应者的?
- 首先,
UIApplication
将事件通过sendEvent:
传递给事件所属的window
- 然后,
window
同样通过sendEvent:
再将事件传递给最佳响应者
2. 什么是响应链?
最佳响应者
首先接收到事件,然后便拥有了对事件的绝对控制权
。它可以选择独吞
这个事件,也可以将这个事件往下传递
给其他响应者,这个由响应者构成的视图链
就称之为响应链
。
六、总结
- 触摸发生时,系统内核生成触摸事件,先由 IOKit 处理封装成 IOHIDEvent 对象,通过 IPC 传递给系统进程 SpringBoard,而后再传递给前台 APP 处理。
- 事件传递到 APP 内部时被封装成开发者可见的 UIEvent 对象,经过 hit-test 寻找第一响应者,而后由 Window 对象将事件传递给 hit-tested view,并开始在响应链上的传递。
- UIResponder、UIGestureRecognizer、UIControl,笼统地讲,事件响应优先级依次递增。
七、补充
1. 如何通过给定的 view,拿到它的控制器 Controller ?
- (UIViewController *)viewController
{
//获取当前view的superView对应的控制器
UIResponder *next = [self nextResponder];
do {
if ([next isKindOfClass:[UIViewController class]]) {
return (UIViewController *)next;
}
next = [next nextResponder];
} while (next != nil);
return nil;
}