事件传递和响应机制
响应者对象
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。例如常见的 :UIApplication ? UIViewController ? UIView
UIResponder 可以处理触摸事件、按压事件(3D touch)、远程控制事件、硬件运动事件。
事件的传递
1. 发生触摸事件后,系统会将该事件加入到一个由UIApplication 管理的事件队列中。因为队列的特点是FIFO,即先进先出,先产生的事件先处理(首先接收到事件的是UIApplication)。
2. UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常先发送事件给应用程序的主窗口(keyWindow)。
3. 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
触摸事件的传递是从父控件传递到子控件: ?UIApplication->window->寻找处理事件最合适的view
UIView不能接收触摸事件的 4 种情况:
1. 不允许交互 :userInteractionEnabled = NO,当前视图不可交互,该视图上面的子视图也不可与用户交互。用户触发的事件都会被该视图忽略(其他视图照常响应),并且该视图对象也会从事件响应队列中被移除。
2. 隐藏 :如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接收事件
3. 透明度 :如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
4. 子视图的部分区域超过父视图,也不会接收触摸事件,因为父视图在调用?pointInside方法时会返回NO。说明触摸点不在自己范围内,则当前 view 的hitTest: withEvent:方法返回 nil,当前 view上 的所有 subview 都不做判断。
注意:如果 Touch 位置超过视图边界,hitTest:withEvent 方法将忽略这个视图和它的所有子视图。结果就是,当视图的ciipsToBounds属性为NO,子视图超过视图边界也不会接收到事件 ,即使 触摸点在它上面。
不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理。 系统通过 hitTest:(CGPoint)point withEvent:(UIEvent*)even 找到最适合处理该事件的view。?
应用如何找到最合适的控件来处理事件
1. 首先判断主窗口(keyWindow)自己是否能接受触摸事件 hitTest 方法。
2. 判断触摸点是否在自己身上,通过pointInside 方法来判断。
3. 如果上面 2 步都满足条件,会把这个事件交给 view处理,会对 view的 subviews子控件数据 进行遍历,直至没有更合适的view为止。
注意:采取从数组最后面往前遍历子控件的方式,因为后添加的view在最上面,最上层的响应者能最先接受响应,阻断事件继续传递,从而降低遍历循环次数。
5. 如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,return self 。
寻找最合适的view底层剖析 ?两个重要的方法:
piontinside方法使用场景 : IOS 增加按钮点击区域 - 使按钮的点击反应区域变大
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event?什么时候调用??事件传递给谁,就会调用谁的hitTest:withEvent:方法。
作用
寻找并返回能够响应事件, ?最合适的view,不管点击哪里,最合适的view都是 hitTest 方法中返回的那个view。
注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,通过调用 hitTest 方法来判断是否可以处理事件。
拦截事件的处理
通过重写 hitTest ?方法,返回指定的view 。就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
注 意:如果 hitTest ?方法中返回 nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。如果同一层级的其他控件也没有合适的view,那么最合适的 view 就是父控件。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,子控件调用自己的hitTest:withEvent: 方法后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用。?
技巧: 想让谁成为最合适的view 就重写谁父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是, 建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view?
原因呢:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,想要返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view B。这就导致了返回的不是自己而是触摸点真正所在的view。所以建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view。
找到最合适的view 后,就会调用该view的 touches 方法处理具体的事件。
触摸事件由触屏生成后如何传递到当前应用?
系统响应阶段
用户触摸屏幕,系统硬件进程会获取到这个点击事件,将事件简单处理封装后存到系统中,由于硬件检测进程和当前App进程是两个进程,所以进程两者之间传递事件用的是端口通信。
1. 指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。
2. IOKit 将触摸事件封装成一个IOHIDEvent 对象,并通过mach port传递给SpringBoad进程。mach port 进程端口,各进程之间通过它进行通信。
3. SpringBoad 是一个系统进程,统一管理和分发系统接收到的触摸事件。将触摸事件交给前台app进程来处理。
参考: RunLoop原理学习 -
APP响应阶段
1. APP进程的mach port 接收到 SpringBoard 进程传递来的触摸事件,主线程的 runloop被唤醒,触发了source1回调。
2. source1回调又触发了一个source0回调,将接收到的 IOHIDEvent 对象封装成 UIEvent 对象。
3. source0 回调内部将触摸事件添加到 UIApplication 对象的事件队列中。事件出队后,UIApplication开始寻找最佳响应者,这个过程又称hit-testing。?
3. 系统判断本次触摸是否导致了一个新的事件。如果是,系统会先从响应网中寻找响应链。如果不是,说明该事件是当前正在进行中的事件产生的一个Touch message, 也就是说已经有保存好的响应链
4. 寻找到最佳响应者后,事件就在响应链中的传递及响应了。
响应者链条:由多个响应者对象连接起来的链条
在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。
事件在 响应者 链上传递,最终结果是事件被处理或被抛弃。响应者链条能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。
每一个响应者对象(UIResponder对象)都有一个nextResponder方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的第一响应者确定了,这个事件所处的响应链就确定了。
响应者对象默认的 nextResponder 如下:
1. UIView 的 nextResponder 属性,如果有管理此 view 的 UIViewController 对象,则为此 2. UIViewController 对象;否则 nextResponder 即为其 superview。
3. UIViewController 的 nextResponder 属性为其管理 view 的 superview.
若 VC 是window的根视图rootVC,则其 nextResponder 为 UIWindow ;
若 VC 是从别的控制器present出来的,则其nextResponder为presenting view controller。
4. UIWindow 的 nextResponder 属性为 UIApplication 对象。
5. UIApplication 的 nextResponder 属性为 nil。
若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。?
响应者链的事件传递过程:
1. ?如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
2. 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
3. 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
4. 如果UIApplication也不能处理该事件或消息,则将其丢弃
响应者对于接收到的事件有3种操作:
1. 不拦截,默认操作. ?事件会自动沿着默认的响应链向上传递,(touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理.
UIResponder中的默认实现是什么都不做,但UIKit中UIResponder的直接子类(UIView,UIViewController…) 的默认实现是将事件沿着responder chain继续向上传递到下一个responder, ?即nextResponder。
2. 拦截,不再往下分发事件, 重写 touchesBegan:withEvent:进行事件处理,不调用父类的 touchesBegan:withEvent, 事件到这里就结束传递进行处理。
3. 拦截,继续往下分发事件, 重写自己的 touchesBegan:withEvent: 进行事件处理,同时调用 [super ?touchesBegan:withEvent:] 将事件往下传递,达到 一个事件多个对象处理 的目的。?
建议使用:[super touchesBegan:touches withEvent:event];?
super 的touches对应方法中默认将事件继续向上传递给?next responder。?
不建议直接向nextResponder发送消息,这样可能会漏掉父类对这一事件的其他处理。
[self.nextResponder? touchesBegan:touches withEvent:event];
手势事件会打断响应链的传递。因为手势比响应链拥有更高的优先级,添加了手势的View 会阻止子View响应链,手势会最先响应,并对事件进行处理。此事件不再响应链中向上传递。
_UIApplicationHandleEventQueue() 识别了一个手势时,首先调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当UIGestureRecognizer 变化(创建/销毁/状态改变)时,回调都会进行处理。
事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。
可根据一个view 找到它对应的VC控制:?
参考文章:
史上最详细的iOS之事件的传递和响应机制-原理篇 -
iOS 响应者及响应者链 -
iOS - 为什么要在主线程中操作UI -