iOS 事件的响应链
1. 基础概念
- UITouch — 手指一次触摸屏幕生成一个UITouch对象
- UIEvent — 系统通过UITouch对象生成UIEvent对象,当有多点触碰的时候,UIEvent对象包含了多个Touch对象,通过allTouches 属性获取
- UIResponder — 每个响应者都派生自 UIResponder 类,本身具有相应事件的能力,响应者默认实现 touchesBegin touchesMove touchesEnded touchesCancelled四个方法。
- Source0 — 事件都是自定义的,非基于端口 port,包括触摸,滚动,selector选择器事件,要手动唤醒 RunLoop 的线程
- Source1 — 基于Port的,包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程
2. 响应一个点击事件的流程:
- 系统内部通过 Source0 把这个屏幕收集到的点击事件分发出去
- 进而把事件的 UITouch 都封装在 UIEvent 里面
- UIApplication 把 Event 传递给UIWindow 做一些逻辑判断工作
- window 把事件传递给上面的子视图
- 开始通过每个view里的 Hit-Test 方法一层一层地 从下往上(这里是相对view显示位置而言,不是view之间的树状结构) 去寻找最佳响应者(运用递归),直到返回一个view(根据递归的性质,最后找到的先输出,也就是找到了最后一个add进来并且符合条件的view就是最佳响应者)
- 本来通过上面找到的view应该是最佳响应者,但是这个view有可能做了不响应处理,所以响应事件还会从上到下去传递事件,直到有人响应为止,如果到最后都没有人响应,事件就会被抛弃。
3. 关键:如何找到最佳响应者?
- 通过递归寻找每个视图和他的子视图,若某个视图能响应了,并且它没有子视图,它理论上就是最佳响应者
4. 注意事项:
- 在找最佳响应者时是对子视图的队列进行倒序遍历,所以一定是从层级最高的那个子视图开始,然后再从这个子视图里面层级最高的子视图里面找,递归下去(不存在同时在两个看似平行的子view上面都有最佳响应者的问题,谁后被add进来,谁就在最上面)
- 普通的UIView是直接继承UIResponder,响应事件通过 touchesBegin 、touchesMove 、touchesEnded、 touchesCancelled 会一级级往下传递
- 通过代码验证:当传递的过程中遇到了 UIControl 的对象(比如button)或者手势,响应链就会被拦截,不再往下传递
- 但是另外通过代码验证:如果当我们一开始就点击添加了手势的view,点击事件不会被里面拦截,还是会通过响应链传下去,再来做手势的target-action