本篇文章将会介绍 iOS 中一个老生常谈的话题: layers .你可能已经熟悉iOS中各种各样的views,但可能还不知道每一个view背后都有一个叫做 layer 的东西存在.而layers是组成 Core Animation framework 的重要一部分.

你可能还不了解 layer 的作用,甚至觉得自己从来不曾使用过 layer ,它可能是一个无足轻重的东西, 不管你是否了解 layer ,你的app中到处都是 layer 的身影.你app中每个 view 的背后都有个 layer 在支撑.是它让你的 app 轻松地将每个 view 的位图信息提供给手机 GPUs 绘制.下面的这张图片清楚的展示了 Core Animation 在 iOS 绘制层级中的位置.

"CALayer"

为什么要使用 Layers?

在智能手机上,用户希望能够飞速的进行各种操作.所以保持界面稳定的刷新帧率给用户丝滑般的感觉就显得尤为重要.在 iOS 系统中屏幕平均每秒钟刷新 60 次,为了保证系统在这个帧率下稳定运行,最基本同时也最强大的能够在 GPU 上精准运行的 OpenGL 就诞生了.
OpenGL 提供了手机图形硬件最低层但也是最快的权限.但这也是需要权衡的,OpenGL过于低层,甚至完成最简单的任务都需要大量的代码.

为了缓解这个问题, Core Graphocs 就诞生了. Core Graphocs 可以用更少的代码提供更轻量的高层次的功能.为了让 Core Graphocs 使用起来更简单, Core Animation 也出现了. Core Animation 提供了 CALayer 类,并且能够使用一些基本的的图形能力.

后来苹果公司发现 Core Animation 强大的功能大部分在常规app内并没有使用到.于是苹果公司便推出了具有更高层次图形权限的 UIKit. 这么设计的好处就是你的 app 可以根据需求自由选择不同的图形层次功能.允许灵活选择需要实现的功能有效的防止了不必要的代码产生.

UIKit 缺点就是高层次的图形 API 所能提供的功能比较少.我们可以这件事中知道: CALayer 可以让 iOS 系统快速便捷地获得 app 页面上 views 的位图信息, 这些信息将会交付给 Core Graphics 甚至 OpenGL处理 然后通过 GPU 绘制在你的手机屏幕上. 虽然在大部分的情况下我们不需要直接使用 CALayer, 但是低层次的 APIs 提供给开发者很多灵活可定制的功能,我们在文章后面将会提到.

获得 CALayer

通过讨论 layers 为什么存在之后, 让我们来学着去使用! 就像我刚才提到的, 每一个 view 的背后都有个 layer 支撑. 我们可以通过 UIView 的属性来获得这个 layer. 假使我们有一个 myView 对象, 我们可以得到它的 layer 就像这样:

1
myView.layer

好了, 当我们拿到 view 的 layer 之后都能做些什么操作呢? 你将会对之后我们能做的事之多感到惊奇. 在接下来的文章中我们将会看到 layer 的一些使用方法和所能达到的效果.

Demo Project

首先, 打开 示例工程 ,学习的最好方法就是实践,接下来我们将要在 app 内的 layer 上添加一些自定义的效果. 打开工程 你将会看到界面很简洁,一个空白的 view 中间有一个方块的 subview. 让我们来帮它美化下. 打开 ViewController.swift 开始操作吧.

CALayer Demo

切圆角

你可以使用 CALayer 的 cornerRadius 属性来制作圆角. 让我们试一下吧. 在 viewDidLoad() 内 添加如下代码:

1
box.layer.cornerRadius = 5

正如期望的那样,这行代码在 box 的 layer 上添加了一个 5个点单位的圆角. 就是下面这个样子:

圆角

还不错吧! 增加圆角弧度会让 layer 更加的圆滑,相反减少圆角弧度 会让layer 更加的棱角分明. 所有的 layer 默认的圆角弧度是 0 .

圆角对比

增加阴影效果

阴影可以让我们的 app 更有立体感,并且阴影在设计界面的时候是非常有帮助的.在阴影效果下 我们可以让 views 看起来像漂浮在屏幕上. 让我们研究下用 CALayer 如何制作出隐形效果. 把下面代码插入到 ViewControllerviewDidLoad 方法中:

1
2
3
4
box.layer.shadowOffset = CGSizeMake(5, 5)
box.layer.shadowOpacity = 0.7
box.layer.shadowRadius = 5
box.layer.shadowColor = UIColor(red: 44.0/255.0, green: 62.0/255.0, blue: 80.0/255.0, alpha: 1.0).CGColor

第一行 设置 layer 的阴影偏移量是 (5,5). 将阴影的 layer.shadowOffset 设置(5,5), 意味着这个 layer 的阴影是到 box.layer 右边 5 个点单位距离, 下边 5 个单位的距离.
第二行 设置 layer 的阴影透明度是 0.7 . 意味着这个阴影应该是70%的不透明度.
第三行 设置的是 layer 的阴影半径是5个点. 阴影的范围是 box.layer 的模糊弧度决定的.更高的弧度会让阴影的范围更广,但是更加模糊不可见.更低的弧度会让阴影更加清晰可见度更高.
第四行 设置的是阴影的颜色是深蓝. 注意这里的颜色属性是 CGColor 类型. 而不是 UIColor. 这两个颜色类的转换是非常简单的,你只需要写成 myUIColor.CGColor.

让我们看一下效果:

阴影效果

添加边框

我们能够轻易地使用 CALayer 添加边框.让我们给 box 添加一个边框.

1
2
box.layer.borderColor = UIColor.blueColor().CGColor
box.layer.borderWidth = 3

第一行 设置 box 的边框颜色是蓝色.这将会让 box 的所有边框都是蓝色的.
第二行 设置边框线条的宽度是 3 点. 也就是 box 的边框厚度是 3 个点的单位.

让我们看一下 box添加边框之后的效果:

添加边框

渲染图片

你也可以吧一张图片赋值给 layer 这样 layer 就会将这张图片渲染出来. 在这里有一张树的图片, 让我们试着用 layer 来显示这张图片.把下面的代码插入到 viewDidLoad:

1
2
3
box.layer.contents = UIImage(named: "tree.jpg")?.CGImage
box.layer.contentsGravity = kCAGravityResize
box.layer.masksToBounds = true

第一行 将图片赋值给 layer 的 contents 属性.
第二行 将 layer 的内容这是成自适应大小, 图片会自适应 layer 大小.
第三行 将 layer 的任何 扩展到 layer 外的子 layer 部分都剪切掉.如果你不明白是什么意思.你可以将 masksToBounds 设置成 false 来看看他们的区别 (阴影被裁剪掉了):

裁剪图片

背景颜色和不透明度

研究了在 layer 添加一些 UIKit 无法实现的特殊效果. 我们也应该讨论下通过 CALayer 来修改 UIKit 类的属性的可能性. 比如 你可以修改 view 的背景颜色和不透明度:

1
2
box.layer.backgroundColor = UIColor.blueColor().CGColor
box.layer.opacity = 0.5

CALayer 的性能

在 layers 上添加太多的特殊效果会影响到性能, 现在我们来聊一聊可以帮助我们提高 app 性能的 2 个 CALayer 属性.

首先是 drawsAsynchronously 属性.这个属性决定 CPU 是否应该在子线程里渲染 layer. 如果设置成 true, 这个 layer 看起来像我们平时看到的样子, 但是 CPU 需要在子线程中来计算和渲染它.如果你的 app 里面有一个 view 需要频繁重绘(比如一个地图view 或者 tableView),你需要将这个属性设置为 true;

第二个属性是 shouldRasterize,这个属性决定这 layer 是否会栅格化.当这个属性设置为 true 后,这个 layer 只会被绘制一次,当它具有动画时 layer不会被渲染 并且第一次绘制的位图信息将会被回收.当你的app有一个不需要频繁绘制的 view 时 你可以把这个属性设置为 true. 注意 当你设置了shouldRasterize属性后,layer 的外观可能在 Retina 屏上有锯齿. 这是因为 layer 有一个控制 layer 的栅格化因子 rasterizationScale.为了防止这种情况. 将 layer 的 rasterizationScale 设置成 UIScreen.mainScreen().scale 这样就不会出现锯齿了.

栅格化 是PS中的一个专业术语,栅格即像素,栅格化即将矢量图形转化为位图。
开启 shouldRasterize 后, CALayer 会被栅格化为 bitmap , layer 的阴影等效果也会被保存到 bitmap 中。

避免 shouldRasterize 和 drawsAsynchronously 的过度使用

当我们开启光栅化后,需要注意三点问题。

如果我们更新已光栅化的layer,会造成大量的 offscreen 渲染。

offscreen rendring指的是在图像在绘制到当前屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕。offscreen渲染会耗费大量资源.

因此 CALayer 的光栅化选项的开启与否需要我们仔细衡量使用场景。 只能用在图像内容不变的前提下的:

用于避免静态内容的复杂特效的重绘,例如前面讲到的UIBlurEffect
用于避免多个View嵌套的复杂View的重绘。
而对于经常变动的内容,这个时候不要开启,否则会造成性能的浪费。

例如我们日程经常打交道的 TableViewCell ,因为 TableViewCell 的重绘是很频繁的(因为Cell 的复用),如果Cell的内容不断变化,则 Cell 需要不断重绘,如果此时设置了cell.layer 可光栅化。则会造成大量的 offscreen 渲染,降低图形性能。

当然,合理利用的话,是能够得到不少性能的提高的,因为使用 shouldRasterize 后 layer 会缓存为Bitmap位图,对一些添加了 shawdow 等效果的耗费资源较多的静态内容进行缓存,能够得到性能的提升。

不要过度使用,系统限制了缓存的大小为 2.5 x Screen Size.
如果过度使用,超出缓存之后,同样会造成大量的 offscreen 渲染。
被光栅化的图片如果超过 100ms 没有被使用,则会被移除

因此我们应该只对连续不断使用的图片进行缓存。对于不常使用的图片缓存是没有意义,且耗费资源的。

基于 99% 的情况下 你都不需要手动设置这两个属性.不当的设置它们可能会反而导致你 app 的性能变得更糟.

题外

关于 offscreen rendering 注意到上面提到的 offscreen rendering 。我们需要注意 shouldRasterize 的地方就是会造成 offscreen rendering 的地方,那么为什么需要避免呢?
WWDC 2011 Understanding UIKit Rendering 指出一般导致图形性能的问题大部分都出在了 offscreen rendering ,因此如果我们发现列表滚动不流畅,动画卡顿等问题,就可以想想和找出我们哪部分代码导致了大量的 offscreen 渲染。
那么为什么 offscreen 渲染会耗费大量资源呢?
原因是显卡需要另外 alloc 一块内存来进行渲染,渲染完毕后在绘制到当前屏幕,而且对于显卡来说, onscreen 到 offscreen 的上下文环境切换是非常昂贵的(涉及到 OpenGL 的 pipelines 和 barrier 等),
我们在开发应用,提高性能通常要注意的是避免 offscreen rendering 。不需要纠结和拘泥于它的定义.
有兴趣可以继续阅读 Andy Matuschak , 前 UIKit team 成员关于offscreen rendering 的 评论

总之,我们通常需要避免大量的offscreen rendering.
会造成 offscreen rendering的原因有:
Any layer with a mask (layer.mask)
Any layer with layer.masksToBounds being true
Any layer with layer.allowsGroupOpacity set to YES and layer.opacity is less than 1.0
Any layer with a drop shadow (layer.shadow*).
Any layer with layer.shouldRasterize being true
Any layer with layer.cornerRadius, layer.edgeAntialiasingMask,
layer.allowsEdgeAntialiasing

因此,对于一些需要优化图像性能的场景,我们可以检查我们是否触发了 offscreen rendering 。并用更高效的实现手段来替换。
阴影绘制:
裁剪图片为圆:
Blending 的过多使用
检查有无过多offscreen渲染
检查有无过多Blending
检查有无不正确图片格式,图片是否被放缩,像素是否对齐。
检查有无使用复杂的图形效果。

– 原创所有,转载请注明出处。