一、本节只记录问题,答案和细节从大神的文章中寻找
二、描述触摸事件的生命周期(从用户触碰屏幕开始)
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: 的内部实现?
- 先判断视图
userInteractionEnabledhiddenalpha三个属性是否符合 - 再判断点击事件的触发位置是否在视图之内
- 再从后往前调用子视图的
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;
}