理解iOS事件传递与响应机制

导语

说说 iOS 中关于事件是如何传递与响应的。

因素 iOS 关键词
时间 点击/长按/摇一摇/音乐暂停/播放时
地点 响应者 UIButton/UIView 事件派发 & 响应者链条
人物 用户/运行循环 NSRunLoop & UIApplication
事件 UITapGestureRecognizer/UITouchUpInside UIEvent
如何 执行的具体操作 外部业务实现

环境 & 工具

macOS Sierra 10.12.3
Xcode 8.2.1
chisel

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#import "ViewController.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// 手势
UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 100, 100, 100)];
v.backgroundColor = [UIColor redColor];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
[v addGestureRecognizer:tap];
[self.view addSubview:v];

// 点击
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 200, 100, 30)];
btn.backgroundColor = [UIColor grayColor];
[btn setTitle:@"测试" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];

// Timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:true];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

// CADisplay
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkAction:)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
link.preferredFramesPerSecond = 6;
}

- (void)btnAction:(UIButton *)btn {
NSLog(@"点击事件");
}

- (void)tapAction:(UITapGestureRecognizer *)tap {
NSLog(@"触摸事件");
}

- (void)timerAction:(NSTimer *)timer {
NSLog(@"timer事件");
}

- (void)displayLinkAction:(CADisplayLink *)link {
NSLog(@"displayLink事件");
}

@end

调用堆栈

类型 截图
点击
手势
Timer
CADisplayLink

小结

各种类型的事件都是由 RunLoop 接收。触屏事件由 UIApplication 通过队列的方式
进行派发。

相关的类: UIEvent

UIEvent 是事件派发中的基本单元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIEvent : NSObject

@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);

@property(nonatomic,readonly) NSTimeInterval timestamp;

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
#else
- (nullable NSSet <UITouch *> *)allTouches;
#endif
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS(3_2);

// An array of auxiliary UITouch’s for the touch events that did not get delivered for a given main touch. This also includes an auxiliary version of the main touch itself.
- (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);

// An array of auxiliary UITouch’s for touch events that are predicted to occur for a given main touch. These predictions may not exactly match the real behavior of the touch as it moves, so they should be interpreted as an estimate.
- (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);

@end

NS_ASSUME_NONNULL_END

触屏事件

事件类型

事件驱动应用程序的主要职责是处理用户事件,即由鼠标,键盘,跟踪器和平板电脑等设备生成的事件。

iOS 应用中相应的成了触屏、加速计、远程控制等事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif

...

// 触屏事件 (Multitouch)
- (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;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

...

// 加速器事件 (Motion Events)
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);

// 远程事件 (Remote Control Events)
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);

@end

响应者

应用程序使用响应者对象接收和处理事件。响应者对象是 UIResponder 类的任何实例,常见的子类包括 UIViewUIViewControllerUIApplicationUIKit 自动管理大多数响应者相关的行为,包括事件如何从一个响应者传递到下一个。但是,您可以修改默认行为来更改事件在应用程序中的传送方式。

响应者链条

在当前场景下的 btn 的响应者链条如下

1
2
3
4
5
6
7
(lldb) presponder btn
<UIButton: 0x115e0fea0; frame = (0 200; 100 30); opaque = NO; layer = <CALayer: 0x1702214a0>>
| <UIView: 0x115d07820; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x17403df00>>
| | <ViewController: 0x115e0d720>
| | | <UIWindow: 0x115e0de20; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x170245d90>; layer = <UIWindowLayer: 0x170220e00>>
| | | | <UIApplication: 0x115e007f0>
| | | | | <AppDelegate: 0x17403b6a0>

通过 UIApplication.h 文件中的注释与下面 lldb 调试的结果

action
A selector identifying an action method. See the discussion for information on the permitted selector forms.
target
The object to receive the action message. If target is nil, the app sends the message to the first responder, from whence it progresses up the responder chain until it is handled.
sender
The object that is sending the action message. The default sender is the UIControl object that invokes this method.
event
A UIEvent object that encapsulates information about the event originating the action message.

比较容易推断出

描述
sendAction btnAction:
to ViewController
from UIButton
forEvent UITounesEvent

UIView 不能接收触屏事件的三种情况:

  1. 不接受用户交互:userInteractionEnabled = NO;
  2. 隐藏:hidden = YES;
  3. 透明:alpha = 0.0~0.01

事件分发

  1. 发生触摸事件后,系统会将该事件加入到一个由 UIApplication 管理的队列事件中(从上面的调用堆栈中可知)。
  2. UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow)
  3. 主窗口 (keyWindow) 对象首先会使用 hitTest:withEvent: 方法寻找此次 Touch 操作初始点所在的视图(View),即需要将触摸事件传递给最合适的视图,这个过程称之为 hit-test view

应用如何找到最合适的控件来处理事件?有以下准则

  1. 首先判断主窗口(keyWindow)自己是否能接受触摸事件
  2. 触摸点是否在自己身上
  3. 从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)
  4. 如果没有符合条件的子控件,那么就认为自己最合适处理
    注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。

事件分发 && 响应者链条都是为了找出合适的响应者

小结

hit-test view 确定最合适的视图,若该视图如果不能响应该事件时,可通过响应者链条找到合适的响应者。

在当前场景下, UIButton 作为第一响应者能响应当前的点击事件,触发 UIResponser 协议的 touchesEnded:withEvent 代理方法,最后作为 sender 参数传递给 ViewControllerbtnAction: 方法。

手势

UIView 中有

1
2
3
4
5
6
7
@interface UIView (UIViewGestureRecognizers)

@property(nullable, nonatomic,copy) NSArray<__kindof UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);

- (void)addGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer NS_AVAILABLE_IOS(3_2);
- (void)removeGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer NS_AVAILABLE_IOS(3_2);
...

The gesture-recognizer objects currently attached to the view.

gestureRecognizers 保存着与当前的视图对象相关联的 UIGestureRecognizer 类型对象

为视图添加手势

为视图添加 addTarget:Action:,将 响应者链条 部分稍作处理可得

小结

无论是用 addTarget:Action: 还是将手势添加到视图对象上,最终都是创建了 UIEvent 这个事件单元对象。

手势相比触碰事件的好处是可以直接使用已经定义好的手势,开发者不用自己计算手指移动轨迹。缺点就是没办法自定义手势,只能用系统已经实现的手势。如果想实现自己发明的某种手势还得去用触摸。

总结

当事件发生时,RunLoop 接收到外部或内部注册的事件,将其传递给 UIApplication 对象,UIApplication 封装成 UIEvent 类型的对象并将其分发给应用程序的主窗口(keyWindow),主窗口对象使用 hit-test view 的流程找到响应当前事件最合适的视图,若该视图无法响应事件,则通过响应者链条向上传递,直到找到合适的响应者,该响应者需要实现了 touchesBegan:withEvent: 等代理方法,若没找到就将事件抛弃,最终会将响应者传递给外部的调用者。

参考

  1. iOS 点击事件传递及响应
  2. Understanding Responders and the Responder Chain
  3. Cocoa Event Handling Guide
  4. iOS手势(UIGestureRecognizer)和触摸(touche event)的区别和联系
全角空格在Markdown中的使用 macOS上如何创建网址快捷方式
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×