事件的生命周期
当指尖触碰屏幕的那一刻,一个触摸事件就在系统中生成了。经过IPC进程间通信,事件最终被传递到了合适的应用。在应用内历经峰回路转的奇幻之旅后,最终被释放。大致经过如下图
系统响应阶段
手指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。
-
IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoad进程。
mach port 进程端口,各进程之间通过它进行通信。
SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。 -
SpringBoard进程因接收到触摸事件,触发了主线程runloop的source1事件源的回调。
此时SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了。
APP响应阶段
- APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的runloop被唤醒,触发了source1回调。
- source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。
- source0回调内部将触摸事件添加到UIApplication对象的事件队列中。事件出队后,UIApplication开始一个寻找最佳响应者的过程,这个过程又称hit-testing,细节将在[寻找事件的最佳响应者]一节阐述。另外,此处开始便是与我们平时开发相关的工作了。
- 寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了,关于响应链相关的内容详见[事件的响应及在响应链中的传递]一节。事实上,事件除了被响应者消耗,还能被手势识别器或是target-action模式捕捉并消耗掉。其中涉及对触摸事件的响应优先级,详见[事件的三徒弟UIResponder、UIGestureRecognizer、UIControl]一节。
- 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒
一. 触摸、事件、响应者
1. UITouch
源起触摸
一个手指一次触摸屏幕,就对应生成一个UITouch对象。多个手指同时触摸屏幕,生成多个UITouch对象。
多个手指先后触摸,系统会根据触摸的位置判断是否更新同一个UITouch对象。若两个手指一前一后触摸同一个位置(即双击),那么第一次触摸时生成一个UITouch对象,第二次触摸会更新这个UITouch对象,这是该UITouch对象的Tap Count属性值从1变成2,若两个手指一前一后触摸的位置不同,将会生成两个UITouch对象,两者之间没有联系。
每个UITouch对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的视图、窗口等信息。
// 触摸的各个阶段状态
// 例如当手指移动时,会更新phase属性到UITouchPhaseMoved;
// 手指离屏后,更新到UITouchPhaseEnded
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
手指离开屏幕一段时间后,确定该UITouch对象不会再被更新,就释放。
2. UIEvent
事件的真身
触摸的目的是生成触摸事件供响应者响应,一个触摸事件对应一个UIEvent对象,其中的type属性标识了事件的类型,事件有如下几种类型:
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),
};
这里我们所说的事件具体指的是触摸事件。
UIEvent对象中包含了触发该对象的触摸对象集合,因为一个触摸事件可能是由多个手指同时触摸产生的。触摸对象集合通过allTouches属性获取。
3.UIResponder
UIResponder是iOS中用于处理用户事件的API,可以处理触摸事件、按压事件(3D touch)、远程控制事件、硬件运动事件。可以通过touchesBegan、pressesBegan、motionBegan、remoteControlReceivedWithEvent等方法,获取到对应的回调消息。UIResponder不只用来接收事件,还可以处理和传递对应的事件,如果当前响应者不能处理,则转发给其他合适的响应者处理。
应用程序通过响应者来接收和处理事件,响应者可以是继承自UIResponder的任何子类,例如UIView、UIViewController、UIApplication等。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者。
第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIResponder的nextResponder决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。
在iOS中不是任何对象都能处理事件, 只有继承了UIResponder的对象才能接收并处理事件,我们称为响应者对象.UIApplication,UIViewController,UIView都继承自UIResponder,因此他们都是响应者对象, 都能够接收并处理事件.继承自UIResponder的类能处理事件是由于UIResponder内部提供了以下方法:
触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
传感器事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
二. iOS中事件的产生和传递
应用程序接收到触摸事件后,将事件放入UIApplication的事件队列,等到处理该事件时,将该事件出队列,UIApplication将事件传递给窗口对象(UIWindow),如果存在多个窗口,则优先询问后显示的窗口
如果窗口UIWindow不能响应事件,则将事件传递给其他窗口;若窗口能响应事件,则从后往前询问窗口的子视图。
以此类推,如果视图不能响应事件,则将事件传递给同级的上一个子视图;如果能响应,就从后往前遍历当前视图的子视图。
如果当前视图的子视图都不能响应事件,则当前视图就是最合适的响应者。
一般事件的传递是从父控件传递到子控件的.
例如:
点击了绿色的View,传递过程如下:UIApplication->Window->白色View->绿色View
点击蓝色的View,传递过程如下:UIApplication->Window->白色View->橙色View->蓝色View
如果父控件接受不到触摸事件,那么子控件就不可能接收到触摸事件.
如何寻找最合适的view?
应用如何找到最合适的控件来处理事件?有以下准则:
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件;
2.触摸点是否在自己身上;
3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素);
4.如果没有符合条件的子控件,那么就认为自己最合适处理.
详述:
1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上;
2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view);
3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上);
4.如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。
在事件传递寻找最合适的View时,底层到底干了哪些事?
寻找合适的View用到两个重要方法:
hitTest:withEvent:
pointInside
1.hitTest:withEvent:方法
什么时候调用?
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法寻找合适的View
- pointInside方法
作用
寻找并返回最合适的view(能够响应事件的那个最合适的view)
注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,
事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
底层具体实现如下 :
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判断当前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判断点在不在当前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
// 把当前控件上的坐标系转换成子控件上的坐标系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 寻找到最合适的view
return fitView;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}
三. iOS中事件的响应
经过Hit-Testing的过程后,UIApplication已经知道了第一响应者是谁,接下来要做的事情就是:
- 将事件传递给第一响应者
- 将事件沿着响应链传递
- 将事件传递给第一响应者:
由于第一响应者具有处理事件的最高优先级,因此UIApplication会先将事件传递给它供其处理。首先,UIApplication将事件通过 sendEvent: 传递给事件所属的window,window同样通过 sendEvent: 再将事件传递给hit-tested view,即第一响应者。过程如下:
UIApplication ——> UIWindow ——> hit-tested view
以点击EView视图为例,在EView的 touchesBegan:withEvent:上断点查看调用栈就能看清这一过程:
从这调用堆栈我们可以看出,UIApplication对于将事件传递给那个UIWindow是很明确的,UIWindow对于将事件传递给哪个视图也是很明确的。因为这些信息都放在了UIEvent的Touch事件里面。
但是这些信息又是什么时候放入到UIEvent内部的呢?
可想而知因为Hit-Testing和SendEvent两者中的UIEvent是同一个UIEvent,所以这应该是在Hit-Testing寻找第一响应者的过程中,填入UIEvent内部的。
- 将事件沿着响应链传递:
因为每个响应者必定都是UIResponder对象,通过4个响应触摸事件的方法来响应事件。每个UIResponder对象默认都已经实现了这4个方法,但是默认不对触摸事件做任何处理,单纯只是将事件沿着响应链传递。若要截获事件进行自定义的响应操作,就要重写相关的方法。
第一响应者接收到触摸事件后,就具有对触摸事件的处理权,它可以选择自己处理这个事件,也可以将这个事件沿着响应链传递给下一个响应者,这个由响应者之间构成的视图链就称之为响应链。
需要注意的是,上一节所说的事件传递的目的是为寻找事件的最佳响应者,是自下而上的传递;这里的事件传递目的是响应者做出对事件的响应,这个过程是自上而下的。前者为“寻找”,后者为“响应”。
响应者对于事件的操作方式:
响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent:方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。
响应者对于接收到的事件有3种操作:
不拦截,默认操作
事件会自动沿着默认的响应链往下传递
拦截,不再往下分发事件
重写 touchesBegan:withEvent:进行事件处理,不调用父类的 touchesBegan:withEvent:
拦截,继续往下分发事件
重写 touchesBegan:withEvent:进行事件处理,同时调用父类的 touchesBegan:withEvent:将事件往下传递
响应链中的事件传递规则:
每一个响应者对象(UIResponder对象)都有一个nextResponder方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的第一响应者确定了,这个事件所处的响应链就确定了。
对于响应者对象,默认的 nextResponder 实现如下:
UIView
若视图是控制器的根视图,则其nextResponder为控制器对象;否则,其nextResponder为父视图。
UIViewController
若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。
UIWindow
nextResponder为UIApplication对象。
UIApplication
若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。
例如:
响应者链如下:
如果点击UITextField后其会成为第一响应者。
如果textField未处理事件,则会将事件传递给下一级响应者链,也就是其父视图。
父视图未处理事件则继续向下传递,也就是UIViewController的View。
如果控制器的View未处理事件,则会交给控制器处理。
控制器未处理则会交给UIWindow。
然后会交给UIApplication。
最后交给UIApplicationDelegate,如果其未处理则丢弃事件
UITextField ——> UIView ——> UIView ——> UIViewController
——> UIWindow ——> UIApplication ——> UIApplicationDelegation
图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,
则其nextResponder为UIViewController对象;
若是直接add在UIWindow上的,则其nextResponder为UIWindow对象。
可以用以下方式打印一个响应链中的每一个响应对象,
在第一响应者的 touchBegin:withEvent: 方法中调用即可(别忘了调用父类的方法)
例如某View的touch Begin:WithEvent:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
[self printResponderChain];
[super touchesBegan:touches withEvent:event];
}
- (void)printResponderChain {
UIResponder *responder = self;
printf("%s",[NSStringFromClass([responder class]) UTF8String]);
while (responder.nextResponder) {
responder = responder.nextResponder;
printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]);
}
}
四.UIGestureRecognizer、UIControl
上面我们讲述了UIResponder响应触摸事件的过程,但除了UIResponder之外,UIGestureRecognizer、UIControl同样具备对事件的处理能力。
以下将通过结合具体的示例来讲解UIGestureRecognizer和UIControl是如何处理触摸事件的。
#pragma mark -------------------------- Life Circle
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"分类";
// view tap
FJFTapView *tmpContainerView = [[FJFTapView alloc] initWithFrame:CGRectMake(50, 80, 260, 300)];
tmpContainerView.backgroundColor = [UIColor redColor];
FJFTapGestureRecognizer *tapGesture = [[FJFTapGestureRecognizer alloc] initWithTarget:self action:@selector(viewTap:)];
[tmpContainerView addGestureRecognizer:tapGesture];
[self.view addSubview:tmpContainerView];
// view longPress
FJFLongPressView *tmpLongPressView = [[FJFLongPressView alloc] initWithFrame:CGRectMake(50, 400, 260, 200)];
tmpLongPressView.backgroundColor = [UIColor grayColor];
FJFLongPressGestureRecognizer *longPressGesture = [[FJFLongPressGestureRecognizer alloc] initWithTarget:self action:@selector(viewlongPress:)];
[tmpLongPressView addGestureRecognizer:longPressGesture];
[self.view addSubview:tmpLongPressView];
// button
FJFButton *tmpButton = [[FJFButton alloc] initWithFrame:CGRectMake(100, 50, 120, 80)];
tmpButton.backgroundColor = [UIColor greenColor];
[tmpButton setTitle:@"UIButton" forState:UIControlStateNormal];
[tmpButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[tmpButton addTarget:self action:@selector(tmpButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[tmpContainerView addSubview:tmpButton];
// imageControl
FJFImageControl *imageControl = [[FJFImageControl alloc] initWithFrame:CGRectMake(100, 150, 120, 80) title:@"imageControl" iconImageName:@"ic_red_box.png"];
imageControl.backgroundColor = [UIColor blueColor];
[imageControl addTarget:self action:@selector(imageControlTouch:) forControlEvents:UIControlEventTouchUpInside];
[tmpContainerView addSubview:imageControl];
}
#pragma mark -------------------------- Response Event
// tap
- (void)viewTap:(UITapGestureRecognizer *)tap {
NSLog(@"%s", __FUNCTION__);
}
// longPress
- (void)viewlongPress:(UILongPressGestureRecognizer *)longPress {
NSLog(@"%s", __FUNCTION__);
}
// buttonClicked
- (void)tmpButtonClicked:(UIButton *)sender {
NSLog(@"%s", __FUNCTION__);
}
// controlTouch
- (void)imageControlTouch:(FJFImageControl *)imageControl {
NSLog(@"%s", __FUNCTION__);
}
如代码所示:
FJFTapView 添加了继承自UITapGestureRecognizer的FJFTapGestureRecognizer 单击手势
FJFLongPressView 添加了继承自UILongPressGestureRecognizer的FJFLongPressGestureRecognizer 长按手势
UIButton 添加 点击事件
FJFImageControl 继承自UIControl,也添加了点击事件,
且UIButton和FJFImageControl都是FJFTapView的子视图。
观察各种情况的日志:
1.点击FJFTapView:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapView touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFTapView touchesCancelled:withEvent:]
2.长按FJFLongPressView:
[FJFLongPressGestureRecognizer touchesBegan:withEvent:]
[FJFLongPressView touchesBegan:withEvent:]
[FJFThreeViewController viewlongPress:]
[FJFLongPressView touchesCancelled:withEvent:]
[FJFLongPressGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewlongPress:]
3.点击UIButton:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFButton touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFButton touchesEnded:withEvent:]
[FJFThreeViewController tmpButtonClicked:]
4.点击FJFImageControl:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFImageControl touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFImageControl touchesCancelled:withEvent:]
接下来我们一一解释这些现象:
- UIGestureRecognizer:
手势分为离散型手势(discrete gestures)和持续型手势(continuous gesture)。系统提供的离散型手势包括点按手势(UITapGestureRecognizer)和轻扫手势(UISwipeGestureRecognizer),其余均为持续型手势。
两者主要区别在于状态变化过程:
离散型:
识别成功:Possible —> Recognized
识别失败:Possible —> Failed
持续型:
完整识别:Possible —> Began —> [Changed] —> Ended
不完整识别:Possible —> Began —> [Changed] —> Cancel
A. 离散型手势
从点击FJFTapView
的日志可以分析:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapView touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFTapView touchesCancelled:withEvent:]
UIWindow
在将事件传递给第一响应者FJFTapView
之前,先将事件传递给相关的手势识别器FJFTapGestureRecognizer
,若手势成功识别事件,就会取消
第一响应者FJFTapView
对事件的响
应;若手势没能识别事件,
第一响应者FJFTapView
就会接手事件的处理。
这里我们可以得出:
UIGestureRecognizer
比UIResponder
具有更高的事件响应的优先级
这个结论我们也可以从官方文档中得出:
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.
还有一点需要注意的是:
UIGestureRecognizer对事件的响应也是通过touch相关的4个方法来实现的,而这4个方法声明在UIGestureRecognizerSubclass.h中。
而这里UIWindow之所以知道要把事件传递给哪些手势识别器,主要还是通过UIEvent里面的gestureRecognizers数组来获取的,而数组里面的手势识别器是在Hit-Test View寻找第一响应者过程中填充的。
因此我们可以分析日志:
UIWindow 先将事件传递给gestureRecognizers数组里的手势识别器,然后再传递给第一响应者FJFTapView.
因为手势识别器识别事件,需要一定时间,因此FJFTapView先调用了touchesBegan,这是因为FJFTapGestureRecognizer成功识别了事件,UIApplication就会取消FJFTapView对事件的响应。
B. 持续型手势
从点击FJFLongPressView日志分析:
[FJFLongPressGestureRecognizer touchesBegan:withEvent:]
[FJFLongPressView touchesBegan:withEvent:]
[FJFThreeViewController viewlongPress:]
[FJFLongPressView touchesCancelled:withEvent:]
[FJFLongPressGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewlongPress:]
从日志我们可以看出长按手势回调了两次,我们通过分析两次调用的堆栈:
我们可以看出第一次调用是在runloop中通知监听的手势识别器的观察者,来通知长按手势识别器对长按事件进行响应,此时手势识别器的state为UIGestureRecognizerStateBegan。
第二次调用是UIWindow 先将事件传递给UIEvent的gestureRecognizers数组里的手势识别器,然后长按手势识别器FJFLongPressGestureRecognizer识别成功进行回调,此时手势识别器的state为UIGestureRecognizerStateEnded。
这里的调用逻辑其实跟单击手势识别器FJFTapGestureRecognizer相似,主要区别在于长按手势识别器FJFLongPressGestureRecognizer调用了两次。
C. 总结
当触摸发生或者触摸的状态发生变化时,UIWindow都会传递事件寻求响应。
UIWindow先将触摸事件传递给响应链上绑定的手势识别器,再发送给触摸对象对应的第一响应者。
手势识别器识别手势期间,若触摸对象的触摸状态发生变化,事件都是先发送给手势识别器,再发送给第一响应者。
手势识别器如果成功识别手势,则通知UIApplication取消第一响应者对于事件的响应,并停止向第一响应者发送事件。
如果手势识别器未能识别手势,而此时触摸并未结束,则停止向手势识别器发送事件,仅向第一响应者发送事件。
如果手势识别器未能识别手势,且此时触摸已经结束,则向第一响应者发送end状态的touch事件,以停止对事件的响应。
D. 拓展
手势识别器的3个属性:
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;
a. cancelsTouchesInView:
默认为YES。表示当手势识别器成功识别了手势之后,会通知Application取消响应链对事件的响应,并不再传递事件给第一响应者。若设置成NO,表示手势识别成功后不取消响应链对事件的响应,事件依旧会传递给第一响应者。
以点击FJFTapView为例,将tapGesture.cancelsTouchesInView = NO;输出日志如下:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapView touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFTapView touchesEnded:withEvent:]
从日志我们可以看出,即便FJFTapGestureRecognizer识别了点击手势,UIApplication也依旧将事件发送给FJFTapView.
b. delaysTouchesBegan:
默认为NO。默认情况下手势识别器在识别手势期间,当触摸状态发生改变时,Application都会将事件传递给手势识别器和第一响应者;若设置成YES,则表示手势识别器在识别手势期间,截断事件,即不会将事件发送给第一响应者。
以点击FJFTapView为例,将tapGesture.delaysTouchesBegan = YES;输出日志如下:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
从日志可以看出,手势识别器识别手势期间,事件不会传递给FJFTapView,因此FJFTapView的touchesBegan:withEvent:不会被调用;而手势识别器成功识别手势后,独吞了事件,不会再传递给FJFTapView,因此只打印手势识别器识别成功后手势的绑定函数。
c. delaysTouchesEnded:
默认为YES。当手势识别失败时,若此时触摸已经结束,会延迟一小段时间(0.15s)再调用响应者的touchesEnded:withEvent:;若设置成NO,则在手势识别失败时会立即通知Application发送状态为end的touch事件给第一响应者以调用 touchesEnded:withEvent:结束事件响应。
2.UIControl
UIControl是系统提供的能够以target-action模式处理触摸事件的控件,iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子类。
值得注意的是,UIConotrol是UIView的子类,因此本身也具备UIResponder应有的身份。
UIControl作为控件类的基类,它是一个抽象基类,我们不能直接使用UIControl类来实例化控件,它只是为控件子类定义一些通用的接口,并提供一些基础实现,以在事件发生时,预处理这些消息并将它们发送到指定目标对象上。
关于UIControl,此处介绍两点:
- target-action机制
- 触摸事件优先级
Target-Action机制
Target-action是一种设计模式,直译过来就是”目标-行为”。当我们通过代码为一个按钮添加一个点击事件时,通常是如下处理:
[button addTarget:self action:@selector(tapButton:) forControlEvents:UIControlEventTouchUpInside];
即当事件发生时,事件会被发送到控件对象中,然后再由这个控件对象去触发target对象上的action行为,来最终处理事件。因此,Target-Action机制由两部分组成:即目标对象Target和行为Selector。目标对象指定最终处理事件的对象,而行为Selector则是处理事件的方法。
UIControl作为能够响应事件的控件,必然也需要待事件交互符合条件时才去响应,因此也会跟踪事件发生的过程。不同于UIResponder以及UIGestureRecognizer通过touches系列方法跟踪,UIControl有其独特的跟踪方式:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
return YES;
}
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
}
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
}
这4个方法和UIResponder的那4个方法几乎吻合,只不过UIControl只能接收单点触控,因此接收的参数是单个UITouch对象。这几个方法的职能也和UIResponder一致,用来跟踪触摸的开始、滑动、结束、取消。不过,UIControl本身也是UIResponder,因此同样有touches系列的4个方法。事实上,UIControl的 Tracking 系列方法是在touch 系列方法内部调用的。比如 beginTrackingWithTouch 是在 touchesBegan 方法内部调用的, 因此它虽然也是UIResponder,但touches 系列方法的默认实现和UIResponder本类还是有区别的。
我们来分析下FJFButton的日志输出以及调用堆栈:
日志输出:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFButton touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFButton touchesEnded:withEvent:]
[FJFThreeViewController tmpButtonClicked:]
从以上信息,我们可以分析:
UIWindow 首先将事件传递给响应链上绑定的手势识别器FJFTapGestureRecognizer,再传递给第一响应者FJFButton
手势识别器FJFTapGestureRecognizer和第一响应者FJFButton分别调用touch相关方法对事件进行识别,
最终第一响应者FJFButton对事件进行响应调用 sendAction:to:forEvent:将target、action以及event对象发送给UIApplication,UIApplication对象再通过 sendAction:to:from:forEvent:向target发送action。
通过这个结果,我们会疑问:UIControl比其父视图上的手势识别器具有更高的事件响应优先级?
接下来我们看下继承自UIControl的FJFImageControl的日志和调用堆栈:
日志输出:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFImageControl touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFImageControl touchesCancelled:withEvent:]
从以上信息,我们又可以得出::UIControl比其父视图上的手势识别器的优先级来的低?
经验证系统提供的有默认action操作的UIControl,例如UIbutton、UISwitch等的单击,UIControl的响应优先级比手势识别器高,而对于自定义的UIControl,响应的优先级比手势低。
我们在一个含有 tap gesture 的 view 上添加一个 UIButton ,点击按钮时响应的是 UIButton 。为什么呢?事实上,这个 tap gesture 并没有获得响应权。问题出在 UIGestureRecognizerDelegate 的 [gestureRecognizerShouldBegin:] 阶段。在 [gestureRecognizerShouldBegin:] 阶段首先被调用的是被触摸视图的 [gestureRecognizerShouldBegin:] 方法,其参数是我们的 tap gesture 。而 UIButton 的 [gestureRecognizerShouldBegin:] 实现中,指定对非添加在自己身上的 tap gesture ,返回 false ,即不可响应。所以点击最终响应的是 UIButton ,其下面视图的 tap gesture 得不到响应。如果读者重写 UIButton 的 [gestureRecognizerShouldBegin:] 方法,让其返回 true ,会发现点击 UIButton 时, UIButton 没有响应,响应的却是其父视图的 tap gesture 。这也说明了 UIGesture 比 UIControl 的优先级更高。另外, UIButton 的 [gestureRecognizerShouldBegin:] 实现中没有对其他手势做限制,即返回的 true ,所以你在 UIButton 上双击、滑动时,这些手势都能得到其父视图的识别。
Target-Action的管理:
UIControl通过addTarget方法和removeTarget方法来添加和删除Target-Action的操作。
// 添加
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
// 删除
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
如果想获取控件对象所有相关的target对象,则可以调用allTargets方法,该方法返回一个集合。集合中可能包含NSNull对象,表示至少有一个nil目标对象。
而如果想获取某个target对象及事件相关的所有action,则可以调用actionsForTarget:forControlEvent:方法。
不过,这些都是UIControl开放出来的接口。我们还是想要探究一下,UIControl是如何去管理Target-Action的呢?
实际上,我们在程序某个合适的位置打个断点来观察UIControl的内部结构,可以看到这样的结果
从图中我们可以看出,UIControl内部实际上是有一个可变数组(_targetActions)来保存Target-Action,数组中的每个元素是一个UIControlTargetAction对象。UIControlTargetAction类是一个私有类,内部维护
@interface UIControlTargetAction : NSObject {
SEL _action;
BOOL _cancelled;
unsigned int _eventMask;// 事件类型,比如:UIControlEventTouchUpInside
id _target;
}
这四个变量,UIControl正是依据UIControlTargetAction来对事件进行处理。
五.事件完整响应链
系统通过 IOKit.framework来处理硬件操作,其中屏幕处理也通过IOKit完成(IOKit可能是注册监听了屏幕输出的端口)
当用户操作屏幕,IOKit收到屏幕操作,会将这次操作封装为IOHIDEvent对象。通过mach port(IPC进程间通信)将事件转发给SpringBoard来处理。SpringBoard是iOS系统的桌面程序。SpringBoard收到mach port发过来的事件,唤醒main runloop来处理。
SpringBoard的main runloop将事件交给当前应用程序的source1处理,这时候会唤醒当前应用程序的runloop,当前应用程序的source1会调用__IOHIDEventSystemClientQueueCallback()函数,该函数内部会判断,是否有程序在前台显示,如果有则通过mach port将IOHIDEvent事件转发给这个程序。
如果前台没有程序在显示,则表明SpringBoard的桌面程序在前台显示,也就是用户在桌面进行了操作。
__IOHIDEventSystemClientQueueCallback()函数会将事件交给source0处理,source0会调用__UIApplicationHandleEventQueue()函数,函数内部会做具体的处理操作。例如用户点击了某个应用程序的icon,会将这个程序启动。
应用程序接收到SpringBoard传来的消息,会唤醒main runloop并将这个消息交给source1处理,source1调用__IOHIDEventSystemClientQueueCallback()函数,在函数内部会将事件交给source0处理,并调用source0的__UIApplicationHandleEventQueue()函数。
在__UIApplicationHandleEventQueue()函数中,会将传递过来的IOHIDEvent转换为UIEvent对象。在函数内部,将事件放入UIApplication的事件队列,等到处理该事件时,将该事件出队列,UIApplication将事件传递给窗口对象(UIWindow),如果存在多个窗口,则从后往前询问最上层显示的窗口
窗口UIWindow通过hitTest和pointInside操作,判断是否可以响应事件,如果窗口UIWindow不能响应事件,则将事件传递给其他窗口;若窗口能响应事件,则从后往前询问窗口的子视图。
以此类推,如果当前视图不能响应事件,则将事件传递给同级的上一个子视图;如果能响应,就从后往前遍历当前视图的子视图。
如果当前视图的子视图都不能响应事件,则当前视图就是第一响应者。找到第一响应者,事件的传递的响应链也就确定的。
如果第一响应者非UIControl子类且响应链上也没有绑定手势UIGestureRecognizer;
那么由于第一响应者具有处理事件的最高优先级,因此UIApplication会先将事件传递给它供其处理。首先,UIApplication将事件通过 sendEvent: 传递给事件所属的window,window同样通过 sendEvent: 再将事件传递给hit-tested view,即第一响应者,第一响应者具有对事件的完全处理权,默认对事件不进行处理,传递给下一个响应者(nextResponder);如果响应链上的对象一直没有处理该事件,则最后会交给UIApplication,如果UIApplication实现代理,会交给UIApplicationDelegate,如果UIApplicationDelegate没处理,则该事件会被丢弃。
如果第一响应者非UIControl子类但响应链上也绑定了手势识别器UIGestureRecognizer;
UIWindow会将事件先发送给响应链上绑定的手势识别器UIGestureRecognizer,再发送给第一响应者,如果手势识别器能成功识别事件,UIApplication默认会向第一响应者发送cancel响应事件的命令;如果手势识别器未能识别手势,而此时触摸并未结束,则停止向手势识别器发送事件,仅向第一响应者发送事件。如果手势识别器未能识别手势,且此时触摸已经结束,则向第一响应者发送end状态的touch事件,以停止对事件的响应。
如果第一响应者是自定义的UIControl的子类同时响应链上也绑定了手势识别器UIGestureRecognizer;这种情况跟第一响应者非UIControl子类但响应链上也绑定了手势识别器UIGestureRecognizer`处理逻辑一样;
如果第一响应者是UIControl的子类且是系统类(UIButton、UISwitch)同时响应链上也绑定了手势识别器UIGestureRecognizer;
UIWindow会将事件先发送给响应链上绑定的手势识别器UIGestureRecognizer,再发送给第一响应者,如果第一响应者能响应事件,UIControl调用调用sendAction:to:forEvent:将target、action以及event对象发送给UIApplication,UIApplication对象再通过 sendAction:to:from:forEvent:向target发送action。
参考链接:
https://www.jianshu.com/p/df86508e2811
https://www.jianshu.com/p/c294d1bd963d
https://www.jianshu.com/p/c294d1bd963d