深入浅出iOS事件机制

深入浅出iOS事件机制

2015年 04月 12日

本文章将讲解有关iOS事件的传递机制,如有错误或者不同的见解,欢迎留言指出。转载自:http://zhoon.github.io/ios/2015/04/12/ios-event.html

iOS的事件有好几种:Touch Events(触摸事件)、Motion Events(运动事件,比如重力感应和摇一摇等)、Remote Events(远程事件,比如用耳机上得按键来控制手机),其中最常用的应该就是Touch Events了,基本存在于每个app的每个地方,今天我们主要就讲讲它,至于其他两个事件有兴趣的可以自行查阅资料。

在网页上当我们讲到事件,我们会讲到事件响应链,我们会讲到事件的响应者和事件的传递方式(冒泡),那么在app上,其实也离不开这几个问题,今天我们也重这几个方面来介绍iOS的事件机制: 1、响应链是什么时候怎样构建的? 2、事件第一个响应者是怎么确定的? 3、事件第一个响应者确定后,系统是怎样传递事件的?

响应链的构建

无论是哪种事件,其传递和响应都与响应链息息相关,那么响应链到底是一个什么样的东西呢? 在UIKit中有一个类:UIResponder,我们可以看看头文件的几个属性和方法:

UIResponder是所有可以响应事件的类的基类(从名字应该就可以看出来了),其中包括最常见的UIView和UIViewController甚至是UIApplication,所以我们的UIView和UIViewController都是作为响应事件的载体。

那么响应链跟这个UIResponder有什么关系呢?事实事件响应链的形成和事件的响应和传递,UIResponder都帮我们做了很多事。我们的app中,所有的视图都是按照一定的结构组织起来的,即树状层次结构,每个view都有自己的superView,包括controller的topmost view(controller的self.view)。当一个view被add到superView上的时候,他的nextResponder属性就会被指向它的superView,当controller被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller,而controller的nextResponder会被指向self.view的superView,这样,整个app就通过nextResponder串成了一条链,也就是我们所说的响应链。所以响应链就是一条虚拟的链,并没有一个对象来专门存储这样的一条链,而是通过UIResponder的属性串连起来的。如下图:

Hit-Testing View

文章开头说到有iOS三种event类型,事件传递中UIWindow会根据不同的event,用不同的方式寻找initial object,initial object决定于当前的事件类型。比如Touch Event,UIWindow会首先试着把事件传递给事件发生的那个view,就是下文要说的hit-testview。对于Motion和Remote Event,UIWindow会把例如震动或者远程控制的事件传递给当前的firstResponder,有关firstResponder的相关信息请看这里。下面主要讲Touch Event的hit-testview。

有了事件响应链,接下来的事情就是寻找响应事件的具体响应者了,我们称着为:Hit-Testing View,寻找这个View的过程我们称着为Hit-Test。

那么什么是Hit-Test呢,我们可以把它理解为一个探测器,通过这个探测器我们可以找到并判断手指是否点击在某个视图上面,换句话说就是通过Hit-Test可以找到手指点击到的处于屏幕最前面的那个UIView。

在解释Hit-Test是怎么工作之前,先来看看它是什么时候被调用的。前面说Hit-Test是一个探测器,那么在代码里面其实就是一个函数,UIView有如下两个方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

每当手指接触屏幕,UIApplication接收到手指的事件之后,就会去调用UIWindow的hitTest:withEvent:,看看当前点击的点是不是在window内,如果是则继续依次调用subView的hitTest:withEvent:方法,直到找到最后需要的view。调用结束并且hit-test view确定之后,这个view和view上面依附的手势,都会和一个UITouch的对象关联起来,这个UITouch会作为事件传递的参数之一,我们可以看到UITouch头文件里面有一个view和gestureRecognizers的属性,就是hitTest view和它的手势。

现在知道Hit-Test是什么时候调用了,那么接下来看看它是怎么工作的。Hit-Test是采用递归的方法从view层级的根节点开始遍历,看看下面这张图:

UIWindow有一个MianVIew,MainView里面有三个subView:view A、view B、view C,他们各自有两个subView,他们层级关系是:view A在最下面,view B中间,view C最上(也就是addSubview的顺序,越晚add进去越在上面),其中view A和view B有一部分重叠。如果手指在view B.1和view A.2重叠的上面点击,按照上面说的递归方式,顺序如下图所示:

递归是向界面的根节点UIWindow发送hitTest:withEvent:消息开始的,从这个消息返回的是一个UIView,也就是手指当前位置最前面的那个 hittest view。 当向UIWindow发送hitTest:withEvent:消息时,hitTest:withEvent:里面所做的事,就是判断当前的点击位置是否在window里面,如果在则遍历window的subview然后依次对subview发送hitTest:withEvent:消息(注意这里给subview发送消息是根据当前subview的index顺序,index越大就越先被访问)。如果当前的point没有在view上面,那么这个view的subview也就不会被遍历了。当事件遍历到了view B.1,发现point在view B.1里面,并且view B.1没有subview,那么他就是我们要找的hittest view了,找到之后就会一路返回直到根节点,而view B之后的view A也不会被遍历了。

一图胜千言:

注意hitTest里面是有判断当前的view是否支持点击事件,比如userInteractionEnabled、hidden、alpha等属性,都会影响一个view是否可以相应事件,如果不响应则直接返回nil。 我们留意到还有一个pointInside:withEvent:方法,这个方法跟hittest:withEvent:一样都是UIView的一个方法,通过他开判断point是否在view的frame范围内。如果这些条件都满足了,那么遍历就可以继续往下走了,代码表现大概如下:

Hit-Test的应用

一、扩大view的点击区域

一个按钮尺寸是10pt*10pt,如果要扩大按钮的点击区域(按钮四周之外的10pt也可以响应按钮的事件),可以怎么做呢?或许重写hittest:withEvent:是个好办法,hitest就是返回可以响应事件的view,如果我们在button的子类里面重写它,在方法里面判断如果point在button的frame之外的10pt内,就返回button自己。

二、将事件传递给兄弟view

如上面第一个图,如果需要是需要view A响应事件而不是B(即使点在重叠的部分),什么都不做的话,当点击在重叠的时候,A是不能响应事件的,除非B的userInteractionEnabled为NO并且者B没有任何事件的响应函数。这个时候通过重写B的hittest可以解决这个问题,在B的hittest里面直接返回nil就行了。

三、将事件传递给subview

如下图,蓝色的scrollView设置pagingEnabled使得image停止滚动后都会固定在居中的位置,如果在scrollView的左边或者右边活动,发现scrollView是无法滚动的,原因就是hittest里面没有满足pointInSide这个条件,scrollView的bound只有蓝色的区域。这个时候重写UIView的hittest:withEvent:,然后返回scrollView即可解决问题。

事件的传递

有了响应链,并且找到了第一个响应事件的对象,接下来就是把事件发送个这个响应者了。 UIApplication中有个sendEvent:的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的接口,把事件发送给hit-testview。这个我们可以从Time Profiler里面得到证实:

当我点击了WRBuyBookButton之后,UIWindow会通过一个私有方法,在里面会去调用按钮的touchesBegan和touchesEnded方法,touchesBegan里面有设置按钮的高亮等之类的动作,这样就实现了事件的传递。而事件的响应,也就是按钮上绑定的action,是在touchEnded里面通过调用UIApplication的sendAction:to:from:forEvent:方法来实现的,至于这个方法里面是怎么去响应action,就只能猜测了(可能是通过oc底层消息机制的相关接口 objc_msgSend 来发送消息实现的,可以参考message.h文件)。如果第一响应者没有响应这个事件,那么就会根据响应链,把事件冒泡传递给nextResponder来响应。

注意这里是怎么把事件传递给nextResponder的呢?拿touch事件来说,UIResponder里面touch四个阶段的方法里面,实际上是什么事都没有做的,UIView继承了它进行重写,重写的内容也是没有什么东西,就是把事件传递给nextResponder,比如:[self.nextResponder touchesBegan:touches withEvent:event]。所以当一个view或者controller里面没有重写touch事件,那么这个事件就会一直传递下去,直到UIApplication,这也就是事件往上冒泡的原理。如果view重写了touch方法,我们一般会看到的效果是,这个view响应了事件之后,事件就被截断了(就像JavaScript里面调用e.stopPropagation()),它的nextResponder不会收到这个事件,即使重写了nextResponder的touch方法。这个时候如果想事件继续传递下去,可以调用[super touchesBegan:touches withEvent:event],不建议直接调[self.nextResponder touchesBegan:touches withEvent:event]。

关于UIScrollView的事件

先说一个现象,我们平时加到UIScrollView(或者UITableView和UICollection)上面的UIButton,即使有设置highLighted的样式,点击的时候却发现这个样式老是不出来,但是按钮的事件明明可以响应的,很诡异。

后来才知道,UIScrollView因为要滚动,所以对事件做了特殊的处理: 当UIScrollView接收到事件之后,会暂时劫持当前的事件300毫秒,如果300毫秒之后手指还没有滚动,则认为你放弃滚动,放弃对事件的劫持并往下传递,但是从Time Profiler看到此时按钮并不是调用自身的touch方法,而是调用自身绑定的手势的touch事件,由于按钮的highLighted样式是写在按钮的touch方法上的,所以这个这个时候就看不到高亮了。但是长按按钮缺可以让按钮有高亮的状态,这个就不太清楚为什么了,因为从Time Profiler里面看按钮的touchesBegan好像还是没有被调。 如果300毫秒之内手指滚动了,则响应滚动的事件,事件就不会继续传给subView了,也就是不会继续调用按钮上手势的touch方法了。

可以通过UIScrollView的一个属性来解决这个问题:delaysContentTouches,意思是是否需要延迟处理事件的传递,默认是NO。把delaysContentTouches设置为YES之后,一切看起来挺好的,按钮终于有高亮样式了哈哈哈,但是发现另一个问题:如果手指点击在按钮上面并滚动UIScrollView,发现怎么也滚动不了。原因是当手指点击UIScrollView并在滚动之前,如果subView接收并且可以响应事件(delaysContentTouches设置为YES),则事件响应链会在subView响应事件之后就截断,即UIScrollView本身不会响应到此事件,不会发生滚动。可以设置canCancelContentTouches为YES来让UIScrollView可以滚动,与之类似的还有一个touchesShouldCancelInContentView:接口,可以根据参数view来更方便的判断是否需要cancel,如果有需要可以在UIScrollView的子类里面重写这个接口。

这一块里面的具体实现原理我们都不知道,水太深了,只能通过Time Profiler来看到一些大概的实现,我们也没必要去深究,大方向理解就好了。真的有兴趣的同学也可以去研究研究,期待你的分享。

参考资料:

1、http://smnh.me/hit-testing-in-ios/

2、http://southpeak.github.io/blog/2015/03/07/uiresponder/

3、https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIScrollView_Class/index.html#//apple_ref/doc/uid/TP40006922

4、https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/event_delivery_responder_chain/event_delivery_responder_chain.html

5、https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW2

6、https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW2

ZhoonChen

时间: 2025-01-04 01:52:05

深入浅出iOS事件机制的相关文章

iOS事件机制(一)

iOS事件机制(一) DEC 7TH, 2013 运用的前提是掌握掌握的本质是理解 本篇内容将围绕iOS中事件及其传递机制进行学习和分析.在iOS中,事件分为三类: 触控事件(单点.多点触控以及各种手势操作) 传感器事件(重力.加速度传感器等) 远程控制事件(远程遥控iOS设备多媒体播放等) 这三类事件共同构成了iOS设备丰富的操作方式和使用体验,本次就首先来针对第一类事件:触控事件,进行学习和分析. Gesture Recognizers Gesture Recognizers是一类手势识别器

iOS事件机制(二)

iOS事件机制(二) DEC 29TH, 2013 本篇内容接上一篇iOS事件机制(一),本次主要介绍iOS事件中的多点触控事件和手势事件. 从上一篇的内容我们知道,在iOS中一个事件用一个UIEvent对象表示,UITouch用来表示一次对屏幕的操作动作,由多个UITouch对象构成了一个UIEvent对象.另外,UIResponder是所有响应者的父类,UIView.UIViewController.UIWindow.UIApplication都直接或间接的集成了UIResponder.关于

jQuery中的事件机制深入浅出

昨天呢,我们大家一起分享了jQuery中的样式选择器,那么今天我们就来看一下jQuery中的事件机制,其实,jQuery中的事件机制与JavaScript中的事件机制区别是不大的,只是,JavaScript中调用是原生的函数方法,而jQuery中调用的绑定的是jQuery中的对象方法,那么在昨天的第一篇中,我们已经说过了jQuery对象和DOM对象之间的转换,至于其中的转换的原理,我们就需要去分析一下jQuery中的源码了,这个我们在源码分析中再去做讨论, 首先呢,我们先来看一下,jQuery中

深入浅出--iOS的TCP/IP协议族剖析&&Socket

深入浅出--iOS的TCP/IP协议族剖析&&Socket 简介 该篇文章主要回顾--TCP/IP协议族中的TCP/UDP.HTTP:还有Socket.(--该文很干,酝酿了许久!你能耐心看完吗?) 我在这个文章中,列举了常见的TCP/IP族中的协议,今天主角是--传输层协议. 传输层(Transport Layer)是OSI(七层模型)中最重要.最关键的一层,它负责总体的数据传输和数据控制的一层,传输层提供端到端(应用会在网卡注册一个端口号)的交换数据的机制,检查分组编号与次序.传输层对

9 WPF之深入浅出话事件

转:http://blog.csdn.net/fwj380891124/article/details/8139260 就像属性系统在WPF中得到了升级.进化为依赖属性一样,事件系统在WPF也得到了升级.进化成为了路由事件(Routed Event),并在其基础上衍生出命令传递机制.这些机制在很大程度上减少了对程序员的束缚,让程序的设计和实现更加的灵活,模块之间的耦合度也进一步降低,这一章我们一起来领略一下新消息机制的风采. 8.1 近观WPF的树形结构 路由一词的意思大概是这样:起点和终点间有

《转之微信移动团队微信公众号》iOS 事件处理机制与图像渲染过程

致歉声明: Peter在开发公众号功能时触发了一个bug,导致群发错误.对此我们深表歉意,并果断开除了Peter.以下交回给正文时间: iOS 事件处理机制与图像渲染过程 iOS RunLoop都干了什么 iOS 为什么必须在主线程中操作UI 事件响应 CALayer CADisplayLink 和 NSTimer iOS 渲染过程 渲染时机 CPU 和 GPU渲染 Core Animation Facebook Pop介绍 AsyncDisplay介绍 参考文章 iOS RunLoop都干了什

iOS 事件处理机制与图像渲染过程

iOS 事件处理机制与图像渲染过程 iOS RunLoop都干了什么 iOS 为什么必须在主线程中操作UI 事件响应 CALayer CADisplayLink 和 NSTimer iOS 渲染过程 渲染时机 CPU 和 GPU渲染 Core Animation Facebook Pop介绍 AsyncDisplay介绍 参考文章 iOS RunLoop都干了什么 RunLoop是一个接收处理异步消息事件的循环,一个循环中:等待事件发生,然后将这个事件送到能处理它的地方. 如图1-1所示,描述了

【移动端兼容问题研究】javascript事件机制详解(涉及移动兼容)--转

前言 javascript事件基础 事件捕获/冒泡 事件对象 事件模拟 移动端响应速度 PC与移动端鼠标事件差异 touch与click响应速度问题 结论 zepto事件机制 注册/注销事件 zepto模拟tap事件 tap事件的问题一览 点透问题 fastclick思想提升点击响应 实现原理 鬼点击 ios与android鼠标事件差异 事件捕获解决鬼点击 结语 前言 这篇博客有点长,如果你是高手请您读一读,能对其中的一些误点提出来,以免我误人子弟,并且帮助我提高 如果你是javascript菜

深入浅出js事件

深入浅出js事件 一.事件流 事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念是为了解决页面中事件流(事件发生顺序)的问题. <div id="outer"> <p id="inner">Click me!</p> </div> 上面的代码当中一个div元素当中有一个p子元素,如果两个元素都有一个click的处理函数,那么我们怎么才能知道哪一个函数会首先被触发呢? 为了解决这个问题微软和网景提出了两种几乎完全相反