iOS的事件传递和响应机制

iOS的事件传递和响应机制,第1张

       

   昌樱兄    UIResponder类继承NSObject,在iOS系统中,只有继承了UIResponder才能够响应事件,在iOS体系中,UIApplication、UIViewController和UIView是直接继承UIResponder类的,所以它们能够直接响应事件。特别的是UIWindow继承UIView所以UIWindow也是可以响应事件。在实际开发中,我们可以重写UIResponder 提供的方法来完成特定的需求。如下是UIResponder的部分源码:

       UITapGestureRecognizer类是在iOS3.2才开始提供的,使开发人员更加容易的处理触摸屏幕的事件。UITapGestureRecognizer有7个子类,能够帮助我们处理常见的需求,如用UITapGestureRecognizer可以在UITabelViewCell里的图片识别手势事件。

UIGestureRecognizer的部分源码如下所示:

大致流程如下:

      1. 主窗口接收到应用程序传递过来的事件后,首先判断自己能否接收手触摸事件。如果能,那么再判断触摸点在不在窗口自己身上,执行步骤二,否则丢弃这个事件。

      2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件。(从后往前遍历的个人理解:结合事件响应机制,它与事件传递机制传递的路径相反,向里传递到UIApplication结束。所以此处的从后往前遍历减少了不不要的遍历次数,假设当前view是最适合接收事件的view,那么就不必要遍历该view的父view或者父父view,从而减少遍历次数,而响应事件传递到该view也停止)。

      3.遍耐袭历到每一个子控件后,再判断是否有子控件,然后重复上面的两个步骤:传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上。

      4. 如此循环遍历子控件,直到找到最合适接收事件的view,如果遍历后没有更合适的子控件,那么自己就成为最合适接收事件的view。

      当事件传递到控件时,无论该控件能不能处理事件,该触点在不在该控件上,该控件首先会调用自己的

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

方法寻找最适合接收事件的view。

      官方对这个方法的介绍如下:

      官方建议我们不需要显式调用只需要重写它达到特殊的功能, 比如屏蔽子控件接受事件。

由上描述可知:

      返回该触点是否在view里,YES是在,NO不在。

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

      需要强调的是: 如果子view超出父view的bounds.那么超出部分的view将不会接受到触摸事件颂派。

      上文介绍了事件传递,它的结果找到了最合适接收事件的view。而事件响应是从这最适合的view开始的。 官网链接 。

       如果当前的响应控件不处理事件,那么该事件沿着响应链向上传递,如果响应该事件,则消费该事件,停止在响应链上传递。如果响应链传到UIApplication还没被处理就丢弃。它与查找最合适View的方向相反。 如图3.1所示 苹果官网介绍的响应链介绍的例子。

                                                                              图3.1 点击事件响应链示意图

       解释: 如果子View(UILabel、UITextField、UIBUtton)不处理事件,UIKit发送事件到父UIView对象,然后是窗口的根视图(UIWindow)。在将事件定向到窗口之前,响应器链从根视图转移到所属的视图控制器。如果窗口不能处理事件,UIKit将事件传递给UIApplication对象,特别的,如果该对象是UIResponder的实例,并且不是responder链的一部分,可能还会传递给应用委托。(如果能处理则停止事件传递)。

       特殊事件: 与加速度计、陀螺仪和磁强计相关的运动事件不遵循响应链。而是由Core Motion直接这些事件传递给指定的对象。

**注:

**新手的文章可能有误,请指正。

响应者对象

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。例如常见的 :UIApplication   UIViewController   UIView

UIResponder 可以处理触摸事件、按压事件(3D touch)、远程控制事件、硬件运动事件。

事件的传递

1. 发生触摸事件后,系统会将该事件加入到一个由UIApplication 管理的事件队列中。因为队列的特点是FIFO,即先进先出,先产生的事件先处理(首先接收到事件的是UIApplication)。

2. UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常先发送事件给应用程序的主窗口(keyWindow)。

3. 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

触摸事件的传递是从父控件传递到子控件:  UIApplication->window->寻找处理事件最合适的view

UIView不能接收触摸事件的 4 种情况:

1. 不允许交互 :userInteractionEnabled = NO,当前视图不可交互,该视图上面的子视图也不可与用户交互。用户触发的事件都会被该视图忽略(其他视图照常响应),并且该视图对象也会从事件响应队列中被移除。

2. 隐藏 :如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接收事件

3. 透明度 :如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。

4. 子视图的部分区域超过父视图,也不会接收触摸事件,因为父视图在调用 pointInside方法时会返回NO。说明触摸点不在自己范围内,则当前 view 的hitTest: withEvent:方法返回 nil,当前 view上 的所有 subview 都不做判断。

注意:如果 Touch 位置超过视图边界,hitTest:withEvent 方法将忽略这个视图和它的所有子视图。结果就是,当视图的ciipsToBounds属性为NO,子视图超过视图边界也不会接收到事件 ,即使 触摸点在它上面。

不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理。 系统通过 hitTest:(CGPoint)point withEvent:(UIEvent*)even 找到最适合处理该事件的view。 

应用如何找到最合适的控件来处理事件

1. 首先判断主窗口(keyWindow)自己是否能接受触摸事件 hitTest 方法。

2. 判断触摸点是否在自己身上,通过pointInside 方法来判断。

3. 如果上面 2 步都满足条件,会把这个事件交给 view处理,会对 view的 subviews子控件数据 进行遍历,直至没有更合适的view为止。

注意:采取从数组最后面往前遍历子控件的方式,因为后添加的view在最上面,最上层的响应者能最先接受响应,阻断事件继续传递,从而降低遍历循环次数。

5. 如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,return self 。

寻找最合适的view底层剖析  两个重要的方法:

piontinside方法使用场景 : IOS 增加按钮点击区域 -     使按钮的点击反应区域变大

 -(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event 什么时候调用 

 事件传递给谁,就会调用谁的hitTest:withEvent:方法。

作用

寻找并返回能够响应事件,  最合适的view,不管点击哪里,最合适的view都是 hitTest 方法中返回的那个view。

注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,通过调用 hitTest 方法来判断是否可以处理事件。

拦截事件的处理

通过重写 hitTest  方法,返回指定的view 。就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。

注 意:如果 hitTest  方法中返回 nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。如果同一层级的其他控件也没有合适的view,那么最合适的 view 就是父控件。

不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,子控件调用自己的hitTest:withEvent: 方法后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用。 

技巧: 想让谁成为最合适的view 就重写谁父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是, 建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view 

原因呢:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,想要返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view B。这就导致了返回的不是自己而是触摸点真正所在的view。所以建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view。

找到最合适的view 后,就会调用该view的 touches 方法处理具体的事件。

触摸事件由触屏生成后如何传递到当前应用?

系统响应阶段

用户触摸屏幕,系统硬件进程会获取到这个点击事件,将事件简单处理封装后存到系统中,由于硬件检测进程和当前App进程是两个进程,所以进程两者之间传递事件用的是端口通信。

1. 指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。

2. IOKit 将触摸事件封装成一个IOHIDEvent 对象,并通过mach port传递给SpringBoad进程。mach port 进程端口,各进程之间通过它进行通信。

3. SpringBoad 是一个系统进程,统一管理和分发系统接收到的触摸事件。将触摸事件交给前台app进程来处理。

参考: RunLoop原理学习 -

APP响应阶段

1. APP进程的mach port 接收到 SpringBoard 进程传递来的触摸事件,主线程的 runloop被唤醒,触发了source1回调。

2. source1回调又触发了一个source0回调,将接收到的 IOHIDEvent 对象封装成 UIEvent 对象。

3. source0 回调内部将触摸事件添加到 UIApplication 对象的事件队列中。事件出队后,UIApplication开始寻找最佳响应者,这个过程又称hit-testing。 

3. 系统判断本次触摸是否导致了一个新的事件。如果是,系统会先从响应网中寻找响应链。如果不是,说明该事件是当前正在进行中的事件产生的一个Touch message, 也就是说已经有保存好的响应链

4. 寻找到最佳响应者后,事件就在响应链中的传递及响应了。

响应者链条:由多个响应者对象连接起来的链条

在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。

事件在 响应者 链上传递,最终结果是事件被处理或被抛弃。响应者链条能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。

每一个响应者对象(UIResponder对象)都有一个nextResponder方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的第一响应者确定了,这个事件所处的响应链就确定了。

响应者对象默认的 nextResponder 如下:

1. UIView 的 nextResponder 属性,如果有管理此 view 的 UIViewController 对象,则为此 2. UIViewController 对象;否则 nextResponder 即为其 superview。

3. UIViewController 的 nextResponder 属性为其管理 view 的 superview.

若 VC 是window的根视图rootVC,则其 nextResponder 为 UIWindow ;

若 VC 是从别的控制器present出来的,则其nextResponder为presenting view controller。

4. UIWindow 的 nextResponder 属性为 UIApplication 对象。

5. UIApplication 的 nextResponder 属性为 nil。

若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。 

响应者链的事件传递过程:

1.  如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图

2. 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理

3. 如果window对象也不处理,则其将事件或消息传递给UIApplication对象

4. 如果UIApplication也不能处理该事件或消息,则将其丢弃

响应者对于接收到的事件有3种 *** 作:

1. 不拦截,默认 *** 作.  事件会自动沿着默认的响应链向上传递,(touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理.

UIResponder中的默认实现是什么都不做,但UIKit中UIResponder的直接子类(UIView,UIViewController…) 的默认实现是将事件沿着responder chain继续向上传递到下一个responder,  即nextResponder。

2. 拦截,不再往下分发事件, 重写 touchesBegan:withEvent:进行事件处理,不调用父类的 touchesBegan:withEvent, 事件到这里就结束传递进行处理。

3. 拦截,继续往下分发事件, 重写自己的 touchesBegan:withEvent: 进行事件处理,同时调用 [super  touchesBegan:withEvent:] 将事件往下传递,达到 一个事件多个对象处理 的目的。 

建议使用:[super touchesBegan:touches withEvent:event] 

super 的touches对应方法中默认将事件继续向上传递给 next responder。 

不建议直接向nextResponder发送消息,这样可能会漏掉父类对这一事件的其他处理。

[self.nextResponder  touchesBegan:touches withEvent:event]

手势事件会打断响应链的传递。因为手势比响应链拥有更高的优先级,添加了手势的View 会阻止子View响应链,手势会最先响应,并对事件进行处理。此事件不再响应链中向上传递。

_UIApplicationHandleEventQueue() 识别了一个手势时,首先调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当UIGestureRecognizer 变化(创建/销毁/状态改变)时,回调都会进行处理。

事件的传递和响应的区别:

事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。

可根据一个view 找到它对应的VC控制: 

参考文章:

史上最详细的iOS之事件的传递和响应机制-原理篇 -

iOS 响应者及响应者链 -

iOS - 为什么要在主线程中 *** 作UI -


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/12360956.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-24
下一篇 2023-05-24

发表评论

登录后才能评论

评论列表(0条)

保存