前言
众所周知,我们作为 iOS 开发者,平日的工作里做得最多的就是画 UI,写页面。既然要写页面,自然要知道视图显示的原理,这样才能写出更多功能的、性能更好的页面,所以这篇文章我会讲讲视图的显示和绘制,以及它的坐标系属性。
UIView 与CALayer
UIView
UIView
是视图编程的基础,ta 指的是在屏幕上一块矩形的、管理"内容展示"的东西(比如图片、文字、按钮和视频),大多数情况下iOS 开发工程师都是跟 ta 打交道。它的工作职责是:
-
Drawing and animation绘制和动画:可以用 UIKit 或者 Core Graphics 绘制内容,也可以用简单的动画展示某个属性的变化
Core Graphics是一个绘图引擎,是 UIKit 的底层框架,它的 API 都是 C 语言的。常用于自定义绘制
-
Layout and subview management布局和管理子视图
-
Event handling处理触摸事件:因为 UIView 是
UIResponder
的子类,同时它也能通过设置gesture recognizer来响应特定的手势动作。
UIView 其实不是实际负责显示的那个,它有一个非常重要的属性------ CALayer
。
objc
@interface UIView : UIResponder <procotols>
@property(nonatomic,readonly,strong) CALayer *layer;
// returns view's layer. Will always return a non-nil value. view is layer's delegate
CALayer
CALayer 不属于 UIKit,它属于 Core Animation,它负责"可视内容的展示"。也就是说,UIView 如果没有CALayer,那么它是不能够展示内容的。反过来说,如果你需要显示一个东西,同时又不需要 ta 响应用户操作,那其实你可以只用 CALayer就能实现该功能。它是轻量级的 UIView。
一个 CALayer要展示可视内容,所以它有一个属性contents
,这指向一块缓存区,称之为backing store,backing store里放的是位图bitmap。
bitmap就是要显示到屏幕上的东西
手机屏幕刷新时,会取这个 bitmap,呈现到屏幕上。
CALayer 是个强大的工具,它能设置显示内容 content 的属性(contentGravity、背景颜色、边框、圆角),能播放视频,能设置滚动效果,能设置文字,能设置渐变背景颜色,能实现粒子喷射效果。。。总之就是公鸡中的战斗机。它的具体功能展示可以看看老外Ron Kliffer写的文章:CALayer Tutorial for iOS: Getting Started。不慌,我为您找到了中文翻译。
但CALayer 不处理触摸事件,前面说了,触摸事件是 UIView 的任务。
两者的关系
CALayer 是 UIView 的一个属性,UIView 是 CALayer 的 delegate。UIView 是老板,它管理CALayer,它要负责创建 CALayer、添加 CALayer,必要时销毁 CALayer
之所以要用两个东西来负责视图显示和响应操作,很大的一个原因是「代码复用 」。因为Apple 不仅开发了 iOS,还开发了 macOS,这两个系统在「视图怎么显示」这个问题上的解决方案很相似,但是「视图如何响应用户操作」这个问题的解决上就有很大的差异,于是 Apple把前者的解决方案抽出来,做成 Core Animation,后者则是分别做成 UIKit 和 AppKit。这样一来,代码既能复用、在出现问题时也可以减少解决成本。
一个View显示在屏幕上,需要走过怎样的流程?绘制与布局
我简单阐述下步骤,其实只有前两个步骤是 iOS 开发者能够插手的地方:
- 布局:CPU 对一个 UIView进行 Frame 布局,设置它的layer 属性的属性(位置、背景色、边框等)。
- 显示 (绘制):layer 的可视内容开始绘制。有两种方法可以设置这个显示内容:一是设置
layer.contents
。二是重写 UIView 的drawRect: 方法或者重写CALayerDelegate
的drawLayer:inContext:
方法。重写这两个方法,我们称之为自定义绘制了。注意,自定义绘制会额外消耗 CPU 的性能,所以Apple 会建议我们尽量不要自定义绘制。 - 解码:Core Animation 框架准备发送数据到渲染服务。对图片进行解码,转换图片格式,图片要显示出来,必须从压缩状态转换到未解压状态,出于节省内存的目的,一般都是在绘制前才去解码图片。
- 提交:把UIView 和 CALayer的层级关系进行打包,提交给渲染服务
- 生成帧缓存:根据图层数据生成前后帧缓存,参照VSync信号和CADisplayLink,切换前后帧缓存
- 渲染:将后帧缓存交给 GPU,最终显示到屏幕上。
我们来关注一下能插手的地方
布局
layoutSubviews()
方法------布局子视图
根据已有的约束,决定子视图的大小和位置,它的触发时机如下:
- 修改 view 的大小(调用view 的
layoutSubviews
) - 调用addSubview添加子视图时(调用子视图的
layoutSubviews
) - 在 UIScrollView 上滚动(调用 UISCrollView 和ta 的父视图的
layoutSubviews
方法) - 更新view 的约束时,(调用view 的
layoutSubviews
)
在什么时候需要重写这个方法?
当autoresizing 和基于约束的布局不满足需求时,就重写这个方法。注意,重写这个方法是有性能损耗的,所以要尽量避免重写。
setNeedsLayout()
方法------标记是否需要重新布局
对某个 view 打上「脏标记」告诉系统这个 view 需要重新布局,系统会在下一次runloop中调用layoutSubviews
进行布局。
layoutIfNeeded()
方法------马上重新布局
告诉系统马上执行打了「脏标记」的layoutSubviews
,不必等下一次 runloop。
显示
drawRect:
方法------自定义绘制
自定义绘制,能够实现标准控件以外的显示效果。我们平时用的都是系统自带的标准组件,比如 UILabel,UIImage等,可以设置他们的一些属性来实现需求,但是有的需求压根就不能通过这种方法来做,于是我们需要把目光投向自定义绘制,这里是一个具体例子。
开发者不能手动调用这个方法。应该是在 runloop 的某个时间节点,由系统调用。
setNeedsDisplay()
方法------标记谁谁谁需要自定义绘制
打上「脏标记」,告诉系统这个 view 需要重新绘制。后续系统会调用这个 view 的drawRect
函数。
UIView 的系统绘制和异步绘制
当调用setNeedsDisplay
函数时,绘制就开始了,它有两条路径,如下:
-
UIView 调用
setNeedsDisplay
后,调用自身 layer属性的setNeedsDisplay
,最终会调用 CALayer 的 display 函数,开始判断 layer 的 delegate 是否实现了displayLayer: 方法。 -
系统绘制 指的是默认的绘制流程,在主线程生成 bitmap,把 bitmap 设置给 layer.contents。
- 异步绘制指的是bitmap在子线程生成,生成了 bitmap 以后再回到主线程,把 bitmap 设置给 layer.contents。异步绘制的好处是能减轻主线程的负担,提高界面的流畅性。
markdown
> Facebook 的[Texture](https://github.com/TextureGroup/Texture)和 ibireme 的[YYAsyncLayer](https://github.com/ibireme/YYAsyncLayer)都是按照这个思想实现的
总结:
- 如果想自定义布局 ,就重写
layoutSubviews
方法 - 如果想自定义绘制 ,就重写
drawRect
方法
视图的坐标系以及属性frame, bounds 和 center
在坐标系中,有几个结构我们经常用到:
结构 | 作用 |
---|---|
CGPoint | 表示位置,包含 x,y 两个属性 |
CGSize | 表示尺寸,包含 width 和 height 两个属性 |
CGRect | 表示屏幕上的一块矩形区域,包含 origin 属性(CGPoint 类型,表示矩形左上角)和size 属性(CGSize 类型) |
UIView 中的 frame、bounds、center属性都能在CALayer 找到对应的属性,如下
UIView | CALayer | 作用 |
---|---|---|
frame | frame | 指定了view大小,和view在父视图 坐标系中的位置。一般用于修改 view 的大小和位置 |
center | position | view 中心点在父视图坐标系中的位置,用于修改 view 的位置 |
bounds | bounds | 指定了view大小,和在自身坐标系中的位置。常用于绘制,修改 bounds 会影响子视图的位置。 |
transform | affineTransform | 用于仿射变换,也就是对 View 进行旋转、缩放、移动等操作 |
无 | anchorPoint | 锚点,CGPoint类型,表示一个相对位置,左上角为(0,0), 右下角为(1,1), anchorPoint 的默认值为(0.5, 0.5)。一般用作 transform 仿射变换时的原点 |
只改位置的时候,最好用 center 属性,因为 center 属性永远是有效的,即便view 缩放和或者旋转了。而 frame 就不一定了,当视图的变换不等于单位的变换时(if the view's transform is not equal to the identity transform),它就是无效的。详见View Programming Guide for iOS
一个反直觉的设置是,当我们把父视图的 bounds 从(0,0, 200, 200) 修改为(50, 50 , 200 , 200)时,它的子视图 的位置会往左上角移动,代码如下:
ini
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self.view setBackgroundColor:UIColor.whiteColor];
UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
[blueView setBackgroundColor:UIColor.blueColor];
[self.view addSubview:blueView];
UITapGestureRecognizer *ge = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onClick)];
[blueView addGestureRecognizer:ge];
UIView *yellowView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
[yellowView setBackgroundColor:UIColor.yellowColor];
[blueView addSubview:yellowView];
self.blueView = blueView;
self.yellowView = yellowView;
}
- (void)onClick {
self.blueView.bounds = CGRectMake(50, 50, 200, 200);
}
这里的解释是:修改后,对于 yellowView,父视图的原点坐标就是在左上角,再往上往左 50 的位置,所以yellowView 就也往左上角去了。解释参考
参考资料
- View Programming Guide for iOS:苹果文档
- CALayer Tutorial for iOS: Getting Started:展示了 CALayer 的强大,中文翻译在此
- iOS 利用 drawRect 方法绘制图形:自定义绘制的例子
- 浅谈 UIView 的刷新与绘制
- iOS 保持界面流畅的技巧
- frame 与 bounds 的区别详解