iOS

iOS触摸事件和事件响应链

RT

Posted by renchao on July 18, 2017

iOS触摸事件和事件响应链

事件的产生

当系统检测到触摸屏幕的事件之后,会将该事件打包成为一个UIEvent对象并放在当前活动的UIApplication管理的事件队列中,然后当前应用会在某个时刻取出该事件并传递给UIWindow。UIWindow对象会使用hitTest:withEvent:方法寻找本次触摸事件的所在视图,即第一响应对象。

hitTest:withEvent:方法中,大概流程是:调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图,若在则向当前视图的所有子视图发送hitTest:withEvent:消息。所有子视图的遍历顺序为最顶层到最底层(因为相比较之下,后添加的view在上面,降低循环次数)。当第一次有子视图返回非空对象,则hitTest方法返回此对象。整个过程相当于是递归的。

找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

处理事件的响应链为:该视图、父视图、视图控制器、window、application。

如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件。

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

  • 不允许交互:userInteractionEnabled = NO
  • 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
  • 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明

默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO,所以如果希望UIImageView可以交互,需要userInteractionEnabled = YES。

这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理。

事件响应链

响应者链条:在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。

在iOS中响应者链的关系可以用下图表示:

img

  • 如果当前这个view是控制器的view,那么控制器就是上一个响应者
  • 如果当前这个view不是控制器的view,那么父控件就是上一个响应者

响应者链的事件传递过程:

  1. 如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
  2. 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
  3. 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
  4. 如果UIApplication也不能处理该事件或消息,则将其丢弃

事件的传递和响应的区别:

事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件)。

详细谈谈

Image

起始阶段

CPU处于睡眠状态,等待事件发生

手指触摸屏幕

系统响应阶段

屏幕硬件感应到输入,并将感应到的事件传递给输入输出驱动IOKit

IOKit.framework封装整个触摸事件为IOHIDEvent对象

IOKit.framework通过IPC将事件转发给SpringBoard.app

以上是系统层的响应。系统感应到外界的输入,并将相应的输入封装成比较概括的IOHIDEvent对象,然后UIKit通过IOHIDEvent的类型,判断出相应事件应该由SpringBoard .app处理,直接通过mach port(IPC进程间通信)转发给SpringBoard.app

SpringBoard.app就是iOS的系统桌面,当触摸事件发生时,也只有负责管理桌面的SpringBoard.app才知道如何正确的响应。因为触摸发生时,有可能用户正在桌面翻页找App,也有可能正处于在微信中刷朋友圈

桌面响应阶段

SpringBoard.app主线程Runloop收到IOKit.framework转发来的消息苏醒,并触发对应Mach Port的Source1回调__IOHIDEventSystemClientQueueCallback()

如果SpringBoard.app监测到有App在前台(记为xxxx.app),SpringBoard.app通过mach port(IPC进程间通信)转发给xxxx.app,如果SpringBoard.app监测到监测无前台App,则SpringBoard.app进入App内部响应阶段的第二段,记触发Source0回调

App内部响应阶段

前台App主线程Runloop收到SpringBoard.app转发来的消息苏醒,并触发对应Mach Port的Source1回调__IOHIDEventSystemClientQueueCallback()

Source1回调内部触发Source0回调__UIApplicationHandleEventQueue()

Soucre0回调内部,封装IOHIDEventUIEvent

Soucre0回调内部调用UIApplicationsendEvent:方法,将UIEvent传给UIWindow

平时开发熟悉的触摸事件响应链从这开始了

通过递归调用UIView层级的hitTest(_:with:),结合point(inside:with:)找到UIEvent中每一个UITouch所属的UIView(其实是想找到离触摸事件点最近的那个UIView)。这个过程是从UIView层级的最顶层往最底层递归查询,但这不是UIResponder响应链,事件响应是在UIEvent中每一个UITouch所属的UIView都确定之后方才开始。当把断点打在某个UIViewhitTest(_:with:)中时,对应的调用堆栈如下: img

根据围绕UITouch所属的UIView及其祖先UIView的gesture recognizers,来确定一个UITouch的gestureRecognizers

UITouch所属的UIView和gestureRecognizers收到此UITouch和相应的UIEvent,并按照UITouch所处的状态调用四大UITouch方法touchesBegan(_:with:)touchesMoved(_:with:)touchesEnded(_:with:) touchesCancelled(_:with:)中的一个。(事件响应开始

—-> 对于UIView收到的UITouches事件(四大UITouch事件都是如此),则会按照UIResponder响应链一直往上传递,直到某个UIResponder因为主动响应触摸事件,切断了响应链(即不调用下一个UIResponder的响应方法),如果一直没有UIResponder做响应处理,则这些UITouches到达最后的响应者即UIApplication后,就被吃掉了,消失了。

—-> 如果在事件响应过程中,有UIGestureRecognizer成功识别,则此UIGestureRecognizer将独自占有所需要的UITouches,这些UITouches所属的UIView及其他的UIGestureRecognizer的touchesCancelled(_:with:)方法将调用(如果在手势的代理中设置可以同时识别两个手势,则允许同时识别的手势均可以收到所需要的UITouches事件)。但与识别成功的UIGestureRecognizer无关的UITouches则会继续按照上述传递逻辑传递。也即允许两个手势同时识别,只要所占有的UITouches不相同。

—-> 如果UIGestureRecognizer识别成功,则调用相应的action,处理对应的逻辑。如果某个UIResponder主动响应了触摸事件,则根据其本身的响应逻辑处理对应的业务,UIControl都是主动响应并切断UITouch的向上传递的。

—-> UITouches事件流动完毕,整个系统重新进入睡眠等待下一个事件

总结

从手指触碰到屏幕,UITouch大致经历三个阶段,系统处理阶段—->SpringBoard.app处理阶段—->前台App处理阶段,事实上日常开发只需知晓最后一个阶段即可,前两个阶段参考资料也不多,更多的还涉及系统底层,这里仅做简单介绍。