iOS-完整的触摸事件传递过程
知 识 点 / 超 人
触摸事件分为四个阶段进行传递:
1.硬件响应阶段
2.系统响应阶段
3.桌面响应阶段
4.应用响应阶段
- 4.1事件传递
- 4.2寻找最佳响应者
- 4.3事件传递的响应链
硬件响应阶段
该阶段是用户触摸到电容屏,电容屏响应并产生触摸信号的阶段
知识点补充:
电容屏原理(百度百科)
电容技术触摸面板CTP是利用人体的电流感应进行工作的。电容屏是一块四层复合玻璃屏,玻璃屏的内表面和夹层各涂一层ITO(纳米铟锡金属氧化物),最外层是只有0.0015mm厚的矽土玻璃保护层,夹层ITO涂层作工作面,四个角引出四个电极,内层ITO为屏层以保证工作环境。
当用户触摸电容屏时,由于人体电场,用户手指和工作面形成一个耦合电容,因为工作面上接有高频信号,于是手指吸收走一个很小的电流,这个电流分别从屏的四个角上的电极中流出,且理论上流经四个电极的电流与手指头到四角的距离成比例,控制器通过对四个电流比例的精密计算,得出位置。可以达到99%的精确度,具备小于3ms的响应速度。电容屏触摸点计算:
电容屏是在两层ITO导电玻璃涂层上蚀刻出不同的ITO导电线路模块。两个模块上蚀刻的图形相互垂直,可以把它们看作是X和Y方向 连续变化的滑条。由于X、Y架构在不同表面,其相交处形成一电容节点。一个滑条可以当成驱动线,另外一个滑条当成是侦测线。当电流经过驱动线中的一条导线时,如果外界有电容变化的信号,那么就会引起另一层导线上电容节点的变化。侦测电容值的变化可以通过与之相连的电子回路测量得到,再经由A/D控制器转为数字讯号让计算机做运算处理取得(X,Y) 轴位置,进而达到定位的目地。
多点操作时,控制器先后供电流给驱动线,因而使各节点与导线间形成一特定电场。然后逐列扫描感测线测量其电极间的电容变化量,从而达成多点定位。
响应时间:响应时间定义为,触摸屏上手指触碰事件与触摸屏控制器产生中断信号之间的时间。
响应步骤:
*第一步: ->用户触摸电容屏
*第二步: ->电容屏根据电流产生反应
*第三步: ->电容屏控制芯片计算出触控点产生电容信号
*第四步: ->CPU处理信号
系统响应阶段
该阶段是iOS系统对CPU指令封装的阶段
知识点补充:
IOKit.framework
是与硬件或内核服务通信的底层框架,主要是面向硬件驱动开发者。在 IOKit 里面,所有的通信都通过 IOKit Master Port 来进行。虽然这是一个公共框架,但苹果不鼓励开发者使用它,任何使用它的应用程序都将被AppStore拒绝。这里有苹果官方对IOKit.framework的说明IPC 通信:跨进程通信。
Mach Port
是在系统内核实现和维护的一种 IPC 消息队列,它持有用于 IPC 通信的 mach messages,可以看做是一个单向的数据发送渠道
,构建一个消息结构体后通过mach_msg() 方法发送消息出去。只有一个进程可以从对应的 port 里 dequeue 一条消息,这个进程被称为接收端(receive-right)
。可以有多个进程往某个 port 里 enqueue 消息,这些进程称为发送端(send-rights)
。因为只能单向发送,所以当 B 进程收到了 A 进程发来的消息之后要自己创建一个新的 Port 然后又发回去 A 进程。B要发消息给A,必须先知道 A进程的 port 信息我们才能往里面发消息。XNU 系统提供了bootstrap port
这个东西,由系统提供查询服务,这样所有的进程都可以去广播自己的 mach port 接收端的名字,也可以查询其他进程的名字。
响应步骤:
*第一步: ->CPU处理信号转为指令
*第二步: ->iOS系统的输入输出驱动IOKit.framework接收
*第三步: ->IOKit封装触摸事件为IOHIDEvent对象
*第四步: ->IOKit通过mach prot(IPC)将IOHIDEvent事件转发给SpringBoard.app
桌面响应阶段
该阶段是iOS桌面进程响应处理阶段
知识点补充:
APP运行的过程本质上是处理各种事件的过程,把事件进行分类,系统层事件、应用层事件、特殊事件。source1基本就是系统事件,source0基本就是应用层事件。
SpringBoard.app
用来管理iOS的主屏幕的应用程序,启动WindowSever(窗口服务器),bootstrapping(引导应用程序),以及在启动时候系统的一些初始化设置都是由这个特定的应用程序负责的。当触摸事件发生时,只有负责管理屏幕的SpringBoard.app才知道如何正确的响应处理事件。因为只有SpringBoard.app才知道屏幕处于什么状态,是有应用在前台活跃着,还是没有应用在前台,如果没有应用在前台则说明在桌面,则由SpringBoard.app进入内部响应,触发Source0回调。如果有应用在前台则通过mach port(IPC)转发给对应App。Source1
基于 mach port的处理事件 ,苹果注册了一个 Source1 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。触发回调后,会调用 _UIApplicationHandleEventQueue() 进行应用内部的事件分发。Source0
非基于Port的 处理事件,什么叫非基于Port的呢?就是说这个消息不是其他进程或者内核直接发送的。一般是APP内部的事件, 比如hitTest:withEvent的处理, performSelectors的事件。
该阶段分为两种情况,无前台App的响应步骤、有前台App的响应步骤。本质上来说是一样的。因为SpringBoard.app本身也是App
无前台App的响应步骤:
*第一步:->SpringBoard.app的主线程的MainRunloop收到IOKit.framework转发来的消息后苏醒
*第二步:->SpringBoard.app通过注册的source1的回调函数__IOHIDEventSystemClientQueueCallback()接收IOHIDEvent事件
*第三步:->SpringBoard.app判断是否由谁响应IOHIDEvent事件
*第四步:->发现无前台App
*第五步:->触发SpringBoard.app内部的source0回调函数__UIApplicationHandleEventQueue()
*第六步:->由SpringBoard.app内部自行处理
有前台App的响应步骤:
*第一步:->SpringBoard.app的主线程的MainRunloop收到IOKit.framework转发来的消息后苏醒
*第二步:->SpringBoard.app通过注册的source1的回调函数__IOHIDEventSystemClientQueueCallback()接收IOHIDEvent事件
*第三步:->SpringBoard.app判断是否由谁响应IOHIDEvent事件
*第四步:->发现有前台App
*第五步:->通过mach port将IOHIDEvent事件转发给对应前台App
应用响应阶段
该阶段是App内部的响应处理阶段,比较复杂。内容比较多,因此知识点补充放在了最后,请耐心看。
我把该阶段细分为三个小阶段:
第一阶段:->事件传递
第二阶段:->寻找最佳响应者
第三阶段:->事件传递的响应链
第一阶段: 事件传递响应步骤:
*第一步: ->App主线程的MainRunloop收到消息后苏醒
*第二步: ->通过App的source1的回调函数__IOHIDEventSystemClientQueueCallback()接收IOHIDEvent事件
*第三步: ->source1内部触发source0的回调函数__UIApplicationHandleEventQueue()
*第四步: ->source0内部对IOHIDEvent进行封装处理成UIEvent
*第五步: ->soucre0内部调用UIApplication的sendEvent:方法,将UIEvent传给UIWindow(不一定是KeyWindow,如果有多个window,则会传递给最上层的window)
第二阶段: 寻找最佳响应者:
*第一步: ->UIWindow通过hitTest:withEvent:与pointInside:withEvent:方法从UIView层级的最顶层往最底层递归查询由那一层View是最佳的事件响应者。
*第二步: -> 首先通过hitTest:withEvent:方法判断该视图是否能响应事件,userInteractionEnabled、hidden、alpha三个属性判断该界面是否能响应事件
*第三步: -> 然后通过pointInside:withEvent:方法判断触摸点是否在该View中
*第四步: -> 最后判断该View是否有子视图,如果有子视图则递归判断子视图是否能响应,如果能响应则在hitTest:withEvent:方法中返回子视图作为最佳响应者,如果子视图不能响应,则返回本身View作为最佳响应者
第三阶段: 事件传递的响应链:
该阶段有两种情况,有手势识别器、没有手势识别器。
有手势识别器的传递:
*第一步: ->判断是否有手势识别器
*第二步: ->有手势识别器
*第三步: ->首先把事件传递给手势识别器,手势识别器接收事件,调用touchesBegan方法,并进入手势识别器UIGestureRecognizer的响应链传递
*第四步: ->然后把事件传递给最佳响应者,调用最佳响应者自身的touchesBegan方法,并进入UIResponder的响应链传递。
*第五步: ->手势识别器自底向上的进行事件传递,直到上面某一层不响应则断层,会分别调用每层响应者手势识别器的touchesMoved、touchesEnded方法。
*第六步: ->手势识别器最终成功识别手势并执行任务
*第七步: ->手势识别器调用响应者的touchesCancelled方法来终止最佳响应者对于事件的响应。(因此最佳响应者自身并不会调touchesMoved、touchesEnded方法)
没有手势识别器的传递:
*第一步: ->判断是否有手势识别器
*第二步: ->没有手势识别器
*第三步: ->把事件传递给最佳响应者,调用最佳响应者自身的touchesBegan方法,并进入UIResponder的响应链传递。
*第四步: ->最佳响应者自底向上的进行事件传递,直到上面某一层不响应则断层,会分别调用每层响应者的touchesMoved、touchesEnded方法。
*第五步: ->最佳响应者最终成功识别事件并执行任务
知识点补充:
响应链
响应链传递流程上图是官网对于响应链的示例展示,如果最佳响应者对象是UITextField,则响应链为:
UITextField->UIView->UIView->UIViewController->UIWindow->UIApplication->UIApplicationDelegate
如果一直没有UIResponder做响应处理,则这些UITouches到达最后的响应者即UIApplication后,就被忽略而消失。
UIGestureRecognizer、UIResponder、UIControl
这里只做简单的介绍,不做深入探讨
UIGestureRecognizer
手势识别器,继承NSObject,UIGestureRecognizer是一个抽象类
,因此我们只能使用它的子类:
UITapGestureRecognizer
(点击)
UIPinchGestureRecognizer
(捏合)
UIRotationGestureRecognizer
(旋转)
UISwipeGestureRecognizer
(滑动,快速移动,是用于监测滑动的方向的)
UIPanGestureRecognizer
(拖移,慢速移动,是用于监测偏移的量的)
UILongPressGestureRecognizer
(长按)手势识别是具有
互斥原则
的,例如 Tap 一次与Tap 兩次、Tap 与 LongPress、Swipe与 Pan。比如单击和双击,如果它识别出一种手势,其后的手势将不被识别。所以对于关联手势,要做特殊处理来帮助程序进行甄别(requireGestureRecognizerToFail:方法)。应该先把当前手势归于一类手势里面,然后做一个特殊处理逻辑,判断手势是否是双击,如果是双击则相应双击事件,如果双击失效,则作为单击手势处理。
其核心就是设置delegate和在需要手势监测的view上使用addGestureRecognizer添加指定的手势监测。
当然要记得在作为delegate的view的头文件加上<UIGestureRecognizerDelegate>。
常用属性介绍
//当前手势的状态
@property(nonatomic,readonly) UIGestureRecognizerState state;
//设置代理
@property(nullable,nonatomic,weak) id <UIGestureRecognizerDelegate> delegate;
//设置手势是否有效
@property(nonatomic, getter=isEnabled) BOOL enabled;
//获取手势所在的view
@property(nullable, nonatomic,readonly) UIView *view;
//获取触发触摸的点
-(CGPoint)locationInView:(nullable UIView*)view;
//设置触摸点数
-(NSUInteger)numberOfTouches;
//获取某一个触摸点的触摸位置
-(CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(nullable UIView*)view;
//调用对象是需要失效的手势,参数对象是生效的手势。如果生效手势失效,则调用对象手势生效
-(void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;
//获取到的是手指移动后,在相对坐标中的偏移量
-(CGPoint)translationInView:(nullable UIView *)view;
/**
当这个属性设置为YES时,默认为YES
如果识别到了手势,系统将会发送touchesCancelled:withEvent:消息在其事件传递的响应链上,终止触摸事件的传递。
设置为NO时
则不会终止事件的传递
*/
@property(nonatomic) BOOL cancelsTouchesInView;
/**
当这个属性设置为NO时,默认为NO
手势识别器识别手势期间,触摸对象就算状态发生变化,也会发送事件给最佳响应者
设置为YES时
识别手势期间,触摸状态若发生变化,则不会发送事件给最佳响应者。
*/
@property(nonatomic) BOOL delaysTouchesBegan;
/**
当这个属性设置为NO时,默认为NO
在手势识别成功后,发送touchesCancelled,手势识别失败时,发送touchesEnded
设置为YES时
在手势识别成功后,发送touchesCancelled,手势识别失败时,会延迟大概0.15ms,期间没有接收到别的touch才会发送touchesEnded
*/
@property(nonatomic) BOOL delaysTouchesEnded
UIGestureRecognizerDelegate方法介绍
//手指触摸屏幕后回调的方法,返回NO则不再进行手势识别,方法触发等
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
//开始进行手势识别时调用的方法,返回NO则结束,不再触发手势
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
//是否支持多时候触发,返回YES,则可以多个手势一起触发方法,返回NO则为互斥
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
//下面这个两个方法也是用来控制手势的互斥执行的
//这个方法返回YES,第一个手势和第二个互斥时,第一个会失效
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
//这个方法返回YES,第一个和第二个互斥时,第二个会失效
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
手势状态介绍
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible, // 已经触发了触摸事件,但尚未识别是何种手势操作,默认状态
UIGestureRecognizerStateBegan, // 手势已经开始,此时已经被识别,但是这个过程中可能发生变化,手势操作尚未完成
UIGestureRecognizerStateChanged, // 手势状态发生转变
UIGestureRecognizerStateEnded, // 手势识别操作完成(此时已经松开手指)
UIGestureRecognizerStateCancelled, // 手势被取消,恢复到默认状态
UIGestureRecognizerStateFailed, // 手势识别失败,恢复到默认状态
UIGestureRecognizerStateRecognized // 手势识别完成,同UIGestureRecognizerStateEnded
UIGestureRecognizerStateEnded // 手势识别完成
};
UIResponder
是专门用来响应用户的操作处理和各种事件的类,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events),UIResponder是一个抽象类
,因此我们主要是使用它的子类:
UIApplication
UIViewController
UIView
UIWindow
(它能响应事件是因为它的父类是UIView,而UIView是UIResponder的子类)
常用方法介绍
//对象是否能成为第一响应者 YES表示能成为
- (BOOL)canBecomeFirstResponder
//设置对象成为第一响应者
- (void)becomeFirstResponder
//对象是否能失去第一响应者的资格 YES表示能
- (BOOL)canResignFirstResponder
//取消对象的第一响应者资格
- (void)resignFirstResponder
//手指触碰屏幕,触摸开始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移动
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指离开屏幕,触摸结束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸结束前,某个系统事件中断了触摸,例如电话呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
UIControl
是UIView的子类,因此也是UIResponder的子类,UIControl是一个抽象类
,因此我们主要使用它的子类:
UIButton
UISwitch
UITextField
UIControl采用了一种不同于UIResponder的处理机制,它将标准的触摸事件转换封装成为易于使用的特殊的控件事件,例如通过UIControl对象处理后,按下按钮的事件就被封装成一个控件事件,而不用去判断触摸屏幕的整个操作过程。
例如使用继承之UIControl的UIButton时,为UIButton对象添加触摸事件
例如:
UIButton *button = [UIButton new];
[button addTarget:self action:@selector(buttonAction) forControlEvents:UIControlEvents];
button
:是要响应事件的控件对象。
self
:是要把触摸事件的消息发送到哪个对象里去。
@selector(buttonAction)
:后面是一个方法选择器,表示该事件需要响应的方法,方法里面是执行的任务;
UIControlEvents
:是事件类型,表示响应什么样的事件。
UIControlEvents事件类型介绍
typedef NS_OPTIONS(NSUInteger, UIControlEvents) {
UIControlEventTouchDown = 1 << 0, // 单点触摸按下事件:用户点触屏幕或者又有新的手指落下的时候。
UIControlEventTouchDownRepeat = 1 << 1, //多点触摸按下事件,点触计数大于1:用户按下第二、三、或第四根手指的时候。
UIControlEventTouchDragInside = 1 << 2, //当一次触摸在控件窗口内拖动时。
UIControlEventTouchDragOutside = 1 << 3, //当一次触摸在控件窗口之外拖动时。
UIControlEventTouchDragEnter = 1 << 4, //当一次触摸从控件窗口之外拖动到内部时。
UIControlEventTouchDragExit = 1 << 5, //当一次触摸从控件窗口内部拖动到外部时。
UIControlEventTouchUpInside = 1 << 6, //所有在控件之内触摸抬起事件。
UIControlEventTouchUpOutside = 1 << 7, //所有在控件之外触摸抬起事件(点触必须开始与控件内部才会发送通知)。
UIControlEventTouchCancel = 1 << 8, //所有触摸取消事件,即一次触摸因为放上了太多手指而被取消,或者被上锁或者电话呼叫打断。
UIControlEventValueChanged = 1 << 12, // 当控件的值发生改变时,发送通知。用于滑块、分段控件、以及其他取值的控件。你可以配置滑块控件何时发送通知,在滑块被放下时发送,或者在被拖动时发送。
UIControlEventPrimaryActionTriggered API_AVAILABLE(ios(9.0)) = 1 << 13, // 按钮触发的语义动作
UIControlEventEditingDidBegin = 1 << 16, // 当文本控件中开始编辑时发送通知。
UIControlEventEditingChanged = 1 << 17, //当文本控件中的文本被改变时发送通知。
UIControlEventEditingDidEnd = 1 << 18, //当文本控件中编辑结束时发送通知。
UIControlEventEditingDidEndOnExit = 1 << 19, // 当文本控件内通过按下回车键(或等价行为)结束编辑时,发送通知。
UIControlEventAllTouchEvents = 0x00000FFF, // 通知所有触摸事件。
UIControlEventAllEditingEvents = 0x000F0000, // 通知所有关于文本编辑的事件。
UIControlEventApplicationReserved = 0x0F000000, // 应用保留事件
UIControlEventSystemReserved = 0xF0000000, // 系统保留事件
UIControlEventAllEvents = 0xFFFFFFFF //通知所有事件
};
UIGestureRecognizer、UIResponder、、UIControl的优先级
UIGestureRecognizer优先级 大于 UIResponder优先级 ,UIResponder优先级 与 UIControl优先级 相同
最后补充一张图片
触摸事件流程图