事件传递和响应链

事件的生命周期

当指尖触碰屏幕的那一刻,一个触摸事件就在系统中生成了。经过IPC进程间通信,事件最终被传递到了合适的应用。在应用内历经峰回路转的奇幻之旅后,最终被释放。大致经过如下图


image.png

系统响应阶段

  1. 手指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。

  2. IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoad进程。

    mach port 进程端口,各进程之间通过它进行通信。
    SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。

  3. SpringBoard进程因接收到触摸事件,触发了主线程runloop的source1事件源的回调。

    此时SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了。

APP响应阶段

  1. APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的runloop被唤醒,触发了source1回调。
  2. source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。
  3. source0回调内部将触摸事件添加到UIApplication对象的事件队列中。事件出队后,UIApplication开始一个寻找最佳响应者的过程,这个过程又称hit-testing,细节将在[寻找事件的最佳响应者]一节阐述。另外,此处开始便是与我们平时开发相关的工作了。
  4. 寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了,关于响应链相关的内容详见[事件的响应及在响应链中的传递]一节。事实上,事件除了被响应者消耗,还能被手势识别器或是target-action模式捕捉并消耗掉。其中涉及对触摸事件的响应优先级,详见[事件的三徒弟UIResponder、UIGestureRecognizer、UIControl]一节。
  5. 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。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不能响应事件,则将事件传递给其他窗口;若窗口能响应事件,则从后往前询问窗口的子视图。

以此类推,如果视图不能响应事件,则将事件传递给同级的上一个子视图;如果能响应,就从后往前遍历当前视图的子视图。

如果当前视图的子视图都不能响应事件,则当前视图就是最合适的响应者。

image.png

一般事件的传递是从父控件传递到子控件的.
例如:
点击了绿色的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

  1. 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已经知道了第一响应者是谁,接下来要做的事情就是:

  • 将事件传递给第一响应者
  • 将事件沿着响应链传递
  1. 将事件传递给第一响应者:

由于第一响应者具有处理事件的最高优先级,因此UIApplication会先将事件传递给它供其处理。首先,UIApplication将事件通过 sendEvent: 传递给事件所属的window,window同样通过 sendEvent: 再将事件传递给hit-tested view,即第一响应者。过程如下:

UIApplication ——> UIWindow ——> hit-tested view

以点击EView视图为例,在EView的 touchesBegan:withEvent:上断点查看调用栈就能看清这一过程:


image.png

从这调用堆栈我们可以看出,UIApplication对于将事件传递给那个UIWindow是很明确的,UIWindow对于将事件传递给哪个视图也是很明确的。因为这些信息都放在了UIEvent的Touch事件里面。

但是这些信息又是什么时候放入到UIEvent内部的呢?

可想而知因为Hit-Testing和SendEvent两者中的UIEvent是同一个UIEvent,所以这应该是在Hit-Testing寻找第一响应者的过程中,填入UIEvent内部的。

  1. 将事件沿着响应链传递:
    因为每个响应者必定都是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。

例如:


image.png

响应者链如下:

如果点击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是如何处理触摸事件的。


image.png
  #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:]

接下来我们一一解释这些现象:

  1. 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就会接手事件的处理。

这里我们可以得出:UIGestureRecognizerUIResponder具有更高的事件响应的优先级

这个结论我们也可以从官方文档中得出:

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中。


image.png

而这里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:]

从日志我们可以看出长按手势回调了两次,我们通过分析两次调用的堆栈:

image.png

image.png

我们可以看出第一次调用是在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];
image.png

即当事件发生时,事件会被发送到控件对象中,然后再由这个控件对象去触发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:]
image.png

从以上信息,我们可以分析:

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:]
image.png

从以上信息,我们又可以得出::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的内部结构,可以看到这样的结果

image.png

从图中我们可以看出,UIControl内部实际上是有一个可变数组(_targetActions)来保存Target-Action,数组中的每个元素是一个UIControlTargetAction对象。UIControlTargetAction类是一个私有类,内部维护

@interface UIControlTargetAction : NSObject {
    SEL _action;
    BOOL _cancelled;
    unsigned int _eventMask;// 事件类型,比如:UIControlEventTouchUpInside
    id _target;
}

这四个变量,UIControl正是依据UIControlTargetAction来对事件进行处理。

五.事件完整响应链

  1. 系统通过 IOKit.framework来处理硬件操作,其中屏幕处理也通过IOKit完成(IOKit可能是注册监听了屏幕输出的端口)
    当用户操作屏幕,IOKit收到屏幕操作,会将这次操作封装为IOHIDEvent对象。通过mach port(IPC进程间通信)将事件转发给SpringBoard来处理。

  2. SpringBoard是iOS系统的桌面程序。SpringBoard收到mach port发过来的事件,唤醒main runloop来处理。

  3. SpringBoard的main runloop将事件交给当前应用程序的source1处理,这时候会唤醒当前应用程序的runloop,当前应用程序的source1会调用__IOHIDEventSystemClientQueueCallback()函数,该函数内部会判断,是否有程序在前台显示,如果有则通过mach port将IOHIDEvent事件转发给这个程序。
    如果前台没有程序在显示,则表明SpringBoard的桌面程序在前台显示,也就是用户在桌面进行了操作。
    __IOHIDEventSystemClientQueueCallback()函数会将事件交给source0处理,source0会调用__UIApplicationHandleEventQueue()函数,函数内部会做具体的处理操作。

  4. 例如用户点击了某个应用程序的icon,会将这个程序启动。
    应用程序接收到SpringBoard传来的消息,会唤醒main runloop并将这个消息交给source1处理,source1调用__IOHIDEventSystemClientQueueCallback()函数,在函数内部会将事件交给source0处理,并调用source0的__UIApplicationHandleEventQueue()函数。
    在__UIApplicationHandleEventQueue()函数中,会将传递过来的IOHIDEvent转换为UIEvent对象。

  5. 在函数内部,将事件放入UIApplication的事件队列,等到处理该事件时,将该事件出队列,UIApplication将事件传递给窗口对象(UIWindow),如果存在多个窗口,则从后往前询问最上层显示的窗口

  6. 窗口UIWindow通过hitTest和pointInside操作,判断是否可以响应事件,如果窗口UIWindow不能响应事件,则将事件传递给其他窗口;若窗口能响应事件,则从后往前询问窗口的子视图。

  7. 以此类推,如果当前视图不能响应事件,则将事件传递给同级的上一个子视图;如果能响应,就从后往前遍历当前视图的子视图。

  8. 如果当前视图的子视图都不能响应事件,则当前视图就是第一响应者。找到第一响应者,事件的传递的响应链也就确定的。

  9. 如果第一响应者非UIControl子类且响应链上也没有绑定手势UIGestureRecognizer;

  10. 那么由于第一响应者具有处理事件的最高优先级,因此UIApplication会先将事件传递给它供其处理。首先,UIApplication将事件通过 sendEvent: 传递给事件所属的window,window同样通过 sendEvent: 再将事件传递给hit-tested view,即第一响应者,第一响应者具有对事件的完全处理权,默认对事件不进行处理,传递给下一个响应者(nextResponder);如果响应链上的对象一直没有处理该事件,则最后会交给UIApplication,如果UIApplication实现代理,会交给UIApplicationDelegate,如果UIApplicationDelegate没处理,则该事件会被丢弃。

  11. 如果第一响应者非UIControl子类但响应链上也绑定了手势识别器UIGestureRecognizer;

  12. UIWindow会将事件先发送给响应链上绑定的手势识别器UIGestureRecognizer,再发送给第一响应者,如果手势识别器能成功识别事件,UIApplication默认会向第一响应者发送cancel响应事件的命令;如果手势识别器未能识别手势,而此时触摸并未结束,则停止向手势识别器发送事件,仅向第一响应者发送事件。如果手势识别器未能识别手势,且此时触摸已经结束,则向第一响应者发送end状态的touch事件,以停止对事件的响应。

  13. 如果第一响应者是自定义的UIControl的子类同时响应链上也绑定了手势识别器UIGestureRecognizer;这种情况跟第一响应者非UIControl子类但响应链上也绑定了手势识别器UIGestureRecognizer`处理逻辑一样;

  14. 如果第一响应者是UIControl的子类且是系统类(UIButton、UISwitch)同时响应链上也绑定了手势识别器UIGestureRecognizer;

  15. 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容