认识CALayer

layer和view的关系

开始开发都是从view开始,并且很长一段时间可能都只认识到view,而只会在某些角落看见layer,好比圆角,好比coreAnimation动画,还有绘制内容时也使用CALayer,因此对于layer的首要疑问确定是:这货跟view到底什么关系?面试

出自 WWDC 2012- iOS App Performance- Graphics and Animations.png

来段文档:api

Layers provide infrastructure for your views. Specifically, layers make it easier and more efficient to draw and animate the contents of views and maintain high frame rates while doing so. However, there are many things that layers do not do. Layers do not handle events, draw content, participate in the responder chain, or do many other things
  • layer给view提供了基础设施,使得绘制内容和呈现更高效动画更容易、更低耗
  • layer不参与view的事件处理、不参与响应链

思考一下一个view在系统里起了什么做用:就是接受用户点击和呈现内容。上面这段的意思就是layer负责了内容呈现部分的工做,而不参与用户点击事件处理的工做。缓存

很简单很好记,对view的理解也加深了。app

内容呈现

知道了layer的工做以后,接下来的疑问就是:内容如何提供?支持哪些内容?怎么呈现的?ide

翻开CALayer的api,跟内容呈现最相关的几个就是:工具

  • displaysetNeedsDisplay`displayIfNeeded`
  • drawInContext:和delegate里面的drawLayer:inContext:
  • 属性contents
更新机制

第一组3个方法跟view里面的那一组相似,它们是类似的逻辑。首先一个内容在layer上发生改变,好比颜色变了,要让用户立马看到,就须要图形系统从新渲染。再试想一下,有可能同时多个layer在很短的时间内同时要刷新,好比打开一个新的结构复杂viewController,好比快速滑动tableView时,这种场景并不特殊。若是每一个layer更新都要系统刷新一遍,那么会致使紊乱的帧率,有时特别卡有时又很闲。性能

因此机制是反过来的,系统有基本稳定的刷新频率,而后在layer内容改变的时候,把这个layer作个须要刷新的标记,这就是setNeedsDisplay,每次刷新时,把上次刷新以后被标记的layer一次性所有提交给图形系统,因此这里还有一个东西,就是事务(CATransaction)学习

layer刷新就是被调用display,但这个咱们不主动调用,让系统调用,它能够把我更好的时机。咱们只须要setNeedsDisplay作标记。若是你真的很是急需,就用displayIfNeeded,对于已被标记为Needed的layer就立马刷新。测试

既提供了稳定和谐的通用机制,又照顾到了偶然的特殊需求,很好。优化

内容提供方法

上面只是说了绘制时机的机制,真正的内容绘制在第二组方法里,根据测试,内容提供的机制是这样的:

  • display
  • delegate的displayLayer:
  • drawInContext:
  • delegate的drawLayer:inContext:;

这四个方法,但凡是有一个方法实现了,就不会继续往下进行了,就认为你已经提供了内容。delegate的方法要检查delegate是否存在且是否实现了对应的方法。

第1和第2个方法是对应的,第3和第4个方法也是对应的,前面两个没有构建内容缓冲区(Backing Store),须要直接提供contents,一种方法就是直接赋值一个CAImageRef:

layer.contents = [UIImage imageNamed:@"xxx"].CGImage;

后两种方法,会给layer开辟一块内存用来存储绘制的内容,在这两个方法里,可使用CoreGraphics的那套api来绘制须要的内容。

delegate的做用

从上面还能够搞清楚一个问题,就是layer的delegate的做用:delegate控制layer的内容,这也是为何UIView自带的layer的delegate是默认指定到view自身的,而也由于这样,绝大多数时候咱们直接修改view的属性(颜色位置透明度等等),layer的呈现就自动发生变化了。

layer和动画的关系

在使用CoreAnimation的动画的时候,是把建立的动画放到layer上,而简单的使用动画,不少时候是使用[UIView animation...],那么后者其实本质是内部建了一个动画放到了layer上吗?是的,动画的载体就是layer,这就是它们的基本关系。但为了更高效的动画,还有更多的细节。

若是你作过位移的动画,而且试着在动画的过程里去输出view的位置,你会惊讶的发现:在动画开始后,view的frame就已是结束位置的值了!

按照常识理解,view的位置应该是随着时间不断变化的,而这个理解上的错差正是理解动画内核的一个好的窗口。

从上面的现象至少能够得出一点:就是你眼睛看到的,跟系统里的数据不是一致的,动画多是一个欺骗把戏。

看段文档:

Instead, a layer captures the content your app provides and caches it in a bitmap, which is sometimes referred to as the backing store. ... When a change triggers an animation, Core Animation passes the layer’s bitmap and state information to the graphics hardware, which does the work of rendering the bitmap using the new information. Manipulating the bitmap in hardware yields much faster animations than could be done in software.

这段话的含义是:layer的内容生成一个位图(bitmap),触发动画的时候,是把这个动画和状态信息传递给图形硬件,图形硬件使用这两个数据就能够构造动画了。处理位图对于图形硬件更快。

模拟一下动画处理过程就是:一个很复杂的view的动画,是把它的layer的内容合成一张图片,而后要旋转,就是把这张图旋转一下显示出来。实际上图形系统在渲染的过程里,对于旋转、缩放、位移等,只须要加一个矩阵就能够了(对应就是transform),对于图形系统而言这些工做就是最基本的操做,很是高效。

因此动画的呈现和view自己的的数据时分离的,也就出现了动画时看到的都是结束时的数据。

若是按照常识理解去实现动画,是怎么作?

view移动,在界面刷新的方法里,不断的更新view的位置,每次更新完,把数据提供给图形系统,从新绘制。对于有复杂子视图的view,要把整个子视图树都所有重绘。

对比二者,基于layer的欺骗性的动画节省了什么?

  • 不用不断的更新view的数据
  • 不用不断的和图形硬件交互数据
  • 对于复杂的view,不用重绘整个图层树
  • 处理这些对图形硬件更擅长

能这么作的本质缘由我以为仍是由于咱们须要的动画是程式化的,有模板、有套路的。哪怕是稍微复杂的动画,也能够用关键帧动画来简化,最后仍是变成一个个离散独立的数据,按照既定的路线去呈现。若是动画是即时计算出来的,就无法这么干了,好比一个球扔到地上后怎么弹,是根据球的材料重量大小地面坡度等来计算的。

图层树

上面的动画系统,也就催生了layer3种不一样的图层树:

  • 模型树(model layer tree),存储了动画的结束值
  • 表现树(presentation tree),包含了动画正在进行中的值
  • 渲染层(render tree),用来表现实际动画的数据,文档无更多说明,应该是跟图形系统相关的数据,好比提供给GPU的bitmap等。

若是要拿到动画过程当中view的数据,能够经过表现树来获取。

性能问题

基本就是off-screen离屏渲染的各类问题

1. 圆角

iOS9以后系统已优化,不考虑。解决方案我认为使用layer覆盖层最好,圆角问题本质是mask,看下面mask部分。

2. 阴影,解决方案:加上shadowPath,替换shadowOffset

为何使用shadowPath能够解决这个问题,我没有找到其余文章说这个,系统文档也只有蛛丝马迹,但根据各方面资料,我作了一个合理的推测。

label的阴影你会发现是跟随文字变化的,而若是label有背景色,阴影就是根据外边框来的。一个imageView,背景色为空,而后使用一个有镂空效果的图片,就会发现阴影是跟着图片那些不透明的那部分来的。

文字阴影

镂空图片阴影

因此我推断:阴影是根据layer的alpha值来生成的。模拟一下生成的过程:分配一块一样大小的shadowlayer,在原layer的alpha不为0的地方,shadowlayer填上shadowColor,就跟现实里的影子生成原理同样,不透明的部分才生成阴影。而后把这个shadowlayer作一个偏移(shadowOffset)加到原layer下面。

并且这个alpha不是指当前layer的内容,而是当前layer和它全部的子layer合成后的alpha,也就是若是layer上面仍是多个子layer,会把这些视图合成到一块儿,再查看alpha值。用多个imageView错开叠加到一块儿就可测试出来。

也就是阴影层是根据内容即时计算出来的,并且会触发离屏渲染,因此消耗巨大。

使用shadowPath以后,那么阴影层的形状就固定了,就相似于加了一个subLayer,不会触发离屏渲染。

shadowPath的注释:

If you specify a value for this property, the layer creates its shadow using the specified path instead of the layer’s composited alpha channel

这里的composited就是指当前layer和全部子layer混合后的结果。有了上面的解释,这句话应该就明白了。

注:在iPhone6上还会卡顿,在8和X上已经很流畅了

3. mask

直接使用CALayermask属性会致使离屏渲染,查看注释

A layer whose alpha channel is used as a mask to select between the layer's background and the result of compositing the layer's contents with its filtered background

mask做用的也不仅是当前layer的内容,而是layer和它全部子layer的合成内容。这个也是能够测试的,设置viewA的layer的mask,而后无论在viewA上加多少个视图都是会被mask做用到。

解决方案是,添加一层layer在最上层来实现蒙版。mask的效果是,alpha>0的部分,内容能够透出来,而为0的部分,内容彻底遮蔽。

能够添加一个alpha正好相反的maskLayer2在最上层,根据混合效果,maskLayer2的alpha为0的地方内容能够透出来,对应就是原maskalpha>0的地方,也是内容能够透过来的地方。

惟一的麻烦就是对于内容变化的视图,添加一个新视图后,新视图的内容会跑到maskLayer2的上面,对这个新视图就没有蒙版效果了。

圆角的解决方案之一就是这个,以前圆角的本质也是添加了mask,从而致使的离屏渲染。

4. shouldRasterize光栅化

这个也是比说的,从前面的几个性能问题里能够看出,性能问题主要由于两点:1.离屏渲染 2.对复杂layer图层每次都要从新计算合成内容

光栅化的优化是针对后一个问题的,好比有10个视图,互相叠加在一块儿,每次都要计算叠加都得内容,开启这个效果后,就把计算后的内容生成一张位图(bitmap),以后渲染引擎会缓存和重用这个位图,而避免从新计算。

举个例子:前者就相似你要告诉一我的手机长什么样子,而后你造了一台手机给他看,每介绍给一我的你就要造一个手机;后者相似你把手机造好了以后拍了一张照,而后每次要介绍给别人,就给它看这个照片就行了。

缺点就是,若是样式是不断变化的,重用效果就会下降,并且存储位图会增长内存消耗。

实际测试:在tableView的cell上面添加文字的阴影,而后文字是随机变化的。阴影会致使离屏渲染,而文字的阴影又没法使用shadowPath来指定,因此会卡顿明显。

  • 开启shouldRasterize以后效果显著。
  • 文字是否是变化并无区别,可能shouldRasterize的重用和变化的概念和内容上的变化并非一个意思。对于tableView而言,新的cell都是没获得重用的,在测试工具里显示都是红色
  • 若是view开启maskToBounds,效果不好。虽然仍然只是新的cell得不到重用。只能说mask带来的性能消耗太大
关于离屏渲染的猜想

通过上面几个触发离屏渲染的属性的认知,发现一个共性,就是它们都须要layer和它的子图层树合成后的结果。mask是这样,阴影也是这样,开启shouldRasterize以后也是这样。

假设正常的内容是A,而后渲染出图形GA,而后你要加一个B内容,那么就是把内容A和B的结果作一个混合(blend)就行了。

可是若是B的内容是基于A呢?你必须先把A渲染出来,才能去生成B,那么在生成B的时候A存放在哪里?这就须要开辟一块新的缓冲区(frame buffer),把A的结果输出到这个地方,而不可以直接输出到屏幕。而后在那个新的环境(context),把A和B合成结束在切回到原来的context,在输出到屏幕。

这就是我对离屏渲染流程和缘由的猜想。

原文地址


推荐文集

* 抖音效果实现

* 音视频学习从零到整

* iOS面试题合集

相关文章
相关标签/搜索