iPhone拥有很好的用户交互体验,这源于iOS系统对交互事件的高效处理和高优响应;
App开发者处理用户交互很是便捷,这源于iOS系统和UIKit对用户操做作了封装和默认处理;
本文围绕iOS的事件传递和处理,探究其具体过程。php
这里讲的事件是用户交互的抽象,像IOHIDEvent和UIEvent都是不一样处理阶段的封装。html
IOHIDEvent是iOS系统对事件的封装,感兴趣能够看源码IOHIDEvent.h和IOHIDEvent.cpp(HID是Human Interface Device的缩写)。node
UIEvent是UIKit封装的描述用户操做类型的对象,可能有touch事件、motion事件、remote-control事件、press事件等。不一样事件在响应链中处理方式不一样,这里咱们主要分析touch事件的传递和处理。git
App外:用户点击->硬件响应->参数量化->数据转发->App接收。github
在用户触摸屏幕以后,屏幕硬件会接受用户的操做,并采集关键的参数传递给IOKit,而IOKit将这些数据打包并传给SpringBoard.app,继而转发给前台App。数组
App内:子线程接收事件->主线程封装事件->UIWindow启动hitTest肯定目标视图->UIApplication开始发送事件->touch事件开始回调。markdown
App启动时便会启动一个com.apple.uikit.eventfetch-thread子线程,负责接收SpringBoard.app转发过来的数据(经过runloop监听source1,查看堆栈中有__CFRunLoopDoSource1),数据会被封装成IOHIDEvent对象,而后转发给主线程;app
主线程一样在启动时监听source0,接收eventfetch-thread线程发送的IOHIDEvent数据,再封装成UIEvent,根据UIEvent的类型判断是否须要启动hitTest。motion事件不须要hitTest,touch事件也有部分不须要hitTest,好比说touch结束触发的事件。iphone
肯定目标视图以后,UIApplication便会发送事件,将UITouch和UIEvent发送给目标视图,触发其touches系列的方法。ide
寻找的过程主要依赖两个UIView的方法:-hitTest:withEvent方法和-pointInsdie:withEvent方法。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
复制代码
hitTest方法返回point和event对应的视图;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
复制代码
pointInside方法返回point和event是否在本身当前视图上;
这两个方法UIView都提供了默认实现,hitTest方法默认会调用全部子视图的hitTest方法,若是有一个返回。
UIKit会从UIWindow开始寻找目标视图,先调用UIWindow的hitTest方法询问是否有响应的视图,hitTest方法首先会先调用UIWindow的pointInside方法询问是否在点击范围内。
a.若是pointInside方法返回NO,则证实UIWindow没法响应该事件,hitTest方法会立刻返回nil;
b.若是pointInside方法返回YES,则证实UIWindow能够响应该事件,hitTest方法会接着调用UIWindow子视图的hitTest方法。
=> b1.若是子视图hitTest方法若是有返回视图,则UIWindow的hitTest方法会返回该视图;
=> b2.若是全部子视图hitTest方法都没有返回视图,则UIWindow的hitTest方法会返回本身。
UIWindow是UIView的子类,UIView的hitTest方法实现和上述过程一致。
思考: UIView在调用子视图hitTest时,是先调用哪些子视图?
从subview数组的末尾开始调用hitTest,subview数组下标越小,视图层级越低。
当UIKit肯定目标视图以后,就会建立UITouch,UITouch的window属性和view属性就是上面过程当中的UIWindow和目标视图。
接着UIApplication就会调用sendEvent:方法,接着UIWindow在sendEvent:方法中会调用sendTouchesForEvent:方法,以下图:
UIWindow的sendTouchesForEvent:方法调用的是咱们熟悉的touches四大方法:
-touchesBegan:withEvent:
-touchesMoved:withEvent:
-touchesEnded:withEvent:
-touchesCancelled:withEvent:
从上一步寻找到的目标视图开始,目标视图会首先被调用touches方法,接着是目标视图的父视图,再是父视图的父视图,若是某个视图是ViewController的.view属性,还会调用ViewController的方法,直到UIWindow、UIApplication、UIApplicationDelegate(咱们建立的AppDelegate)。
下面是官方文档给出的回调顺序:(Responder chains in an app)
手势(UIGestureRecognizer)是iPhone的重要交互方式,手势识别 介绍了手势是如何识别,甚至能够添加自定义手势。
UIGestureRecognizer一样有touches系列方法:
手势处理的发生时机咱们能够经过手势的touchesBegan:withEvent:方法来看,当咱们断点在手势的touchesBegan方法时,咱们看到堆栈:
注意到堆栈中的UIApplication的sendEvent:方法,sendEvent是发生在UIKit寻找目标视图过程以后。从另一种角度来思考,touchesBegan方法中会用到UITouch,而UITouch中的view属性是目标视图,因此手势的处理应该也放在UIKit寻找目标视图以后。
当手势的touchesBegan:withEvent:处理完成以后,便会触发目标视图的touchesBegan方法。
可是当手势识别成功以后,默认会cancel后续touch操做,从目标视图开始的响应链都会收到touchesCancelled方法,而不是正常的touchesEnded方法,堆栈以下:
这个行为也能够经过设置下面的cancelsTouchesInView=NO来避免触发touchesCancelled方法。
注意到无论是手势处理开始的touchesBegan方法,仍是手势识别成功后触发touchesCancelled方法,堆栈中都有一个UIGestureEnvironment类。这是一个UIKit的私有类,在网上搜到相关代码介绍:
@interface UIGestureEnvironment : NSObject {
NSMutableArray * _delayedPresses;
NSMutableArray * _delayedPressesToSend;
NSMutableArray * _delayedTouches;
NSMutableArray * _delayedTouchesToSend;
UIGestureGraph * _dependencyGraph;
NSMutableArray * _dirtyGestureRecognizers;
bool _dirtyGestureRecognizersUnsorted;
struct __CFRunLoopObserver { } * _gestureEnvironmentUpdateObserver;
NSMutableSet * _gestureRecognizersNeedingRemoval;
NSMutableSet * _gestureRecognizersNeedingReset;
NSMutableSet * _gestureRecognizersNeedingUpdate;
NSMapTable * _nodesByGestureRecognizer;
bool _updateExclusivity;
}
- (void)addGestureRecognizer:(id)arg1;
- (void)addRequirementForGestureRecognizer:(id)arg1 requiringGestureRecognizerToFail:(id)arg2;
- (bool)gestureRecognizer:(id)arg1 requiresGestureRecognizerToFail:(id)arg2;
- (id)init;
- (void)removeGestureRecognizer:(id)arg1;
...
复制代码
从头文件的方法声明,咱们能够大概知道这是一个手势管理类,手势的添加、移除、响应都在内部完成。
思考:
一、UIButton的点击回调是怎么实现的?
二、若是给UIButton添加Tap手势,点击UIButton的时候是触发UIButton的Tap手势,仍是触发UIButton的点击回调?
因此综上三步,咱们能够知道整个流程大概是:
iOS的用户交互相关很是复杂。因为时间有限,这里仅仅从事件的传递和处理出发,来创建一个基础的认知。
手势识别 developer.apple.com/documentati…
响应链介绍 developer.apple.com/documentati…
一、UIButton的点击回调是怎么实现的?
UIButton是UIControl的子类,经过追踪touch事件的变化获得一些UIControl定义的事件(UIControlEvents);UIButton的点击操做是经过UIControlEvents的事件变化回调来触发,本质依赖的是响应链回调过程当中的touches系列方法。
二、若是给UIButton添加Tap手势,点击UIButton的时候是触发UIButton的Tap手势,仍是触发UIButton的点击回调?
上文分析了手势的识别是发生在响应链回调以前,也就是tap手势是发生在touches系列方法回调以前,那么Tap手势应该是在UIButton的touches方法以前。若是UIButton监听的是经常使用的UIControlEventTouchUpInside事件,则不会回调;若是监听的是UIControlEventTouchCancel事件,则在触发完Tap手势以后,还会收到回调。