在实际的开发中会经常遇到处理事件的操作,对于我们这样的新手而言,会遇到的一是事件发生后在什么时候去处理这个事件,二是在事件处理的时候会得不到响应等问题;鉴于此,明确关于事件是如何发生,如何传递以及最后是谁来做响应处理的是非常有必要的。以触摸事件为例,来具体阐述:
本文首先从事件的传递来说起,也就是来解决以下问题:
- 事件是如何产生的?
- 点击屏幕的触摸事件是如何从屏幕转移到应用内的?
- 系统是如何将点击事件传递到window上;
一、相关术语:触摸,事件以及响应者
1、触摸:UITouch
手指触摸屏幕产生对应的UItouch对象,以下触摸的各个阶段的状态;触摸发生在屏幕上,是人为操作产生;
2、事件:UIEvent
触摸的目的是生成触摸事件供响应者响应,一个触摸事件对应一个UIEvent对象,其中的type属性标识了事件的类型(事件可以分为很多种,不止是触摸事件)。UIEvent对象中包含了触发该事件的触摸对象的集合,因为一个触摸事件可能是由多个手指同时触摸产生的。触摸对象集合通过allTouches属性获取。
以下是UIEventType,标识了 不同的事件,包括加速器事件,远程控制事件,移动事件,滚动事件等等。
3、响应者:UIResponder
在ios中不是所有对象都能接收到事件,继承于UIResponder的对象才可以接收并处理事件,这些被称为“响应者对象”;
UIView,UIViewController,UIApplication,Appdelegate响应者能够响应事件,是因为UIResponder中提供了四个事件处理的方法:
通过以上的阐述,很清楚的能够明白,触摸产生触摸事件,响应者来对触摸事件进行响应。这些了解帮助我们解决第一个问题,事件是如何产生的,同样以触摸事件为例,人为的触摸屏幕,系统生成对应的UITouch对象, 有了UITouch对象后同时会对应生成UIEvent对象,一个UITouch对象对应一个UIEvent对象,此时,会产生我们所说对应的触摸事件。而后,该事件进行传递找到最佳的响应者对象对其进行响应处理。
到此仍然没有解决我们事件是如何传递的问题,那继续。
二、系统如何将点击事件传递到window上;
当用户触摸屏幕时,就会产生对应的触摸事件,经过IPC进程间通信,事件最终被传递到了合适的应用内;触摸事件从触屏产生后,由IOKit将触摸事件传递给SpringBoard进程,再由SpringBoard分发给当前前台APP处理。以下是处理的简单流程图:
通过以上的流程之后,我们的触摸就会被转入对应app或者系统桌面上来做处理;这里会引入我们的runloop的内容,runloop事件循环机制等,后续再展开;所有的事件最终会被加入到runloop循环当中,等待处理;
总结:当一个外界的事件(触摸,摇晃,重力事件等)发生后,首先由IOKit.framework生成一个IOHIDEvent事件并且会交由SpringBord来接收。SpringBord接收该事件后,通过mac port转发给当前对应的app进程,随后苹果注册的那个source1 就会触发回调,并调用_UIApplicationHandleEventQueue()进行应用内部的分发。
相关的术语概念:
- IOKit是是一个系统框架的集合,用来驱动一些系统事件;用来处理接收和处理硬件事件,获取设备的相关信息等作用的库;在这里用来接收触摸事件并且生成对应的IOHIDEvent对象;
- IOHIDEvent中的 HID 代表 Human Interface Device,即人机交互驱动。
- mach port 进程端口,各进程之间通过它进行通信。
- SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。(不止是触摸事件,SpringBoad接收按键,触摸,加速,重力传感等事件);
自此,对于一个事件是如何产生并且是如何从用户行为转移到手机屏幕的window上的,有了一定的答案;那么,事件最终会被分配到桌面系统或者是对应的app进程中来做处理,那么,如何来找到最佳的事件响应者,在app内部的传递过程又是如何的?接下来:
三、事件在App内部的传递
在通过从用户触摸屏幕,到通过IPC进程间通信,由IOKit将触摸事件传递给SpringBoard进程,再由SpringBoard分发给当前前台App处理。此时,该事件到达App,App进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的runloop被唤醒,触发了source1回调。source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时App将正式开始对于触摸事件的响应。
如下图所示,是事件在App内部的传递过程:
如上图所示是模拟的app内部的视图,视图层级为A( B(D,E) , C(G,F));A中加入B和C,B中有D和E,C中有F和G;
那么,第一:当触摸事件发生在F所在的区域时,事件的传递和响应过程如下所示:
判断是否能够响应该事件,从四个方面:(允许交互,不隐藏,透明度以及子视图没有超过父视图的有效范围也就是点击区域);
- UIApplication先将事件传给A判断自身是否能够响应该事件;
- 如果能,那A继续将该事件传给C(从后向前进行询问查询);
- C判断自己是否能够响应该事件,如果能,C继续将该事件传给G;
- G判断自己不能够响应快该事件,那么,回退到C,C继续将该事件传给F;
- F判断自己可以响应该事件,并且F已经没有子视图,所以F本身就是事件的最终响应者;
第二:当触摸事件发生D在所在的区域且不在其父视图B的范围内时,事件的传递和响应过程如下所示:
- UIApplication先将事件传给A判断自身是否能够响应该事件;
- 如果能,那A继续将该事件传给(从后向前进行询问查询);
- C判断自己是否能够响应该事件;
- C不能响应该事件,回退到A,A继续将该事件传给B;
- B判断自己可以不能响应该事件;回退到A;
- 回退到A后,A已经没有子视图可以传递事件,那么A就是最终的事件响应者;
(这里有个问题是:如果不了解这个流程的话,在实现的时候,会认为“我给D设置了点击事件,那为什么有时候点击D的控件时,它对点击的响应是时有时无呢?”
这就是当可以响应的时候,说明我们的点击区域在D所在的B父视图的区域内,当D不响应的时候,就是我们上面这种情况;)
以上是两种事件传递的情况,一是正常情况,二是异常情况;总的来说事件在App中的传递过程总结如下流程图所示:
也就是说:事件在App内部的传递流程是从后向前不断查找,找到最终最佳响应者;以上的过程就事件从产生到找到最佳响应者的过程;
事件的传递遵循的原则:同级从后向前传递;到此 ,本文对于事件的产生和传递做了一些论述(参考相关内容),那么接下来通过简单的示例来加以验证。
那么,上面提到,判断当前视图是否可以响应事件从四个方面来看:
- 是否允许交互:将响应者对象的userInteractionEnabled属性设为NO,表示不允许交互;为YES时表示允许交互;
- 是否隐藏:hidden = YES;如果父视图隐藏,那么子视图也会隐藏,隐藏的视图无法接收事件;hidden = NO,不隐藏;
- 透明度的量级:alpha < 0.01如果设置一个视图的透明度<0.01,会直接影响子视图的透明度。alpha:0.0~0.01为透明。
- 子视图是否超出了父视图的有效范围;
在事件的传递过程中,每经过一个视图时都需要做以上的判断,那么,app内部是如何来做这些判断的呢?由此,就必须提到一个方法:hitTest方法;
四、hitTest方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
hitTest是UIView中的一个方法,每一个响应者对象都会有一个对应的hitTest方法,用来判断是否可以响应事件并传递事件;
以下是往上给出的hitTest的判断逻辑:
所以hitTest的作用有两个:
- 一是用来询问事件在当前视图中的响应者,返回的是最终响应这个的事件的响应者对象;
- 二是事件传递的一个桥梁;
本节总结:
本节从事件的发生,事件如何从屏幕传到window上,以及事件在app内部的传递流程等来介绍事件的传递相关内容;回答文章开头的问题。同时在事件传递的过程中很重要的一个方法hitTest方法,对其做简单的说明。
回顾问题:
- 一、事件是如何产生的?
- 二、事件是如何从屏幕中到达window上?
- 三、事件在App内部的传递过程?
补充问题:当在代码实现过程中,发现事件没法响应,应该从哪几个方面去查找?
初学者的简单自学。