谈谈你对事件的传递链和响应链的理解?
- 一:响应者链
UIResponser
包括了各种Touch message
的处理,比如开始,移动,停止等等。常见的UIResponser
有UIView
及子类,UIViController
,APPDelegate
,UIApplication
等等。
回到响应链,响应链是由UIResponser
组成的,那么是按照哪种规则形成的。
A: 程序启动
UIApplication
会生成一个单例,并会关联一个APPDelegate
。APPDelegate
作为整个响应链的根建立起来,而UIApplication
会将自己与这个单例链接,即UIApplication
的nextResponser
(下一个事件处理者)为APPDelegate
。B:创建
UIWindow
程序启动后,任何的UIWindow
被创建时,UIWindow
内部都会把nextResponser
设置为UIApplication
单例。UIWindow
初始化rootViewController,rootViewController
的nextResponser
会设置为UIWindow
C:
UIViewController
初始化loadView
,VC
的view
的nextResponser
会被设置为VC
.D:
addSubView addSubView
操作过程中,如果子subView
不是VC
的View,
那么subView
的nextResponser
会被设置为superView
。如果是VC
的View
,那就是subView
->subView.VC
->superView
如果在中途,subView.VC
被释放,就会变成subView.nextResponser = superView
我们使用一个现实场景来解释这个问题:当一个用点击屏幕上的一个按钮,这个过程具体发生了什么。
-
- 用户触摸屏幕,系统硬件进程会获取到这个点击事件,将事件简单处理封装后存到系统中,由于硬件检测进程和当前
App
进程是两个进程,所以进程两者之间传递事件用的是端口通信。硬件检测进程会将事件放到APP
检测的那个端口。
- 用户触摸屏幕,系统硬件进程会获取到这个点击事件,将事件简单处理封装后存到系统中,由于硬件检测进程和当前
2.APP启动主线程
RunLoop
会注册一个端口事件,来检测触摸事件的发生。当事件到达,系统会唤起当前APP
主线程的RunLoop
。来源就是App
主线程事件,主线程会分析这个事件。3.最后,系统判断该次触摸是否导致了一个新的事件, 也就是说是否是第一个手指开始触碰,如果是,系统会先从响应网中 寻找响应链。如果不是,说明该事件是当前正在进行中的事件产生的一个
Touch message
, 也就是说已经有保存好的响应链二:事件传递链
通过两种方法来做这个事情。
// 先判断点是否在View内部,然后遍历subViews
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
//判断点是否在这个View内部
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
复制代码
- A: 流程
- 1:先判断该层级是否能够响应
(1.alpha>0.01 2.userInteractionEnabled == YES 3.hidden = NO)
- 2:判断改点是否在
view
内部, - 3:如果在那么遍历子
view
继续返回可响应的view
,直到没有。
B:常见问题 - 父view设置为不可点击,子view可以点击吗
- 不可以,hit test 到父view就截止了
- 子view设置view不可点击不影响父类点击
- 同父view覆盖不影响点击
- 手势对responder方法的影响
- C:实际用法
- 点一一个圆形控件,如何实现只点击圆形区域有效,重载
pointInside
。此时可将外部的点也判断为内部的点,反之也可以。 - 事件响应链在复杂功能界面进行不同控件间的通信,简便某些场景下优于代理和
block
iOS事件链有两条:事件的响应链;Hit-Testing
事件的传递链
响应链:由离用户最近的
view
向系统传递。initial view
–>super view
–> … –>view controller
–>window
–>Application
–>AppDelegate
传递链:由系统向离用户最近的
view
传递。UIKit
–>active app's event queue
–>window
–>root view
–> … –>lowest view
在iOS中只有继承UIResponder
的对象才能够接收并处理事件,UIResponder
是所有响应对象的基类,在UIResponder
类中定义了处理上述各种事件的接口。我们熟悉的UIApplication
、UIViewController
、UIWindow
和所有继承自UIView
的UIKit
类都直接或间接的继承自UIResponder
,所以它们的实例都是可以构成响应者链的响应者对象,首先我们通过一张图来简单了解一下事件的传递以及响应
- 传递链
- 事件传递的两个核心方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // def
- 第一个方法返回的是一个UIView,是用来寻找最终哪一个视图来响应这个事件
- 第二个方法是用来判断某一个点击的位置是否在视图范围内,如果在就返回YES
- 其中UIView不接受事件处理的情况有
1. alpha <0.01
2. userInteractionEnabled = NO
3. hidden = YES
- 事件传递的流程图
- 流程描述
- 我们点击屏幕产生触摸事件,系统将这个事件加入到一个由
UIApplication
管理的事件队列中,UIApplication
会从消息队列里取事件分发下去,首先传给UIWindow
- 在
UIWindow
中就会调用hitTest:withEvent:
方法去返回一个最终响应的视图- 在
hitTest:withEvent:
方法中就会去调用pointInside: withEvent:
去判断当前点击的point
是否在UIWindow
范围内,如果是的话,就会去遍历它的子视图来查找最终响应的子视图- 遍历的方式是使用倒序的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都回去调用它的
hitTest:withEvent:
方法,可以理解为是一个递归调用- 最终会返回一个响应视图,如果返回视图有值,那么这个视图就作为最终响应视图,结束整个事件传递;如果没有值,那么就会将
UIWindow
作为响应者
- 响应链
- 响应者链流程图
- 响应者链的事件传递过程总结如下
- 如果
view
的控制器存在,就传递给控制器处理;如果控制器不存在,则传递给它的父视图- 在视图层次结构的最顶层,如果也不能处理收到的事件,则将事件传递给
UIWindow
对象进行处理- 如果
UIWindow
对象也不处理,则将事件传递给UIApplication
对象- 如果
UIApplication
也不能处理该事件,则将该事件丢弃
- 实例场景
- 在一个方形按钮中点击中间的圆形区域有效,而点击四角无效
- 核心思想是在
pointInside: withEvent:
方法中修改对应的区域
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 如果控件不允许与用用户交互,那么返回nil
if (!self.userInteractionEnabled || [self isHidden] || self.alpha <= 0.01) {
return nil;
}
//判断当前视图是否在点击范围内
if ([self pointInside:point withEvent:event]) {
//遍历当前对象的子视图(倒序)
__block UIView *hit = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//坐标转换,把当前坐标系上的点转换成子控件坐标系上的点
CGPoint convertPoint = [self convertPoint:point toView:obj];
//调用子视图的hitTest方法,判断自己的子控件是不是最适合的View
hit = [obj hitTest:convertPoint withEvent:event];
//如果找到了就停止遍历
if (hit) *stop = YES;
}];
//返回当前的视图对象
return hit?hit:self;
}else {
return nil;
}
}
// 该方法判断触摸点是否在控件身上,是则返回YES,否则返回NO,point参数必须是方法调用者的坐标系
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGFloat x1 = point.x;
CGFloat y1 = point.y;
CGFloat x2 = self.frame.size.width / 2;
CGFloat y2 = self.frame.size.height / 2;
//判断是否在圆形区域内
double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
if (dis <= self.frame.size.width / 2) {
return YES;
}
else{
return NO;
}
}