建材秒知道
登录
建材号 > 链条 > 正文

iOS面试题:讲讲iOS事件响应链的原理

冷静的冥王星
知性的萝莉
2022-12-31 20:03:05

iOS面试题:讲讲iOS事件响应链的原理

最佳答案
欣喜的舞蹈
风中的胡萝卜
2025-04-22 11:17:09

1、响应者链通常是由视图(UIView)构成的;

2、一个视图的下一个响应者是它视图控制器(UIViewController)(如果有的话),然后再转给它的父视图(Super View);

3、视图控制器(如果有的话)的下一个响应者为其管理的视图的父视图;

4、单例的窗口(UIWindow)的内容视图将指向窗口本身作为它的下一个响应者

需要指出的是,Cocoa Touch应用不像Cocoa应用,它只有一个UIWindow对象,因此整个响应者链要简单一点;

5、单例的应用(UIApplication)是一个响应者链的终点,它的下一个响应者指向nil,以结束整个循环。

更多: iOS面试题合集

最新回答
激情的泥猴桃
无限的蜜粉
2025-04-22 11:17:09

注 意 : 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

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

注 意 :默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互,需要设置UIImageView的userInteractionEnabled = YES。

1.点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

2.UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。

3.窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO那么传递下来的事件就会由该view的父控件处理。

例如,不想让蓝色的view接收事件,那么可以设置蓝色的view的userInteractionEnabled = NO那么点击黄色的view或者蓝色的view所产生的事件,最终会由橙色的view处理,橙色的view就会成为最合适的view。

所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!也就是说,如果蓝色视图不能处理事件,点击蓝色视图产生的触摸事件不会由被点击的视图(蓝色视图)处理!

注意:如果设置父控件的透明度或者hidden,会直接影响到子控件的透明度和hidden。如果父控件的透明度为0或者hidden = YES,那么子控件也是不可见的!

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

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

2.触摸点是否在自己身上

3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)

4.如果没有符合条件的子控件,那么就认为自己最合适处理

详述:1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上

 2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)

 3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)

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

找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。

注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。

两个重要的方法:

hitTest:withEvent: 方法

pointInside 方法

什么时候调用?

作用

注 意 :不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法

拦截事件的处理

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

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

所以事件的传递顺序是这样的:

产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回 更合适 的view->[子控件 hitTest:withEvent:]->返回 最合适 的view

事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。

不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。

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

原因 在于在自己的hitTest:withEvent:方法中返回自己有时候会出现问题。因为会存在这么一种情况:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是触摸点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!

例如: whiteView有redView和greenView两个子控件。redView先添加,greenView后添加。如果要求无论点击那里都要让redView作为最合适的view(把事件交给redView来处理)那么只能在whiteView的hitTest:withEvent:方法中return self.subViews[0]这种情况下在redView的hitTest:withEvent:方法中return self是不好使的!

特殊情况:

谁都不能处理事件,窗口也不能处理。

只能有窗口处理事件。

return nil的含义:

hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的view不是合适的view,子控件也不是合适的view。如果同级的兄弟控件也没有合适的view,那么最合适的view就是父控件。

寻找最合适的view底层剖析之hitTest:withEvent:方法底层做法

hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。

pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

屏幕上现在有一个viewA,viewA有一个subView叫做viewB,要求触摸viewB时,viewB会响应事件,而触摸viewA本身,不会响应该事件。如何实现?

1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理

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

响应者对象: 能处理事件的对象,也就是继承自UIResponder的对象

作用: 能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。

如何判断上一个响应者

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

事件处理的整个流程总结:

1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。

3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

4.最合适的view会调用自己的touches方法处理事件

5.touches默认做法是把事件顺着响应者链条向上抛。

touches的默认做法:

事件的传递与响应:

1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication ->UIWindow ->UIView ->initial view,以上就是事件的传递,也就是寻找最合适的view的过程。

2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[supertouches….]就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法

如何做到一个事件多个对象处理:

因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。

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

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

怕孤独的野狼
丰富的秋天
2025-04-22 11:17:09

1.发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的队列事件中

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

3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件

事件的具体传递过程,如图:

一般事件的传递是从父控件传递到子控件的

例如:

点击了绿色的View,传递过程如下:UIApplication->Window->白色View->绿色View

点击蓝色的View,传递过程如下:UIApplication->Window->白色View->橙色View->蓝色View

如果父控件接受不到触摸事件,那么子控件就不可能接收到触摸事件

应用如何找到最合适的控件来处理事件?有以下准则

详述:

1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上

2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)

3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)

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

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

寻找最合适的view过程,如图:

这里点击了橙色的那块区域,事件传递判断过程如下:

1.UIApplication从事件队列中取出事件分发给UIWindow

2.UIWindow判断自己是否能接受触摸事件,可以

3.UIWindow判断触摸点是否在自己身上,是的。

4.UIWindow从后往前便利自己的子控件,取出白1(a.UIWindow的子控件只有一个,那就是白1)

5.白1都满足最上面两个条件,遍历子控件橙2

特别说明:

( a.白1的子控件有两个,绿2和橙2 )

( b.添加顺序是,先添加绿2,后添加橙2 )

( c.根据后添加的子控件先遍历的原则,肯定是先遍历橙2子控件 )

6.橙2都满足最上面两个条件,遍历子控件,先取出红3

( a.橙2的子控件有两个,蓝3和红3,注意黄4不属于橙2的子控件而是蓝3的子控件 )

( b.添加顺序是,先添加蓝3,后添加红3 )

( c.根据后添加的子控件先遍历的原则,肯定是先遍历红3子控件 )

7.红3不满足条件2,取出蓝3

8.蓝3也不满足条件2,最后最合适的控件是橙2

寻找合适的View用到两个重要方法:

什么时候调用?

只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法寻找合适的View

作用

寻找并返回最合适的view(能够响应事件的那个最合适的view)

注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,

事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法

hitTest:withEvent:底层调用流程:

事件传递给窗口或控件后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。

不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。

以上是事件传递的顺序:

上文介绍了事件的传递过程,找到合适的View之后就会调用该view的touches方法要进行响应处理具体的事件,找不到最合适的view,就不会调用touches方法进行事件处理。

这里先介绍一下响应者链条:响应者链条其实就是很多响应者对象(继承自UIResponder的对象)一起组合起来的链条称之为响应者链条

一般默认做法是控件将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理 (即调用super的touches方法)。

那么如何判断当前响应者的上一个响应者是谁呢?有以下两个规则:

1.判断当前是否是控制器的View,如果是控制器的View,上一个响应者就是控制器

2.如果不是控制器的View,上一个响应者就是父控件

响应过程如下图:

touch响应:

如果控制器也不响应响应touches方法,就交给UIWindow。如果UIWindow也不响应,交给UIApplication,如果都不响应事件就作废了。

最后总结来说一次完整的触摸事件的传递响应过程为:

UIApplication-->UIWindow-->递归找到最合适处理的控件-->控件调用touches方法-->判断是否实现touches方法-->没有实现默认会将事件传递给上一个响应者-->找到上一个响应者-->找不到方法作废

一句话总结整个过程是:触摸或者点击一个控件,然后这个事件会从上向下(从父->子)找最合适的view处理,找到这个view之后看他能不能处理,能就处理,不能就按照事件响应链向上(从子->父)传递给父控件

额外添加:

有了响应网为基础,事件的传递就比较简单,只需要选择其中一条响应链,但是选择那一条响应链来传递呢?为了弄清真个过程,我们先来查看一下从触摸硬件事件转化为UIEvent消息。

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

其次,APP启动主线程RunLoop会注册一个端口事件,来检测触摸事件的发生。当时事件到达,系统会唤起当前APP主线程的Runloop。唤起原因就是端口触摸事件,主线程会分析这个事件。

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

神勇的小虾米
整齐的小天鹅
2025-04-22 11:17:09

Runtime是iOS核心运行机制之一,iOS App加载库、加载类、执行方法调用,全靠Runtime,这一块的知识个人认为是最基础的,基本面试必问。

1)iOS调用一个方法时,实际上会调用objc_msgSend(receiver, selector, arg1, arg2, ...),该方法第一个参数是消息接收者,第二个参数是方法名,剩下的参数是方法参数;

2)iOS调用一个方法时,会先去该类的方法缓存列表里面查找是否有该方法,如果有直接调用,否则走第3)步;

3)去该类的方法列表里面找,找到直接调用,把方法加入缓存列表;否则走第4)步;

4)沿着该类的继承链继续查找,找到直接调用,把方法加入缓存列表;否则消息转发流程;

很多面试者大体知道这个流程,但是有关细节不是特别清楚。

如果在消息发送阶段没有找到方法,iOS会走消息转发流程,流程图如下所示:

1)动态消息解析。检查是否重写了resolveInstanceMethod 方法,如果返回YES则可以通过class_addMethod 动态添加方法来处理消息,否则走第2)步;

2)消息target转发。forwardingTargetForSelector 用于指定哪个对象来响应消息。如果返回nil 则走第3)步;

3)消息转发。这步调用 methodSignatureForSelector 进行方法签名,这可以将函数的参数类型和返回值封装。如果返回 nil 执行第四步;否则返回 methodSignature,则进入 forwardInvocation ,在这里可以修改实现方法,修改响应对象等,如果方法调用成功,则结束。否则执行第4)步;

4)报错 unrecognized selector sent to instance。

很多人知道这四步,但是笔者一般会问:

+load在main函数之前被Runtime调用,+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。

笔者在面试过程中发现很多人对于load与initialize在分类、继承链的调用顺序不清楚。对怎么保证初始化安全也不清楚

RunLoop苹果原理图

图中展现了 Runloop 在线程中的作用:从 input source 和 timer source 接受事件,然后在线程中处理事件。

两个都是 Runloop 事件的来源,其中 Input Source 又可以分为三类

因为定时器默认是运行在NSDefaultRunLoopMode,在列表滚动时候,主线程会切换到UITrackingRunLoopMode,导致定时器回调得不到执行。

有两种解决方案:

iOS 检测到手指触摸 (Touch) 操作时会将其打包成一个 UIEvent 对象,并放入当前活动Application的事件队列,UIApplication 会从事件队列中取出触摸事件并传递给单例的 UIWindow 来处理,UIWindow 对象首先会使用 hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为 hit-test view。

hitTest:withEvent:方法的处理流程如下:

iOS的事件分发机制是为了找到第一响应者,事件的处理机制叫做响应者链原理。

所有事件响应的类都是 UIResponder 的子类,响应者链是一个由不同对象组成的层次结构,其中的每个对象将依次获得响应事件消息的机会。当发生事件时,事件首先被发送给第一响应者,第一响应者往往是事件发生的视图,也就是用户触摸屏幕的地方。事件将沿着响应者链一直向下传递,直到被接受并做出处理。一般来说,第一响应者是个视图对象或者其子类对象,当其被触摸后事件被交由它处理,如果它不处理,就传递给它的父视图(superview)对象(如果存在)处理,如果没有父视图,事件就会被传递给它的视图控制器对象 ViewController(如果存在),接下来会沿着顶层视图(top view)到窗口(UIWindow 对象)再到程序(UIApplication 对象)。如果整个过程都没有响应这个事件,该事件就被丢弃。一般情况下,在响应者链中只要由对象处理事件,事件就停止传递。

一个典型的事件响应路线如下:

First Responser -->父视图-->The Window -->The Application -->nil(丢弃)

我们可以通过 [responder nextResponder] 找到当前 responder 的下一个 responder,持续这个过程到最后会找到 UIApplication 对象。

1) 在VC的cellForRowAtIndexPath方法中cell的block直接引用self或者直接以_形式引用属性造成循环引用。

解决方案:把self改成weakSelf;

2)在cell的block中直接引用VC的成员变量造成循环引用。

解决方案有两种:

3)delegate属性声明为strong,造成循环引用。

解决方案:delegate声明为weak

4)在block里面调用super,造成循环引用。

解决方案,封装goback调用

5)block声明为strong

解决方案:声明为copy

6)NSTimer使用后不invalidate造成循环引用。

解决方案:

考察viewDidLoad、viewWillAppear、ViewDidAppear等方法的执行顺序。

假设现在有一个 AViewController(简称 Avc) 和 BViewController (简称 Bvc),通过 navigationController 的push 实现 Avc 到 Bvc 的跳转,调用顺序如下:

1、A viewDidLoad

2、A viewWillAppear

3、A viewDidAppear

4、B viewDidLoad

5、A viewWillDisappear

6、B viewWillAppear

7、A viewDidDisappear

8、B viewDidAppear

如果再从 Bvc 跳回 Avc,调用顺序如下:

1、B viewWillDisappear

2、A viewWillAppear

3、B viewDidDisappear

4、A viewDidAppear

如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群 931542608 来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

活泼的铃铛
含糊的路灯
2025-04-22 11:17:09
按照时间顺序,事件的生命周期是这样的:

事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)->找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。

UIApplication

UIViewController

UIView(superView、subView)

事件的传递:是从上到下(父控件到子控件)

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

当用户用手指触摸屏幕时,会创建一个与手指相关的UITouch对象。

触摸事件的传递是从父控件传递到子控件

也就是UIApplication->window->寻找处理事件最合适的view

注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

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

2.判断触摸点是否在自己身上

3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)

4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。

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

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

1.点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

2.UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。

3.窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

//==================" 系统框架 "==================

//==================" 类别 "==================

羞涩的网络
平常的烤鸡
2025-04-22 11:17:09

这里主要讲解记录下用户触摸点击手机屏幕后产生的事件是如何派发传递的,如何查找到适合响应事件的第一响应者控件,以及找到响应者后事件是如何通过响应链向下传递的,直到事件被接收并做出具体处理或被废弃。事件响应链也是面试中经常会被问起的知识点!

第一响应者一般指的是用户当前触摸的响应者对象,表示当前该对象正在与用户交互,第一响应者是响应者链的开端。

具有响应和处理iOS事件能力的对象,也就是继承UIResponder的类的对象。我们常用的UIApplication、UIWindow、UIViewController、UIView、UIScene(iOS13以后)都是继承或间接UIResponder类,所以他们的实例对象都可以成为响应者对象。

类的继承关系:

由多个不同响应者对象链接起来构成的一个链条;

iOS中事件主要分为三大类:

1、Touch Event (触摸事件)

2、Motion Event (运动事件)

3、Remote-ControlEvent(远程控制事件)

测试展示图

当用户触摸屏幕时,系统会检测到屏幕上的压力感知到触摸事件,iOS系统检测到触摸操作后会将这个事件打包成一个UIEvent对象,并将该事件加入到一个由UIApplication管理的事件队列中,然后UIApplication会从事件队列中取出触摸事件并传递给UIWindow处理,keyWindow会使用hitTest:withEvent:方法寻找一个最合适的响应者来处理事件,一般寻找到的适合处理事件的控件是touch操作初始点的视图,找到合适第一响应者的视图控件后,就会调用该视图控件的touches方法来处理具体的事件,这个过程称之为hit-test。

事件的传递是由父控件向子控件传递的,例如上面的view层次图,viewA、viewB、viewE被添加到rootView中,viewC、viewD是viewB的子view。加入用户点击viewC的时间传递链是

传递方向:由底层系统向可以响应事件的控件传递

UIKit→UIApplication的事件队列→keyWindow→rootView→一些列subView→事件响应view

主要方法:

流程图:

以测试展示图为例,假设用户点击viewC后的处理流程

由是第一响应者的控件向系统传递

事件响应view→superView→rootVIew→viewController→window→Application→AppDelegate

说明示图:

输出的log 显示VC.nextUIResponder = VC.View.superView

凶狠的哈密瓜,数据线
心灵美的果汁
2025-04-22 11:17:09
所有继承响应者对象UIResponder都能接收并处理事件。按照时间顺序,先找到到最合适的view,然后就会调用view的touches方法,这些方法的默认做法是将事件顺着响应这链条向上传递,将事件交由上一个响应者进行处理,直到有一个view能处理该响应为止或者丢弃。这是一个从上到下,再从下到上的过程。

事件的传递先从父控件传递到子控件(UIApplication->window->寻找处理事件最合适的view)。

如果父view不能接受触摸事件,那么子view也不能接收到触摸事件。

有两个重要的方法:

view会调用hitTest:withEvent:方法,hitTest:withEvent:方法底层会调用pointInside:withEvent:方法判断触摸点是不是在这个view的坐标系上。如果在坐标系上,会分发事件给这个view的子view。然后每个字view重复以上步骤,直至最底层的一个合适的view。

事件响应会先从底层最合适的view开始,然后随着上一步找到的链一层一层响应touch事件。默认touch事件会传递给上一层。如果到了viewcontroller的view,就会传递给viewcontroller。如果viewcontroller不能处理,就会传递给UIWindow。如果UIWindow无法处理,就会传递给UIApplication。如果UIApplication无法处理,就会传递给UIApplicationDelegate。如果UIApplicationDelegate不能处理,则会丢弃该事件。

高高的乐曲
个性的灰狼
2025-04-22 11:17:09
在 UIKit 中我们使用响应者对象(Responder)接收和处理事件。一个响应者对象一般是 UIResponder 类的实例,它常见的子类包括 UIView,UIViewController 和 UIApplication,这意味着几乎所有我们日常使用的控件都是响应者,如 UIButton,UILabel 等等。

点击后,事件传递由Application->UIWindow->ViewController->View这样传递上来(事件传递是由父到子)

在这过程中,系统会去调用两个方法,如下

hitTest遍历寻找first responder 的规则:

1、调用pointInside来判断点击是否落在当前视图

2、如果pointInside返回NO,则hitTest:withEvent返回nil,查找其他同级的view

3、如果pointInside返回YES,则继续遍历subViews(最后添加的view优先被查找,FILO原则),向subView发送hitTest查找响应的view。

4、如果有子视图返回非空对象,那么hiTest:withEvent返回此对象,处理结束。(该子视图没有subview,否则应该继续遍历该子视图的subView)

5、如果全部的子视图都返回nil,则hitTest:withEvent返回自身(self)

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

参考:

https://juejin.cn/post/6894518925514997767

娇气的大白
留胡子的秀发
2025-04-22 11:17:09
响应者对象

在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 -