最近在面试,好基友池子
跑过来对我说:响应者链
这是个必考点,一般会这么问:响应者事件传递顺序是什么, 响应者的响应顺序是什么?
池子
认为事件传递的过程是自上而下
的,事件响应是自下而上
而上的。为此和池子
争论了一番。争议点在事件传递
上,就此达成一致的是响应者链
的顺序是自上而下
的。Jeverson
认为响应者链
寻找最合适的(第一响应者)响应者调用HitTest
的过程–事件响应
,找到第一响应者
发现没有相应的处理函数,向上传递事件的过程–事件传递
。池子
则认为相反。
Apple注册了一个 Source1
(基于mach_port
的)用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallBack().
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由IOKit.framework
生成一个IOHIDEvent
事件并由SrpingBoard
接收
SpringBoard
只接收按键(锁屏/静音)、触摸,加速,接近传感器等几种Event
,随后用mach_port
转发给需要的 App
进程。随后苹果注册的那个 Source1
就会触发触发回调,并调用_UIApplicationHandleEventQueue()
进行应用的内部分发。
_UIApplicationHandleEventQueue()
会把IOHIDEvent
处理并包装成UIEvent
进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow
等。通常事件比如UIButton点击 、touchesBegin/move/end/cancel
事件都是在这个回调中完成的。
因Jeverson
在面试中遇到过两位面试官
,两个人对事件传递
就如何池子
和Jevreson
一样。而本人呢却刚好巧妙的避开两位面试官
的理解。故本文就抛开歧义,全面理解HitTest
,响应者链
。并参照池子对两争议名词的理解开展。
上面讲述到_UIApplicationHandleEventQueue()
将UIEvent
进行处理或分发给window
,接着是如何找出最佳响应者(responder)
, 这里就要引入UIview
的两个方法:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
方法一:返回视图层级结构,并显示第一响应者
方法二:返回点击是否在该视图中 2.1 响应者一瞥
上面讲到过,UIView
的两个重要方法;那我们来定义一个嵌套涂层来一探这两个方法的调用过程。我们的示例参照stackoverflow
+----------------------------+
|A |
|+--------+ +------------+ |
||B | |C | |
|| | |+----------+| |
|+--------+ ||D || |
| |+----------+| |
| +------------+ |
+----------------------------+
手指在D
上点击究竟发生了什么,hitTest:withEvent:
, pointInside:withEvent:
发生了什么,让我们来一探究竟究竟。
Category
以交换方法
的方式一探究竟
+ (void)load {
Method originHitTest = class_getInstanceMethod([UIView class], @selector(hitTest:withEvent:));
Method customHitTest = class_getInstanceMethod([UIView class], @selector(jj_hitTest:withEvent:));
method_exchangeImplementations(originHitTest, customHitTest);
Method originPointInside = class_getInstanceMethod([UIView class], @selector(pointInside:withEvent:));
Method customPointInside = class_getInstanceMethod([UIView class], @selector(jj_pointInside:withEvent:));
method_exchangeImplementations(originPointInside, customPointInside);
}
- (UIView *)jj_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ hitTest", NSStringFromClass([self class]));
UIView *view = [self jj_hitTest:point withEvent:event];
NSLog(@"%@ hitTest return %@", NSStringFromClass([self class]), NSStringFromClass([view class]));
return view;
}
- (BOOL)jj_pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ pointInside", NSStringFromClass([self class]));
BOOL result = [self jj_pointInside:point withEvent:event];
NSLog(@"%@ pointInside return %@", NSStringFromClass([self class]), (result?@"true":@"false"));
return result;
}
2.3 log
2022-02-25 17:13:12.187599+0800 ResponserChain[16154:291200] UIWindow hitTest
2022-02-25 17:13:12.187789+0800 ResponserChain[16154:291200] UIWindow pointInside
2022-02-25 17:13:12.187892+0800 ResponserChain[16154:291200] UIWindow pointInside return true
2022-02-25 17:13:12.187998+0800 ResponserChain[16154:291200] UITransitionView hitTest
2022-02-25 17:13:12.188105+0800 ResponserChain[16154:291200] UITransitionView pointInside
2022-02-25 17:13:12.188200+0800 ResponserChain[16154:291200] UITransitionView pointInside return true
2022-02-25 17:13:12.188290+0800 ResponserChain[16154:291200] UIDropShadowView hitTest
2022-02-25 17:13:12.188399+0800 ResponserChain[16154:291200] UIDropShadowView pointInside
2022-02-25 17:13:12.188712+0800 ResponserChain[16154:291200] UIDropShadowView pointInside return true
2022-02-25 17:13:12.189025+0800 ResponserChain[16154:291200] AView hitTest
2022-02-25 17:13:12.189276+0800 ResponserChain[16154:291200] AView pointInside
2022-02-25 17:13:12.189609+0800 ResponserChain[16154:291200] AView pointInside return true
2022-02-25 17:13:12.189910+0800 ResponserChain[16154:291200] CView hitTest
2022-02-25 17:13:12.190180+0800 ResponserChain[16154:291200] CView pointInside
2022-02-25 17:13:12.190461+0800 ResponserChain[16154:291200] CView pointInside return true
2022-02-25 17:13:12.190727+0800 ResponserChain[16154:291200] DView hitTest
2022-02-25 17:13:12.190984+0800 ResponserChain[16154:291200] DView pointInside
2022-02-25 17:13:12.191231+0800 ResponserChain[16154:291200] DView pointInside return true
2022-02-25 17:13:12.191497+0800 ResponserChain[16154:291200] UILabel hitTest
2022-02-25 17:13:12.191759+0800 ResponserChain[16154:291200] UILabel hitTest return (null)
2022-02-25 17:13:12.192088+0800 ResponserChain[16154:291200] DView hitTest return DView
2022-02-25 17:13:12.193328+0800 ResponserChain[16154:291200] CView hitTest return DView
2022-02-25 17:13:12.193609+0800 ResponserChain[16154:291200] AView hitTest return DView
2022-02-25 17:13:12.193836+0800 ResponserChain[16154:291200] UIDropShadowView hitTest return DView
2022-02-25 17:13:12.194166+0800 ResponserChain[16154:291200] UITransitionView hitTest return DView
2022-02-25 17:13:12.194396+0800 ResponserChain[16154:291200] UIWindow hitTest return DView
2022-02-25 17:13:12.194743+0800 ResponserChain[16154:291200] UIWindow hitTest
2022-02-25 17:13:12.195022+0800 ResponserChain[16154:291200] UIWindow pointInside
2022-02-25 17:13:12.195276+0800 ResponserChain[16154:291200] UIWindow pointInside return true
2022-02-25 17:13:12.195509+0800 ResponserChain[16154:291200] UITransitionView hitTest
2022-02-25 17:13:12.195889+0800 ResponserChain[16154:291200] UITransitionView pointInside
2022-02-25 17:13:12.196137+0800 ResponserChain[16154:291200] UITransitionView pointInside return true
2022-02-25 17:13:12.201222+0800 ResponserChain[16154:291200] UIDropShadowView hitTest
2022-02-25 17:13:12.201357+0800 ResponserChain[16154:291200] UIDropShadowView pointInside
2022-02-25 17:13:12.201451+0800 ResponserChain[16154:291200] UIDropShadowView pointInside return true
2022-02-25 17:13:12.201550+0800 ResponserChain[16154:291200] AView hitTest
2022-02-25 17:13:12.201648+0800 ResponserChain[16154:291200] AView pointInside
2022-02-25 17:13:12.201740+0800 ResponserChain[16154:291200] AView pointInside return true
2022-02-25 17:13:12.201830+0800 ResponserChain[16154:291200] CView hitTest
2022-02-25 17:13:12.202077+0800 ResponserChain[16154:291200] CView pointInside
2022-02-25 17:13:12.202335+0800 ResponserChain[16154:291200] CView pointInside return true
2022-02-25 17:13:12.202581+0800 ResponserChain[16154:291200] DView hitTest
2022-02-25 17:13:12.202853+0800 ResponserChain[16154:291200] DView pointInside
2022-02-25 17:13:12.203083+0800 ResponserChain[16154:291200] DView pointInside return true
2022-02-25 17:13:12.203344+0800 ResponserChain[16154:291200] UILabel hitTest
2022-02-25 17:13:12.203607+0800 ResponserChain[16154:291200] UILabel hitTest return (null)
2022-02-25 17:13:12.203868+0800 ResponserChain[16154:291200] DView hitTest return DView
2022-02-25 17:13:12.204110+0800 ResponserChain[16154:291200] CView hitTest return DView
2022-02-25 17:13:12.204367+0800 ResponserChain[16154:291200] AView hitTest return DView
2022-02-25 17:13:12.204641+0800 ResponserChain[16154:291200] UIDropShadowView hitTest return DView
2022-02-25 17:13:12.204916+0800 ResponserChain[16154:291200] UITransitionView hitTest return DView
2022-02-25 17:13:12.205192+0800 ResponserChain[16154:291200] UIWindow hitTest return DView
日志中不难看出hitTest:withEvent
、pointInside:withEvent
的调用是从window
开始的;值得注意的是,A执行完成之后的调用,并没有调用与C同级的B的方法,主要是因为子视图的调用顺序是从后向前的 而C是在B之后并且调用C之后返回的是true
故这里并没有再去遍历B。这里引用Apple的解释hitTest:withEvent 返回包含指定点的视图层次结构(包括其自身)中接收器的最远后代。
然后找到最佳响应者DView
,值得注意的是此处的Label 的userInteraction
没有打开。因为Label
是View的子试图,所以还有一次递归调用,返回是Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.
1、最佳响应者寻找的方式是通过hitTest:withEvent
和pointInside
来完成的
2、hitTest
的调用是从window
开始的,子视图
的调用方式是从后向前
。
3、递归调用找到最合适的响应者,并返回包含指定点的视图层次结构(包括其自身)中接收器的最远后代
找到了最佳响应者,那么手势识别的过程是怎样的呢?
3.1 手势识别当上面的_UIApplicationHandleEventQueue()
识别了一个手势,其首先会调用cancel
将当前的touchesBegin/move/end
系列回调打断。随后系统将对应的UIGestureRecoginizer
标记为待处理
。
苹果注册了一个observer
监测 beforeWaiting
(Loop即将进入休眠)事件,这个observer
的回调函数是_UIGestureRecognizerUpdateObserver()
,其内部会获取刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。当有UIGestureRecognizer的变化(创建/销毁/改变状态
)是,这个回调都会进行相应的处理。
我们在ViewD
的touch
事件,ViewD
作为最佳响应者,理应处理事件。但是我们这边并没有处理(重写touchesBegin/move/end
的方法)。那么事件将会被传递,那么D
的下一个响应者是谁,谁来处理,事件在传递的过程中若响应者链中没有处理那么终将被废弃掉。
那么我们来一探事件传递的过程。
nextResponder
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (2 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
UIResponder *responder = self.dView.nextResponder;
NSMutableString *pre = [NSMutableString stringWithString:@"--"];
NSLog(@"%@", NSStringFromClass([self.dView class]));
while (responder) {
NSLog(@"%@%@", pre, NSStringFromClass([responder class]));
[pre appendString:@"--"];
responder = responder.nextResponder;
}
});
3.3.2 logs
2022-02-27 10:00:54.761528+0800 ResponserChain[4933:159322] DView
2022-02-27 10:00:54.761673+0800 ResponserChain[4933:159322] --CView
2022-02-27 10:00:54.761815+0800 ResponserChain[4933:159322] ----AView
2022-02-27 10:00:54.761925+0800 ResponserChain[4933:159322] ------ViewController
2022-02-27 10:00:54.762091+0800 ResponserChain[4933:159322] --------UIDropShadowView
2022-02-27 10:00:54.762209+0800 ResponserChain[4933:159322] ----------UITransitionView
2022-02-27 10:00:54.762328+0800 ResponserChain[4933:159322] ------------UIWindow
2022-02-27 10:00:54.762424+0800 ResponserChain[4933:159322] --------------UIWindowScene
2022-02-27 10:00:54.762700+0800 ResponserChain[4933:159322] ----------------UIApplication
2022-02-27 10:00:54.763049+0800 ResponserChain[4933:159322] ------------------AppDelegate
3.3.3 注意点
我们在3.3.1
查找nextResnponder
时,将方法延时执行的,若直接在ViewController
的viewDidLoad
中,nextResponder
将在ViewController
中结束。
2022-02-27 10:10:45.560602+0800 ResponserChain[5275:168169] DView
2022-02-27 10:10:45.560739+0800 ResponserChain[5275:168169] --CView
2022-02-27 10:10:45.560852+0800 ResponserChain[5275:168169] ----AView
2022-02-27 10:10:45.560952+0800 ResponserChain[5275:168169] ------ViewController
从这里推理出我们响应者树的构造过程是在ViewDidLoad周期中来完成的,这个函数会将当前实例的构成的响应者子树合并到我们整个根树中
根据logs
我们得出,ResponderChain
是自上而下
的即AppDelegate->UIApplication->UIWindowSece->UIWindow->ViewController->AView->CView->DView
.事件处理的顺序将是自下而上而上,由ResponderChain
的逆向传递去处理的。因为想看到事件传递的全过程,我们这边并没有去处理touch
事件,下面我们将处理事件来验证事件处理的过程。
我们在A
View上来处理touch/(begain/cancel/move/end)
事件,overwrite
前面的方法。
@implementation AView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@ touchBegin", NSStringFromClass([self class]));
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@ toucheModed", NSStringFromClass([self class]));
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@ touchEnded", NSStringFromClass([self class]));
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@ touchCanclled", NSStringFromClass([self class]));
[super touchesCancelled:touches withEvent:event];
}
@end
我们得到下面的log
2022-02-27 10:42:08.346133+0800 ResponserChain[6196:190756] UIWindow hitTest return DView
2022-02-27 10:42:08.347626+0800 ResponserChain[6196:190756] AView touchBegin
2022-02-27 10:42:08.439185+0800 ResponserChain[6196:190756] AView touchEnded
若我们在DView中添加手势,并且不去实现touch
的方法,那又会发生什么呢
- (void)awakeFromNib {
[super awakeFromNib];
[self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)]];
}
- (void)tapAction:(UITapGestureRecognizer *)tapReg {
NSLog(@"%@ DView taped", tapReg.self);
}
2022-02-27 11:06:06.307171+0800 ResponserChain[6932:210072] UIWindow hitTest return DView
2022-02-27 11:06:06.308727+0800 ResponserChain[6932:210072] AView touchBegin
2022-02-27 11:06:06.377668+0800 ResponserChain[6932:210072] ; target= <(action=tapAction:, target=)>> DView taped
2022-02-27 11:06:06.377883+0800 ResponserChain[6932:210072] AView touchCanclled
3.3.5 总结
我们得到下面结论
1、响应者链找出最佳响应者firstResopnder
->DView
2、DView顺着响应者链,找到合适的处理者AView
(实现了touch
)方法。 (DView有视图,会根据nextResponder 寻找到CView,发现C没有处理,C会根据nextResponder找到A,结果发现A处理了那那么事件传递到此结束)
3、若本例中的A没有处理,会向上找到ViewController
,若还是找不到会沿着响应者链
一直向上传递,知道AppDelegate也不处理。那么这个事件将被废弃掉。
无法响应即不调用hitTest
的状况,主要有以下几点
篇幅有限,关于案例中问题;可以自行百度。考查的知识点也可以从上述的篇幅中找到答案。
5.参考文献iOS响应者链彻底掌握
Apple_Event Handling Guide for UIKit Apps
Responder一点也不神秘————iOS用户响应者链完全剖析
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)