视图-①本质

视图-①本质

[toc]

目录

1
2
3
4
5
6
7
8
9
10
附:整个响应链及事件链
1、完善响应链查找知识点
2、基础概念等详解
2.1 响应者对象(UIResponder)
2.2、UITouch(点击对象)
2.2.1、UITouch的几个主要属性和方法
2.2.2、UITouch的生成场景
2.3、UIEvent(事件对象)
3、响应链的应用
>

一、在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么

这其实是一个事件传递和响应链的问题。(其实,按钮点击后,这里还包括runloop的唤醒等知识,不过这点我们放在下一大点讲)。

答:在我们点击按钮的时候,会产生了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来想象。

附:整个响应链及事件链

整个响应链(向下)及事件链(向上),大概如图所示:

响应链(向下)及事件链(向上)及事件链(向上).png)
在上图,当- hitTest: withEvent:方法沿着红色箭头方向寻找第一响应者后,若第一响应者没有处理事件,则沿着响应者链向上追溯寻找响应者(即灰色箭头方向)执行touches方法。
所以响应链为红色部分,事件链的顺序可以理解为图上的灰色箭头部分(个人理解)。

1、完善响应链查找知识点

我们已经知道响应者链是由多个响应者组合起来的链条。那么怎么找到这些相应者呢?

响应者的查找为通过UIView内部的下面两个方法来查找的

1
2
3
4
5
//根据点击坐标返回事件是否发生在本视图以内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds

// 返回响应点击事件的对象(当点击区域在分为内时候,如果有子视图则返回子视图里最终的响应者,如果没有子视图则返回自身)
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

对于一个视图

①、若子视图中的- pointInside: withEvent:方法返回为NO,即判断用户点击的区域不在该子视图范围内的话,则停止对这个子视图里的子视图继续查找,- hitTest: withEvent:返回nil。

②、若子视图中的- pointInside: withEvent:方法返回为YES,即判断用户点击的区域在该子视图范围内的话,则继续往该子视图里的子视图查找,直到没有子视图,然后- hitTest: withEvent:返回这个子视图,而后之前的视图的- hitTest: withEvent:也返回这个子视图。

  • 1
     

hitTest-withEvent-查找过程举例,如下图

hitTest-withEvent-查找过程举例
图片中view等级

1
2
3
4
[ViewA addSubview:ViewB];
[ViewA addSubview:ViewC];
[ViewB addSubview:ViewD];
[ViewB addSubview:ViewE];

那么点击viewE后,发生的过程是怎样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.A 是UIWindow的根视图,首先对A进行hitTest:withEvent:
2.判断A的userInteractionEnabled,如果为NO,A的hitTest:withEvent返回nil;
3.pointInside:withEvent:方法判断用户点击是否在A的范围内,显然返回YES
4.遍历A的子视图B和C。由于从后向前遍历,因此先查看C,再查看B。
>
4.1 查看C:
调用C的hitTest:withEvent方法:pointInside:withEvent:方法判断用户点击是否在C的范围内,不在返回NO,C对应的hitTest:withEvent: 方法return nil;
>
4.2 再查看B
调用B的hitTest:withEvent方法:pointInside:withEvent:判断用户点击是否在B的返回内,在返回YES。
>遍历B的子视图D和E,从后向前遍历,所以先查看E,再查看D。
4.2.1先查看E,调用E的hitTest:withEvent方法:pointInside:withEvent:方法 判断用户点击是否在E的范围内,在返回YES,E没有子视图,因此E对应的hitTest:withEvent方法返回E,再往前回溯,就是B的hitTest:withEvent方法返回E,因此A的hitTest:withEvent方法返回E。
4.2.2查看D,略
>
至此,点击事件的第一响应者就找到了。

2、基础概念等详解

iOS中的事件可以分为3大类型:

  1. 触摸事件
  2. 加速计事件
  3. 远程控制事件

在iOS中不是任何对象都能处理事件,能接受并这些处理事件的对象只有直接或间接继承自UIResponder的对象,我们称之为“响应者对象”。

2.1 响应者对象(UIResponder)

①、为什么只有继承自UIResponder的类才能够接收并处理事件呢?因为处理这些事件的方法是卸载UIResponder中的啊。详细的UIResponder中提供的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
4个处理触摸事件的对象方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

和3个处理加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

以及1个处理远程控制事件的方法
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

附:如何实现UIView的拖拽呢?即让UIView随着手指的移动而移动。

答: 重写touchsMoved:withEvent:方法

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
// 想让控件随着手指移动而移动,监听手指移动
// 获取UITouch对象
UITouch *touch = [touches anyObject];
// 获取当前点的位置
CGPoint curP = [touch locationInView:self];
// 获取上一个点的位置
CGPoint preP = [touch previousLocationInView:self];
// 获取它们x轴的偏移量,每次都是相对上一次
CGFloat offsetX = curP.x - preP.x;
// 获取y轴的偏移量
CGFloat offsetY = curP.y - preP.y;
// 修改控件的形变或者frame,center,就可以控制控件的位置
// 形变也是相对上一次形变(平移)
// CGAffineTransformMakeTranslation:会把之前形变给清空,重新开始设置形变参数
// make:相对于最原始的位置形变
// CGAffineTransform t:相对这个t的形变的基础上再去形变
// 如果相对哪个形变再次形变,就传入它的形变
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

②、那么iOS中能接收并处理事件或者说继承自UIResponder的类有哪些呢?

1
2
3
> iOS中能接收并处理事件或者说继承自UIResponder的类有:
UIApplication、UIWindow、UIViewController和所有继承UIView的UIKit类都直接或间接的继承自UIResponder。
>

从UIResponder内部提供的方法可以看出,触摸方法接收两个参数,一个UITouch对象的集合,还有一个UIEvent对象。这两个参数分别代表的是点击对象和事件对象。

2.2、UITouch(点击对象)

UITouch表示单个点击,其类文件中存在枚举类型UITouchPhase的属性,用来表示当前点击的状态。这些状态包括点击开始、移动、停止不动、结束和取消五个状态。每次点击发生的时候,点击对象都放在一个集合中传入UIResponder的回调方法中。

2.2.1、UITouch的几个主要属性和方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@property(nonatomic,readonly) NSTimeInterval      timestamp;    // 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) UITouchPhase phase; // 当前触摸事件所处的状态
@property(nonatomic,readonly) NSUInteger tapCount; // 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0);


@property(nullable,nonatomic,readonly,strong) UIWindow *window; //触摸产生时所处的窗口
@property(nullable,nonatomic,readonly,strong) UIView *view; //触摸产生时所处的视图
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);

/**
* 获取当前点击位置的坐标点
*
* @param view 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
*
* @return 返回值表示触摸在view上的位置点(这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0)))
*/
- (CGPoint)locationInView:(nullable UIView *)view;

/// 获取前一个触摸点位置的坐标点
- (CGPoint)previousLocationInView:(nullable UIView *)view;
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);


typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),
};

typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,

// for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,

// for UIEventTypeRemoteControl, available in iOS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104,
UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

介绍了以上响应者对象(UIResponder)及其相关的UITouch(点击对象)和UIEvent(事件对象)相关概念后,我们就知道了用户点击后,会产生了UITouch(点击对象)和UIEvent(事件对象)并打包发送,最后由响应者对象(UIResponder)来处理这些事件。

现在的问题是你知道它是怎么通过用户的点击位置找到处理该点击事件的响应者对象吗?

3、响应链的应用

既然已经知道了系统是怎么获取响应视图的流程了,那么我们可以通过重写查找事件处理者的方法来实现不规则形状点击。

最常见的不规则视图就是圆形视图,在demo中我设置view的宽高为200,那么重写方法事件如下:

1
2
3
4
5
6
7
8
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
const CGFloat halfWidth = 100;
CGFloat xOffset = point.x - 100;
CGFloat yOffset = point.y - 100;
CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset);
return radius <= halfWidth;
}

最终的效果图如下:
响应链的应用1_点击不规则图形

前面说过按钮点击后,这里还包括一些runloop相关的知识,如唤醒等,所以下面我们就专门开讲一件Runloop。

三、CALayer和UIView

UIView与CALayer是什么关系。

<单一职责原则>
UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
CALayer负责显示内容contents

UIViewCALayerdelegate(CALayerDelegate)

UIView继承自UIResponder类,可以响应事件

CALayer直接继承自NSObject类,不可以响应事件

UIView主要处理事件,CALayer负责绘制

每个UIView内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的Layer所提供。两者都有树状层级结构,Layer内部有SubLayersView内部有SubViews,但是LayerView多了个AnchorPoint

CALayer的结构图如下:

CALayer的结构图

在 iOS 系统中所有显示的视图都是从基类UIView继承而来的,同时UIView负责接收用户交互。 但是实际上你所看到的视图内容,包括图形等,都是由UIView的一个实例图层属性来绘制和渲染的,那就是CALayer。

CALayer类的概念与UIView非常类似,它也具有树形的层级关系,并且可以包含图片文本、背景色等。它与UIView最大的不同在于它不能响应用户交互,可以说它根本就不知道响应链的存在。

在每一个UIView实例当中,都有一个默认的支持图层,UIView负责创建并且管理这个图层。实际上这个CALayer图层才是真正用来在屏幕上显示的,UIView仅仅是对它的一层封装,实现了CALayer的delegate,提供了处理事件交互的具体功能,还有动画底层方法的高级 API。

以上摘要来自:内存恶鬼drawRect - 谈画图功能的内存优化中的CALayer和UIView介绍部分。

CALayer有三个视觉元素:背景色、内容和边框,其中,内容的本质是一个CGImage

CALayer和UIView

简述CALayer和UIView的关系

答:UIView和CALayer是相互依赖的关系。UIView依赖与calayer提供的内容,CALayer依赖uivew提供的容器来显示绘制的内容。归根到底CALayer是这一切的基础,如果没有CALayer,UIView自身也不会存在,UIView是一个特殊的CALayer实现,添加了响应事件的能力

结论:
UIView来自CALayer,高于CALayer,是CALayer高层实现与封装。UIView的所有特性来源于CALayer支持。

为什么CLLayer可以在子线程,而UIView不行?

CLLayer可以在子线程中运行,而UIView不行,这主要是因为它们在iOS系统中的职责和设计原则不同。

  1. 职责不同
    • UIView:UIView是用户界面的基础类,负责处理用户交互(如触摸事件),并且所有的UI更新和渲染都必须在主线程中进行。UIView通过其内部的CALayer来处理内容的显示,UIView本身作为CALayer的delegate,负责响应CALayer的变化并进行相应的UI更新。
    • CALayer:CALayer是Core Animation框架的一部分,主要负责内容的绘制和显示。CALayer可以独立于UIView存在,并且可以在子线程中进行绘制操作,因为它不直接处理用户交互,只负责图形的渲染。
  2. 线程安全
    • UIView:UIKit框架并不是线程安全的,官方建议所有的UI操作都在主线程进行,以避免出现线程安全问题和不可预测的UI行为。
    • CALayer:CALayer可以在子线程中进行绘制,因为它支持异步绘制。通过实现CALayer的代理方法displayLayer:,可以在子线程中完成绘制工作,然后将绘制好的图像数据传回主线程更新到CALayer中,这种方式被称为异步绘制。
  3. 性能优化
    • UIView:由于UIView的所有UI操作都需要在主线程执行,这限制了其在多核CPU上的性能扩展能力。
    • CALayer:通过在子线程中进行异步绘制,可以减轻主线程的负担,提高应用的性能和响应速度,尤其是在处理复杂的图形渲染时。

综上所述,CALayer可以在子线程中运行,因为它的设计允许异步绘制,而UIView的所有UI更新必须在主线程中进行,以保证线程安全和UI的响应性。

Flutter 的 RenderObject 和 iOS 中的 CALayer 都扮演着渲染树中节点的角色,它们都负责绘制界面的一部分,并且都可以包含子节点。

在Flutter中,三棵树(Widget树、Element树、RenderObject树)与iOS中的类对应关系如下:

  1. Widget树
    • Flutter中的Widget树类似于iOS中的UIView控件树。Widget是Flutter中用于描述用户界面的构件,它们是不可变的,并且当状态变化时,Flutter会构建一个新的Widget树。
  2. Element树
    • Element树在Flutter中扮演着将Widget树的变更映射到RenderObject树的角色。每一个Widget都有一个对应的Element,Element树是Widget树和RenderObject树之间的桥梁。在iOS中,这个概念没有直接对应的类,因为它是Flutter特有的架构。但是,如果非要找一个类比,可以说Element树的作用类似于UIView的实例化和状态管理,因为Element持有Widget和RenderObject的引用,并负责协调它们的状态。
  3. RenderObject树
    • RenderObject树是Flutter中实际负责布局和绘制的树。它与iOS中的CALayer树相似,因为CALayer负责实际的渲染工作。RenderObject树根据Widget的属性进行布局(layout)和绘制(paint),这与CALayer在iOS中的作用类似。

总的来说,Flutter的Widget树类似于iOS的UIView树,Element树是Flutter特有的,没有直接对应的iOS类,而RenderObject树类似于iOS的CALayer树。这种架构使得Flutter能够在不同的平台上以统一的方式处理渲染和布局。

img

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开发经验:button不能响应的原因

离屏渲染

在使用圆角、阴影和遮罩等视图功能的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所有就需要在屏幕外的上下文中渲染,即离屏渲染。

更多参考:iOS离屏渲染之优化分析该文非常重要。

END

< 返回目录