## 一、在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么 这其实是一个事件传递和响应链的问题。(其实,按钮点击后,这里还包括runloop的唤醒等知识,不过这点我们放在下一大点讲)。
1
2
3
4
5
6
7
8
9 附:整个响应链及事件链
1、完善响应链查找知识点
2、基础概念等详解
2.1 响应者对象(UIResponder)
2.2、UITouch(点击对象)
2.2.1、UITouch的几个主要属性和方法
2.2.2、UITouch的生成场景
2.3、UIEvent(事件对象)
3、响应链的应用
答:在我们点击按钮的时候,会产生了UITouch(点击对象)和UIEvent(事件对象),这两个对象组合成一个点击事件。而发生触摸事件后,
①消息循环(runloop)/系统就会接收到这个触摸事件,并将它放到一个由UIApplication管理的消息队列(先进先出)里。
②UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理。首先UIApplication将事件传递给的是UIWindow对象(即一般为应用程序的主窗口keyWindow)。
③然后,UIWindow(继承自UIView)对象会继续向它的子View对象传递,直到传递到最上层。(或者说UIWindow使用hitTest:withEvent:方法查找touch操作的所在的视图view)
其中的应用程序逐级寻找能够响应这个事件的对象,直到没有响应者响应。这一寻找的过程,被称作事件的响应链。
二、事件的响应链
事件的响应链大概过程如下图所示:
1、在传递的过程中,下一响应者的查找是通过UIView里的
- pointInside: withEvent:和- hitTest: withEvent:两个方法来确定的。当从最初的只有一个响应者通过这样的方式不断的找到下一响应者后,这些响应者就组成了一个响应者链。2、当通过
- hitTest: withEvent:找到第一响应者后,若第一响应者没有处理事件,则沿着响应者链向上追溯寻找响应者(即灰色箭头方向)执行touches方法。这个过程就是事件的传递过程。从这可以看出它的方向是跟响应链方向相反的。这里我们可以用UITableViewCell中点击上面的label来想象。
附:整个响应链及事件链
整个响应链(向下)及事件链(向上),大概如图所示:
在上图,当- hitTest: withEvent:方法沿着红色箭头方向寻找第一响应者后,若第一响应者没有处理事件,则沿着响应者链向上追溯寻找响应者(即灰色箭头方向)执行touches方法。
所以响应链为红色部分,事件链的顺序可以理解为图上的灰色箭头部分(个人理解)。
1、完善响应链查找知识点
我们已经知道响应者链是由多个响应者组合起来的链条。那么怎么找到这些相应者呢?
响应者的查找为通过UIView内部的下面两个方法来查找的
1 | //根据点击坐标返回事件是否发生在本视图以内 |
对于一个视图
①、若子视图中的
- pointInside: withEvent:方法返回为NO,即判断用户点击的区域不在该子视图范围内的话,则停止对这个子视图里的子视图继续查找,- hitTest: withEvent:返回nil。②、若子视图中的
- pointInside: withEvent:方法返回为YES,即判断用户点击的区域在该子视图范围内的话,则继续往该子视图里的子视图查找,直到没有子视图,然后- hitTest: withEvent:返回这个子视图,而后之前的视图的- hitTest: withEvent:也返回这个子视图。
1
2
3
4
5
6
7
8
9
hitTest-withEvent-查找过程举例,如下图
> <img src="3视图-①本质/hitTest-withEvent-查找过程举例.png" alt="hitTest-withEvent-查找过程举例" style="zoom:33%;" />
> 图片中view等级
```objective-c
[ViewA addSubview:ViewB];
[ViewA addSubview:ViewC];
[ViewB addSubview:ViewD];
[ViewB addSubview:ViewE];
那么点击viewE后,发生的过程是怎样的
1 | 1.A 是UIWindow的根视图,首先对A进行hitTest:withEvent: |
2、基础概念等详解
iOS中的事件可以分为3大类型:
- 触摸事件
- 加速计事件
- 远程控制事件
在iOS中不是任何对象都能处理事件,能接受并这些处理事件的对象只有直接或间接继承自UIResponder的对象,我们称之为“响应者对象”。
2.1 响应者对象(UIResponder)
①、为什么只有继承自UIResponder的类才能够接收并处理事件呢?因为处理这些事件的方法是卸载UIResponder中的啊。详细的UIResponder中提供的方法如下:
1 | 4个处理触摸事件的对象方法 |
附:如何实现UIView的拖拽呢?即让UIView随着手指的移动而移动。
答: 重写touchsMoved:withEvent:方法
代码如下:
1 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{ |
②、那么iOS中能接收并处理事件或者说继承自UIResponder的类有哪些呢?
1
2 iOS中能接收并处理事件或者说继承自UIResponder的类有:
UIApplication、UIWindow、UIViewController和所有继承UIView的UIKit类都直接或间接的继承自UIResponder。
从UIResponder内部提供的方法可以看出,触摸方法接收两个参数,一个UITouch对象的集合,还有一个UIEvent对象。这两个参数分别代表的是点击对象和事件对象。
2.2、UITouch(点击对象)
UITouch表示单个点击,其类文件中存在枚举类型UITouchPhase的属性,用来表示当前点击的状态。这些状态包括点击开始、移动、停止不动、结束和取消五个状态。每次点击发生的时候,点击对象都放在一个集合中传入UIResponder的回调方法中。
2.2.1、UITouch的几个主要属性和方法:
1 | @property(nonatomic,readonly) NSTimeInterval timestamp; // 记录了触摸事件产生或变化时的时间,单位是秒 |
2.2.2、UITouch的生成场景:
前言:每根手指触摸屏幕时都会创建一个与该手指相关的UITouch对象。一根手指对应一个UITouch对象。每个UITouch对象保存着跟手指相关的信息,比如触摸的位置、时间、阶段。
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。
当手指离开屏幕时,系统会销毁相应的UITouch对象实际调用现象举例:
①、当用户用一根手指触摸屏幕时,view会调用1次touchesBegan:withEvent:方法。touches参数中装着1个UITouch对象。
②、如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象。
③、如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
2.3、UIEvent(事件对象)
iOS使用UIEvent表示用户交互的事件对象,在UIEvent.h文件中,我们可以看到有一个UIEventType类型的属性,这个属性表示了当前的响应事件类型。分别有多点触控、摇一摇以及远程操作(在iOS之后新增了3DTouch事件类型)。在一个用户点击事件处理过程中,UIEvent对象是唯一的。
1 | @property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0); |
介绍了以上响应者对象(UIResponder)及其相关的UITouch(点击对象)和UIEvent(事件对象)相关概念后,我们就知道了用户点击后,会产生了UITouch(点击对象)和UIEvent(事件对象)并打包发送,最后由响应者对象(UIResponder)来处理这些事件。
现在的问题是你知道它是怎么通过用户的点击位置找到处理该点击事件的响应者对象吗?
3、响应链的应用
既然已经知道了系统是怎么获取响应视图的流程了,那么我们可以通过重写查找事件处理者的方法来实现不规则形状点击。
最常见的不规则视图就是圆形视图,在demo中我设置view的宽高为200,那么重写方法事件如下:
1 | - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event |
最终的效果图如下:
前面说过按钮点击后,这里还包括一些runloop相关的知识,如唤醒等,所以下面我们就专门开讲一件Runloop。
## 三、CALayer和UIView **UIView与CALayer**是什么关系。<单一职责原则>
UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
CALayer负责显示内容contents
UIView是CALayer的delegate(CALayerDelegate)
UIView继承自UIResponder类,可以响应事件
CALayer直接继承自NSObject类,不可以响应事件
UIView主要处理事件,CALayer负责绘制
每个UIView内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的Layer所提供。两者都有树状层级结构,Layer内部有SubLayers,View内部有SubViews,但是Layer比View多了个AnchorPoint
CALayer的结构图如下:
在 iOS 系统中所有显示的视图都是从基类UIView继承而来的,同时UIView负责接收用户交互。 但是实际上你所看到的视图内容,包括图形等,都是由UIView的一个实例图层属性来绘制和渲染的,那就是CALayer。
CALayer类的概念与UIView非常类似,它也具有树形的层级关系,并且可以包含图片文本、背景色等。它与UIView最大的不同在于它不能响应用户交互,可以说它根本就不知道响应链的存在。
在每一个UIView实例当中,都有一个默认的支持图层,UIView负责创建并且管理这个图层。实际上这个CALayer图层才是真正用来在屏幕上显示的,UIView仅仅是对它的一层封装,实现了CALayer的delegate,提供了处理事件交互的具体功能,还有动画底层方法的高级 API。
以上摘要来自:内存恶鬼drawRect - 谈画图功能的内存优化中的CALayer和UIView介绍部分。
CALayer有三个视觉元素:背景色、内容和边框,其中,内容的本质是一个CGImage
简述CALayer和UIView的关系
答:UIView和CALayer是相互依赖的关系。UIView依赖与calayer提供的内容,CALayer依赖uivew提供的容器来显示绘制的内容。归根到底CALayer是这一切的基础,如果没有CALayer,UIView自身也不会存在,UIView是一个特殊的CALayer实现,添加了响应事件的能力。
结论:
UIView来自CALayer,高于CALayer,是CALayer高层实现与封装。UIView的所有特性来源于CALayer支持。
为什么CLLayer可以在子线程,而UIView不行?
CLLayer可以在子线程中运行,而UIView不行,这主要是因为它们在iOS系统中的职责和设计原则不同。
- 职责不同:
- UIView:UIView是用户界面的基础类,负责处理用户交互(如触摸事件),并且所有的UI更新和渲染都必须在主线程中进行。UIView通过其内部的CALayer来处理内容的显示,UIView本身作为CALayer的delegate,负责响应CALayer的变化并进行相应的UI更新。
- CALayer:CALayer是Core Animation框架的一部分,主要负责内容的绘制和显示。CALayer可以独立于UIView存在,并且可以在子线程中进行绘制操作,因为它不直接处理用户交互,只负责图形的渲染。
- 线程安全:
- UIView:UIKit框架并不是线程安全的,官方建议所有的UI操作都在主线程进行,以避免出现线程安全问题和不可预测的UI行为。
- CALayer:CALayer可以在子线程中进行绘制,因为它支持异步绘制。通过实现CALayer的代理方法displayLayer:,可以在子线程中完成绘制工作,然后将绘制好的图像数据传回主线程更新到CALayer中,这种方式被称为异步绘制。
- 性能优化:
- UIView:由于UIView的所有UI操作都需要在主线程执行,这限制了其在多核CPU上的性能扩展能力。
- CALayer:通过在子线程中进行异步绘制,可以减轻主线程的负担,提高应用的性能和响应速度,尤其是在处理复杂的图形渲染时。
综上所述,CALayer可以在子线程中运行,因为它的设计允许异步绘制,而UIView的所有UI更新必须在主线程中进行,以保证线程安全和UI的响应性。
Flutter 的 RenderObject 和 iOS 中的 CALayer 都扮演着渲染树中节点的角色,它们都负责绘制界面的一部分,并且都可以包含子节点。
在Flutter中,三棵树(Widget树、Element树、RenderObject树)与iOS中的类对应关系如下:
- Widget树:
- Flutter中的Widget树类似于iOS中的UIView控件树。Widget是Flutter中用于描述用户界面的构件,它们是不可变的,并且当状态变化时,Flutter会构建一个新的Widget树。
- Element树:
- Element树在Flutter中扮演着将Widget树的变更映射到RenderObject树的角色。每一个Widget都有一个对应的Element,Element树是Widget树和RenderObject树之间的桥梁。在iOS中,这个概念没有直接对应的类,因为它是Flutter特有的架构。但是,如果非要找一个类比,可以说Element树的作用类似于UIView的实例化和状态管理,因为Element持有Widget和RenderObject的引用,并负责协调它们的状态。
- RenderObject树:
- RenderObject树是Flutter中实际负责布局和绘制的树。它与iOS中的CALayer树相似,因为CALayer负责实际的渲染工作。RenderObject树根据Widget的属性进行布局(layout)和绘制(paint),这与CALayer在iOS中的作用类似。
总的来说,Flutter的Widget树类似于iOS的UIView树,Element树是Flutter特有的,没有直接对应的iOS类,而RenderObject树类似于iOS的CALayer树。这种架构使得Flutter能够在不同的平台上以统一的方式处理渲染和布局。

Element 同时持有 Widget 和 RenderObject。而无论是 Widget 还是 Element,其实都不负责最后的渲染,只负责发号施令,真正去干活儿的只有 RenderObject。那你可能会问,既然都是发号施令,那为什么需要增加中间的这层 Element 树呢?直接由 Widget 命令 RenderObject 去干活儿不好吗?
答案是,可以,但这样做会极大地增加渲染带来的性能损耗。
因为 Widget 具有不可变性,但 Element 却是可变的。实际上,Element 树这一层将 Widget 树的变化(diff)做了抽象,可以只将真正需要修改的部分同步到 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。
这,就是 Element 树存在的意义。
## 常见笔试/面试题 [< 返回目录](#目录)Element 是可复用的,只要 Widget 前后类型一样。比如 Widget 是蓝色的,重建后变红色了,Element 是会复用的。所以多个 Widget(销毁前后)可以对应一个 Element
问:UIButton从子类到父类依次继承自什么?
答:UIControl-> UIView-> UIResponder。
哪些视图的设置能禁止其相应事件
1、userInterface = NO;
2、hidden = YES;
3、当UIBUTTON透明度为0就不响应事件了,当UIBUTTON透明度为0就不响应事件了。
离屏渲染
在使用圆角、阴影和遮罩等视图功能的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所有就需要在屏幕外的上下文中渲染,即离屏渲染。
更多参考:iOS离屏渲染之优化分析该文非常重要。
## END [< 返回目录](#目录)
%E5%8F%8A%E4%BA%8B%E4%BB%B6%E9%93%BE(%E5%90%91%E4%B8%8A).png)

