iOS

浅谈UIView的刷新与绘制

2021-10-25  本文已影响0人  天明天
topPic

概述:

UIView是我们在做iOS开发时每天都会接触到的类,几乎所有跟页面显示相关的控件也都继承自它。但是关于UIView的布局、显示、以及绘制原理等方面笔者一直一知半解,只有真正了解了它的原理才能更好的服务我们的开发。并且在市场对iOS开发者要求越来越高的大环境下,对App页面流畅度的优化也是对高级及以上开发者必问的面试题,这就需要我们要对UIView有更深的认知。

一.UIView 与 CALayer

UIView:一个视图(UIView)就是在屏幕上显示的一个矩形块(比如图片,文字或者视频),它能够拦截类似于鼠标点击或者触摸手势等用户输入。视图在层级关系中可以互相嵌套,一个视图可以管理它的所有子视图的位置,在iOS当中,所有的视图都从一个叫做UIView的基类派生而来,UIView可以处理触摸事件,可以支持基于Core Graphics绘图,可以做仿射变换(例如旋转或者缩放),或者简单的类似于滑动或者渐变的动画。

CALayer:CALayer类在概念上和UIView类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。它们有一些方法和属性用来做动画和变换。和UIView最大的不同是CALayer不处理用户的交互。

CALayer并不清楚具体的响应链(iOS通过视图层级关系用来传送触摸事件的机制),于是它并不能够响应事件,即使它提供了一些方法来判断一个触点是否在图层的范围之内。

1. UIView 与 CALayer的关系

每一个UIView都有一个CALayer实例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作.

两者的关系:实际上这些背后关联的图层(Layer)才是真正用来在屏幕上显示和做动画,UIView仅仅是对它的一个封装,提供了一些iOS类似于处理触摸的具体功能,以及Core Animation底层方法的高级接口。

这里引申出面试常问的一个问题:为什么iOS要基于UIView和CALayer提供两个平行的层级关系呢?为什么不用一个简单的层级来处理所有事情呢?

原因在于要做职责分离(单一职责原则),这样也能避免很多重复代码。在iOS和Mac OS两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为什么iOS有UIKitUIView,但是Mac OS有AppKitNSView的原因。他们功能上很相似,但是在实现上有着显著的区别。把这种功能的逻辑分开并封装成独立的Core Animation框架,苹果就能够在iOS和Mac OS之间共享代码,使得对苹果自己的OS开发团队和第三方开发者去开发两个平台的应用更加便捷。

2. CALayer的一些常用属性

contents属性

CALayer的contents属性可以让我们为layer图层设置一张图片,我们看下它的定义

/* An object providing the contents of the layer, typically a CGImageRef,
 * but may be something else. (For example, NSImage objects are
 * supported on Mac OS X 10.6 and later.) Default value is nil.
 * Animatable. */

@property(nullable, strong) id contents;

这个属性的类型被定义为id,意味着它可以是任何类型的对象。在这种情况下,你可以给contents属性赋任何值,你的app都能够编译通过。但是,如果你给contents赋的不是CGImage,那么你得到的图层将是空白的。事实上,你真正要赋值的类型应该是CGImageRef,它是一个指向CGImage结构的指针,UIImage有一个CGImage属性,它返回一个CGImageRef,但是要使用它还需要进行强转:

layer.contents = (__bridge id _Nullable)(image.CGImage);
contentGravity属性
/* A string defining how the contents of the layer is mapped into its
 * bounds rect. Options are `center', `top', `bottom', `left',
 * `right', `topLeft', `topRight', `bottomLeft', `bottomRight',
 * `resize', `resizeAspect', `resizeAspectFill'. The default value is
 * `resize'. Note that "bottom" always means "Minimum Y" and "top"
 * always means "Maximum Y". */

@property(copy) CALayerContentsGravity contentsGravity;

如果我们为图层layer设置contents为一张图片,那么可以使用这个属性来让图片自适应layer的大小,它类似于UIView的contentMode属性,但是它是一个NSString类型,而不是像对应的UIKit部分,那里面的值是枚举。contentsGravity可选的常量值有以下一些:

kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill

例如,如果要让图片等比例拉伸去自适应layer的大小可以直接这样设置

layer.contentsGravity = kCAGravityResizeAspect;
contentsScale属性
/* Defines the scale factor applied to the contents of the layer. If
 * the physical size of the contents is '(w, h)' then the logical size
 * (i.e. for contentsGravity calculations) is defined as '(w /
 * contentsScale, h / contentsScale)'. Applies to both images provided
 * explicitly and content provided via -drawInContext: (i.e. if
 * contentsScale is two -drawInContext: will draw into a buffer twice
 * as large as the layer bounds). Defaults to one. Animatable. */

@property CGFloat contentsScale

contentsScale属性定义了contents设置图片的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数。这个属性其实属于支持Retina屏幕机制的一部分,它的值等于当前设备的物理尺寸与逻辑尺寸的比值。如果contentsScale设置为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片。当用代码的方式来处理contents设置图片的时候,一定要手动的设置图层的contentsScale属性,否则图片在Retina设备上就显示得不正确啦。代码如下:

layer.contentsScale = [UIScreen mainScreen].scale;
maskToBounds属性

maskToBounds属性的功能类似于UIView的clipsToBounds属性,如果设置为YES,则会将超出layer范围的图片进行裁剪.

contentsRect属性

contentsRect属性在我们的日常开发中用的不多,它的主要作用是可以让我们显示contents所设置图片的一个子区域。它是单位坐标取值在0到1之间。默认值是{0, 0, 1, 1},这意味着整个图片默认都是可见的,如果我们指定一个小一点的矩形,比如{0,0,0.5,0.5},那么layer显示的只有图片的左上角,也就是1/4的区域。

实际上给layer的contents赋CGImage的值不是唯一的设置其寄宿图的方法。我们也可以直接用Core Graphics直接绘制。通过继承UIView并实现-drawRect:方法来自定义绘制,如果单独使用CALayer那么可以实现其代理(CALayerDelegate)方法- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;在这里面进行自主绘制。实际的方法绘制流程我们在下面进行探讨。

二.View的布局与显示

1.图像显示原理

在开始介绍图像的布局与显示之前,我们有必要先了解下图像的显示原理,也就是我们创建一个显示控件是怎么通过CPU与GPU的运算显示在屏幕上的。这个过程大体分为刘六个阶段:


绘制

注意:当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:为了显示屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须要由CPU做这些事情。

前四个阶段都在软件层面处理(通过CPU),第五阶段也有CPU参与,只有最后一个完全由GPU执行。而且,你真正能控制只有前两个阶段:布局和显示,Core Animation框架在内部处理剩下的事务,你也控制不了它。所以接下来我们来重点分析布局与显示阶段。

2.布局

布局:布局就是一个视图在屏幕上的位置与大小。UIView有三个比较重要的布局属性:frameboundscenter.UIView提供了用来通知系统某个view布局发生变化的方法,也提供了在view布局重新计算后调用的可重写的方法。

layoutSubviews()方法

layoutSubviews():当一个视图“认为”应该重新布局自己的子控件时,它便会自动调用自己的layoutSubviews方法,在该方法中“刷新”子控件的布局.这个方法并没有系统实现,需要我们重新这个方法,在里面实现子控件的重新布局。这个方法很开销很大,因为它会在每个子视图上起作用并且调用它们相应的layoutSubviews方法.系统会根据当前run loop的不同状态来触发layoutSubviews调用的机制,并不需要我们手动调用。以下是他的触发时机:

setNeedsLayout()方法

setNeedsLayout()方法的调用可以触发layoutSubviews,调用这个方法代表向系统表示视图的布局需要重新计算。不过调用这个方法只是为当前的视图打了一个脏标记,告知系统需要在下一次run loop中重新布局这个视图。也就是调用setNeedsLayout()后会有一段时间间隔,然后触发layoutSubviews.当然这个间隔不会对用户造成影响,因为永远不会长到对界面造成卡顿。

layoutIfNeeded()方法

layoutIfNeeded()方法的作用是告知系统,当前打了脏标记的视图需要立即更新,不要等到下一次run loop到来时在更新,此时该方法会立即触发layoutSubviews方法。当然但如果你调用了layoutIfNeeded之后,并且没有任何操作向系统表明需要刷新视图,那么就不会调用layoutsubview.这个方法在你需要依赖新布局,无法等到下一次 run loop的时候会比setNeedsLayout有用。

3.显示

和布局的方法类似,显示也有触发更新的方法,它们由系统在检测到更新时被自动调用,或者我们可以手动调用直接刷新。

drawRect:方法

在上面我们提到过,如果要设置视图的寄宿图,除了直接设置view.layer.contents属性,还可以自主进行绘制。绘制的方法就是实现view的drawRect:方法。这个方法类似于布局的layoutSubviews方法,它会对当前View的显示进行刷新,不同的是它不会触发后续对视图的子视图方法的调用。跟layoutSubviews一样,我们不能直接手动调用drawRect:方法,应该调用间接的触发方法,让系统在 run loop 中的不同结点自动调用。具体的绘制流程我们在本文第三节进行介绍。

setNeedsDisplay()方法

这个方法类似于布局中的setNeedsLayout。它会给有内容更新的视图设置一个内部的标记,但在视图重绘之前就会返回。然后在下一个run loop中,系统会遍历所有已标标记的视图,并调用它们的drawRect:方法。大部分时候,在视图中更新任何 UI 组件都会把相应的视图标记为“dirty”,通过设置视图“内部更新标记”,在下一次run loop中就会重绘,而不需要显式的调用setNeedsDisplay.

三.UIView的系统绘制与异步绘制流程

UIView的绘制流程

接下来我们看下UIView的绘制流程

绘制

系统绘制

xitong

注意:使用CPU进行绘图的代价昂贵,除非绝对必要,否则你应该避免重绘你的视图。提高绘制性能的秘诀就在于尽量避免去绘制。

异步绘制

什么是异步绘制?

通过上面的介绍我们熟悉了系统绘制流程,系统绘制就是在主线程中进行上下文的创建,控件的自主绘制等,这就导致了主线程频繁的处理UI绘制的工作,如果要绘制的元素过多,过于频繁,就会造成卡顿。而异步绘制就是把复杂的绘制过程放到后台线程中执行,从而减轻主线程负担,来提升UI流畅度。

异步绘制流程
pic

上面很明显的展示了异步绘制过程:

当然我们在日常开发中还要考虑线程的管理与绘制时机等问题,使用第三方库YYAsyncLayer可以让我们把注意力放在具体的绘制上,具体的使用流程可以点这里去查看.

四.总结

我们知道,当我们实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法,图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽X图层高X4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 2048X15264字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。可见使用Core Graphics利用CPU进行绘制代价是很高的,那么如何进行高效的绘图呢?iOS-Core-Animation-Advanced-Techniques给出了答案,我们在日常开发中完全可以使用Core AnimationCAShapeLayer代替Core Graphics进行图形的绘制,具体的方法这里就不介绍了,感兴趣的可以自行去查看。

参考引用:
iOS-Core-Animation-Advanced-Techniques
YYAsyncLayer
https://juejin.cn/post/6844903567610871816

上一篇 下一篇

猜你喜欢

热点阅读