背景
这里有一个问题,触摸事件是如何从屏幕转移到APP内的?
先说结论:触摸事件从触屏产生后,由IOKit将触摸事件传递给SpringBoard进程,再由SpringBoard分发给当前前台APP处理。
直接上图:
传递流程
系统响应阶段
1、手指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。
2、IOKit
将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoad进程。
mach port 进程端口,各进程之间通过它进行通信。
SpringBoad.app 是一个系统进程,可以理解为桌面系统
,可以统一管理和分发系统接收到的触摸事件。
3、SpringBoard进程因接收到触摸事件,触发了主线程runloop的source1事件源的回调
。
此时SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,
接下来的事情便是APP内部对于触摸事件的响应了。
APP响应阶段
1、APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的runloop被唤醒,触发了source1回调。
2、source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象
,此时APP将正式开始对于触摸事件的响应。
3、source0回调内部将触摸事件添加到UIApplication对象的事件队列中。事件出队后,UIApplication开始一个寻找最佳响应者的过程,这个过程又称hit-testing,细节将在[寻找事件的最佳响应者]一节阐述。另外,此处开始便是与我们平时开发相关的工作了。
4、寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了。事实上,事件除了被响应者消耗,还能被手势识别器或是target-action模式捕捉并消耗掉。其中涉及对触摸事件的响应优先级,简单总结,UIRespnder、UIGestureRecognizer、UIControl三个一起时,笼统地讲,事件响应优先级依次递增。
5、触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。
Hit-Testing的本质
事件自下而上的传递
UIApplication ——> UIWindow ——> 子视图 ——> ... ——> 子视图
上面讲了事件在响应者之间传递的规则,视图通过判断自身能否响应事件来决定是否继续向子视图传递。
那么问题来了:视图如何判断能否响应事件?以及视图如何将事件传递给子视图?
首先要知道的是,以下几种状态的视图无法响应事件:
不允许交互:userInteractionEnabled = NO
隐藏:hidden = YES 如果父视图隐藏,那么子视图也会隐藏,隐藏的视图无法接收事件
透明度:alpha < 0.01 如果设置一个视图的透明度<0.01,会直接影响子视图的透明度。alpha:0.0~0.01为透明。
hitTest:withEvent:
每个UIView对象都有一个 hitTest:withEvent: 方法,这个方法是Hit-Testing过程中最核心的存在,其作用是询问事件在当前视图中的响应者,同时又是作为事件传递的桥梁
。
hitTest:withEvent: 方法返回一个UIView对象,作为当前视图层次中的响应者。默认实现是:
1、若当前视图无法响应事件,则返回nil
2、若当前视图可以响应事件,但无子视图可以响应事件,则返回自身作为当前视图层次中的事件响应者
3、若当前视图可以响应事件,同时有子视图可以响应,则返回子视图层次中的事件响应者
一开始UIApplication将事件通过调用UIWindow对象的 hitTest:withEvent: 传递给UIWindow对象,UIWindow的 hitTest:withEvent: 在执行时若判断本身能响应事件,则调用子视图的 hitTest:withEvent: 将事件传递给子视图并询问子视图上的最佳响应者。最终UIWindow返回一个视图层次中的响应者视图给UIApplication,这个视图就是hit-testing的最佳响应者。
事件传递顺序:
UIApplication事件管理队列->分发window->子视图递归寻找最合适的view
注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
事件响应顺序:
事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。
拦截事件的处理
事件传递给谁,就会调用谁的hitTest:withEvent:方法。
- (nullableUIView *)hitTest:(CGPoint)point withEvent:(nullableUIEvent *)event;称为方法A
- (BOOL)pointInside:(CGPoint)point withEvent:(nullableUIEvent *)event;称为方法B
如果方法A中,我们调用了父类的这个方法,也就是
[super hitTest:point withEvent:event];那这个时候系统就要调用方法B;通过这个方法的返回值,来判断当前这个view能不能响应消息。
如果方法B返回的是no,那就不用再去遍历它的子视图。方法A返回的view就是可以响应事件的view。 如果方法B返回的是YES,那就去遍历它的子视图。
(就是上图我们描述的那样,找到合适的view返回,如果找不到,那就由方法A返回的view去响应这个事件。)