Core Animation Basic

图层是绘图和动画的基础

Layer 是Core Animation所有操作的核心。iOS中view其实仅仅做了布局和事件处理,和视觉相关的所有操作都是由所关联的Layer对象完成。

基于layer的绘图模型

大部分图层都不会进行实际的绘图操作。它会根据view的内容生成对应的位图,并将其缓存。随后更改图层的属性时,系统仅仅只是更改与图层对象关联的状态信息。当触发动画时,Core Animation会将图层的位图和状态信息传递给图形硬件,图形硬件会使用新信息渲染位图,如图1-1所示。在硬件中操作位图会产生比在软件中更快的动画。

因为它操纵静态位图,所以基于图层的绘图与更传统的基于视图的绘图技术有很大不同。使用基于视图的绘图时,对视图本身的更改通常会导致调用视图的drawRect:方法以使用新参数重绘内容。但是以这种方式绘制是很昂贵的,因为它是在主线程上使用CPU完成的。核心动画通过在硬件中操纵缓存的位图来实现相同或类似的效果。

尽管Core Animation尽可能使用缓存内容,但仍必须提供初始内容并不时更新。应用有多种方法可以为图层对象提供内容,详细信息请参阅Providing a Layer’s Contents

设置Layer的内容

图层的内容由包含要显示的可视数据的位图组成。可以通过以下三种方式之一为该位图提供内容:

  • 将图像对象直接指定给图层对象的contents属性。(适用于从未或很少更改的图层内容)
  • 实现图层的delegate方法,让delegate绘制图层内容。(适用于可能会定期更改并可由外部对象提供的图层内容)
  • 定义图层子类并覆盖其绘图方法以提供图层内容。(如果必须创建自定义图层子类或者如果要更改图层的基本绘图行为,则此技术是合适的)

使用图像作为图层的内容

由于图层只是用于管理位图图像的容器,因此可以将图像直接指定给图层的contents属性。分配给图层的图像必须是CGImageRef类型。(在OS X v10.6及更高版本中,您还可以分配NSImage对象。)分配图像时,要注意提供其分辨率与本机设备分辨率匹配的图像。对于具有Retina显示屏的设备,可能还需要调整contentsScale属性。

使用delegate提供图层的内容

如果图层的内容动态更改,则可以使用delegate对象在需要时提供和更新该内容。在显示时,图层调用delegate方法提供所需的内容:

  • 如果您的delegate实现了displayLayer:方法,那么该实现负责创建位图并将其分配给图层的contents属性。
  • 如果delegate实现了drawLayer:inContext:方法,Core Animation会创建一个位图,创建一个图形上下文以绘制到该位图,然后调用您的委托方法来填充位图。您的所有委托方法都要绘制到所提供的图形上下文中。

委托对象必须实现displayLayer:drawLayer:inContext:方法。如果同时实现了displayLayer:drawLayer:inContext:方法,则仅调用displayLayer:方法。

覆盖该displayLayer:方法最适合自己加载或创建想要显示的位图的情况。清单2-3显示了displayLayer:委托方法的示例实现。在此示例中,委托使用辅助对象来加载和显示所需的图像。委托方法根据自己的内部状态选择要显示的图像,在示例中,该状态是调用的自定义属性displayYesImage

Listing 2-3 Setting the layer contents directly

1
2
3
4
5
6
7
8
9
10
11
- (void)displayLayer:(CALayer *)theLayer {
// Check the value of some state property
if (self.displayYesImage) {
// Display the Yes image
theLayer.contents = [someHelperObject loadStateYesImage];
}
else {
// Display the No image
theLayer.contents = [someHelperObject loadStateNoImage];
}
}

如果没有预渲染图像或辅助对象来创建位图,则delegate可以使用drawLayer:inContext:方法动态绘制内容。清单2-4显示了该drawLayer:inContext:方法的示例实现。在此示例中,委托使用固定宽度和当前渲染颜色绘制简单的弯曲路径。

Listing 2-4 Drawing the contents of a layer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)drawLayer:(CALayer *)theLayer inContext:(CGContextRef)theContext {
CGMutablePathRef thePath = CGPathCreateMutable();

CGPathMoveToPoint(thePath,NULL,15.0f,15.f);
CGPathAddCurveToPoint(thePath,
null,
15.f,250.0f,
295.0f,250.0f,
295.0f,15.0f);

CGContextBeginPath(theContext);
CGContextAddPath(theContext,thePath);

CGContextSetLineWidth(theContext,5);
CGContextStrokePath(theContext);

//release path
CFRelease(thePath);
}

通过子类提供层内容

如果要实现自定义图层类,则可以重载图层类的绘图方法来绘图。图层本身生成内容的情况并不常见,不过图层可以管理内容的显示。例如,CATiledLayer该类通过将大图像分成可以单独管理和呈现的较小图块来管理大图像。因为只有图层具有关于在任何给定时间需要渲染哪些图块的信息,所以它直接管理绘图行为。

子类化时,您可以使用以下任一技术绘制图层的内容:

  • 重载display方法并直接设置图层的contents属性。
  • 重载drawInContext:方法并使用它绘制到提供的图形上下文中。

使用哪种方法取决于在绘图过程中需要多少控制粒度。display方法是更新图层内容的主要入口点,重载这个方法可以完全控制该过程。重载以后就需要自己来创建CGImageRef并赋值给contents属性。所以,如果只是想绘制内容(或让图层管理绘图操作),则重载drawInContext:方法就行了,后面的细节让系统来处理。

图层的几何特性

视图的frame是一个虚拟属性,是根据 bounds,position以及transform计算而来,所以当其中任何一个值发生改变,frame都会变化。反之,改变frame的值同样会影响到他们中的值。

当对图层做变换的时候,比如旋转或者缩放,frame实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域,也就是说 frame的宽高可能和bounds的宽高不再一致。

image-20200414163505778

视觉效果

图层蒙板

CALayer的mask属性可以设置图层蒙板。mask本身就是个CALayer类型,它定义了父图层的部分可见区域,mask本身的颜色不起作用,起作用的是alpha值,它决定了是隐藏还是部分/全部显示父图层的内容。前面说到过CALayer的content可以是一张图,可以直接用带alpha通道的图来设置mask;也可以自定义绘图逻辑,这也意味着mask的内容可以编程动态来更新。

拉伸过滤

􏱲􏳊􏱾图片在放大或者缩小时,系统会进行从新采样。CALayer提供了3中采样模式:

  • kCAFilterLinear
  • kCAFilterTrilinear

  • kCAFilterNearest

默认是kCAFilterLinear,不过如果有时候放大太多会不清晰,此时使用kCAFilterTrilinear就会好一点。kCAFilterNearest适用于颜色值较小,纯色较多的情况。

组透明

有时给空间设置了透明度,但是控件的子视图也有一定的透明度,此时看起来就会比较怪异。可以启用 shouldrasterize属性来解决这个问题。在设置这个属性后,还要确保对应的rasterizationscale和屏幕分辨率相匹配。

隐式动画

CALayer的动画是默认开启的,如果改变一个单独的Layer(非UIView的内部layer)的可做动画的属性,它会自动播放一个从原值到新值的动画,因为都是系统默认的,没有额外的动画逻辑所以也叫隐式动画。

动画的类型和属性有关,如颜色和大小的动画就会有区别,动画的时间有默认设置,这些都是由CATransaction类来做管理。CATransaction可以用begin和commit方法来提交动画事务,用setAnimationduration:方法设置当前事务的动面时间。在iOS4以后的API中,基本上都用 [UIView animateWithDuration: animations:^{ }] 来完成这个步骤,它在内部也是调用的CATransaction 的方法。

隐式动画的实现

需要注意的是,UIView关联的CALayer的隐式动画默认被禁用了。关于被禁用的细节,需要先了解隐式动画的实现。

CALayer的属性被修改时候,它会调用actionForKey:方法,传递被修改属性的名称。后面的处理逻辑大致如下:

  • 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的actionForLayer:forKey方法。
  • 如果有,直接调用并返回结果。如果没有委托,或者委托没有实现actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
  • 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
  • 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的defaultActionForKey:方法。

所以一轮完整的搜索结束之后,actionForKey:要么返回空(这种情况下将不会有动面发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画.

于是这就解释了UKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并实现了actionForLayer:forKey。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动面block范围之内,它就返回了一个非空值。

不过这并不是禁止隐式动画的唯一办法。CATransaction有个方法叫setDisableActions:可以设置全局开启/关闭隐式动画。

呈现树和数值树

当你修改一个CALayer的属性,它的属性值是马上更新到新值的(如果你读取它的数据,你会发现它的值在你设置它的那一刻就已经生效了)。但是屏幕上的绘制效果并不是瞬间更新。这是因为设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画结束之后将要变化的外观。

CALayer的属性,实际上是在定义当前事务结束之后图层如何显示的模型。Core Animation扮演了一个控制器的角色,并且负责根据图层行为和事务设置去不断更新视图的这些属性在屏幕上的状态。

它是一个典型的微型MVC模式。CALayer是一个连接用户界面(就是MVC中的view)虚构的类,但是在界面本身这个场景下,CALAyer的行为更像是存储了视图如何显示和动画的数据模型。

在iOS中,屏幕每秒钟重绘60次。如果动画时长比1/60秒要长,Core Animation就需要根据动画属性给每一帧生成动画效果,所以必须要知道当前显示在屏幕上的属性值的记录。每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过presentationLayer方法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值。

呈现树通过图层树中所有图层的呈现图层所形成。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用presentationLayer将会返回nil。

呈现树也可以反查自己对应的数值树。在呈现图层上调用modelLayer方法将会返回它对应的数值树的CALAyer。而在一个数值树上调用modeller会返回self。

image-20200415115134567

大多数情况下直接设置CALayer的属性就够了,不过在做同步动画以及做用户交互时,用呈现树会更精确一些:

  • 做基于定时器的动画,这个时候准确地知道在某一时刻图层显示在什么位置就比较重要。
  • 动画的图层响应用户输入时,可以使用hitTest:来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用hitTest:会更精确,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。

显式动画

常用动画

CABasicAnimation是执行显式动画的类。建议在将动画添加到图层之前设置动画的开始和结束值,持续时间或其他参数。下面代码显示了如何使用动画对象淡出图层。创建对象时,指定要设置动画的属性的关键路径,然后设置动画参数。要执行动画,可以使用该addAnimation:forKey:方法将其添加到要设置动画的图层。

1
2
3
4
5
6
7
8
CABasicAnimation* fadeAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeAnim.fromValue = [NSNumber numberWithFloat:1.0];
fadeAnim.toValue = [NSNumber numberWithFloat:0.0];
fadeAnim.duration = 1.0;
[theLayer addAnimation:fadeAnim forKey:@"opacity"];

// Change the actual data value in the layer to the final value.
theLayer.opacity = 0.0;

注意: 创建显式动画时,建议设置fromValue。如果未指定此属性的值,Core动画将使用图层的当前值作为起始值。如果已将属性更新为最终值,可能没有效果。

和通过更新view属性触发的隐式动画不同,显式动画不会修改图层树中的数据。显式动画仅生成动画。在动画结束时,Core Animation从图层中移除动画对象,并使用当前数据值重绘图层。如果希望显式动画的更改是永久性的,则还必须更新图层的属性。

隐式和显式动画通常在当前运行循环周期结束后开始执行,并且当前线程必须具有run loop才能执行动画。如果更改多个属性,或者向图层添加多个动画对象,则会同时对所有这些属性更改进行动画处理。

除了CABasicAnimation还有一些动画类可以使用,如用来做关键帧动画的CAKeyframeAnimation、以及有弹性效果的CASpringAnimation

虚拟属性

􏷄属性动画在设置时,往往要设置keyPath来指定是哪个属性。其实这个keyPath不仅仅可以写属性名。还可以和它的名字一样,写属性路径。如做一个旋转效果,可以用transform.rotation这个路径来达到效果。如下面的代码一样:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)viewDidLoad {
[super viewDidLoad];
CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 128, 128);
shipLayer.position = CGPointMake(150, 150);
[self.containerView.layer addSublayer:shipLayer]; //animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = 2.0;
animation.byValue = @(M_PI * 2);
[shipLayer addAnimation:animation forKey:nil];
}

这个效果要比设置keyPath为transform,然后toValue设置成一个旋转完毕的transform好。这样的好处在于:

  • 可以一步旋转多于180度的动面
  • 可以用相对值而不是绝对值旋转(设置 byvalue而不是 tovalue)。
  • 可以不用创建 CATransform3D,而是使用一个简单的数值来指定角度
  • 不会和 transform.position或者 transform.scale冲突(如果同样是使用关键路径来做动画属性的话)

transform.rotation属性本身并不存在,它实际上是一个 CALayer用于处理动画变换的虚拟属性。同样的也不能直接设置transform.rotation或者transform.scale,他们不能被直接使用。当你对它们做动画时,Core animation自动地根据通过 CAValuefunction来计算的值来更新 transform属性。

CAValuefunction用于把我们赋给虚拟的 transform.rotation简单浮点值转换成真正的用于摆放图层的CATransform3D矩阵值。默认的CAValuefunction的行为可以修改,设置CAPropertyAnimation的valuefunction属性后,默认的行为就会用自定义的函数替代。

动画组

CABasicAnimation和 CAKeyframeAnimation仅仅作用于单独的属性。而CAAnimationGroup可以把这些动画组合在一起。 CAAnimationGroup是另外继承于CAAnimation的子类,它添加了一个animations数组的属性,用来组合别的动面。

过渡效果

属性动画只对图层的可动画属性起作用,有时候想对视图的层级关系变化做动画,就有了过渡效果的概念。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。CATransition是专门用来实现过渡效果的类。

1
2
3
4
5
6
7
8
9
- (IBAction)switchImage {
CATransition *transition = [CATransition animation];
transition.duration = 0.25;
transition.type = kCATransitionFade;
transition.subtype = kCATransitionFromRight;
[self.imgView.layer addAnimation:transition forKey:nil];
self.currentIndex = (self.currentIndex + 1) % [self.images count];
self.imgView.image = self.images[self.currentIndex];
}

过渡动面和之前的属性动面或者动面组添加到图层上的方式一致,都是通过addanimation:forKey:方法。但是和属性动画不同的是,对指定的图层一次只能使用一次 CATransition,无论你对动画的key设置什么值,系统都会把它的key设置成kCATransition。

除了CATransition,UIView还提供了两个方法:

1
2
[UIView transitionFromView:nil toView:nil duration:0 options:1 completion:^(BOOL finished) {}];
[UIView transitionWithView:nil duration:0 options:1 animations:^{} completion:^(BOOL finished) {}];

这两个方法也能实现过渡效果。

图层时间

CAMediaTiming协议

持续和重复

duration表示动画持续时间,repeatCount表示动画持续次数。两者默认都是0,表示重复一次,时长0.25秒。

如果想重复指定时长的话,可用repeatDuration属性指定。

另外autoreverses属性很有用,用来做循环往复动画的时候,直接指定这个属性为YES就能达到效果。

相对时间

每个动画都有它自己的描述时间,可以独立地加速,延时或者偏移。

beginTime指定了动画开始前的延迟时间。这里延迟是从动画添加到可见图层开始算。

speed是一个时间倍数,默认为1.0,如果速度变成2.0,那么duration为1的动画,实际时间将变成0.5秒

timeOffset和beginTime类似,但是timeOffset是让动画快进到某一点,有点像拖动视频进度条的概念。对于duration为1秒的动画,timeOffset为0.5表示动画从一半的地方开始。

填充模式

如果beginTime大于0,那动画添加到图层上以后,会有一段时间啥动画都没有。类似的,当removeOnCompletion被设置成NO后,动画结束时,仍旧会保持最后一帧。那动画开始之前和动画结束之后,动画的属性是什么?这个就需要用fillMode属性来指定。

fillMode是个字符串类型,取值有一下几种,默认是kCAFillModeRemoved。

  • kCAFillModeForwards 动画开始前,保持动画开始的效果
  • kCAFillModeBackwards 动画结束后,保持动画结束的效果
  • kCAFillModeBoth 两者都有
  • kCAFillModeRemoved 当动画结束时,显示图层模型指定的值

用户交互动画

timeOffset一个很有用的功能就是可以和用户的交互关联起来,变成用户控制的交互式动画。

渐入渐出

常用效果

渐入渐出效果是动画中常用的效果。CAAnimation的timingFunction可以指定更丰富的动画加速度效果。

  • kCAMediaTimingFunctionLinear 线性
  • kCAMediaTimingFunctionEaseIn 渐入
  • kCAMediaTimingFunctionEaseOut 渐出
  • kCAMediaTimingFunctionEaseInEaseOut 渐入渐出
  • kCAMediaTimingFunctionDefault 默认

自定义效果

动画加速度函数除了系统预置的几个外,还可以自己定义。除了functionwithName:之外,CAMediaTiming Function同样有另一个构造函数,一个有四个浮点参数的functionwithcontrolpoints:::: 使用这个方法,可以自定义一个加速度函数,这个函数的参数是一个三次贝塞尔曲线,4个参数分别是曲线的四个控制点。

基于定时器的动画

CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新前启动。它有个整型的frameInterval属性,制定了间隔多少帧之后才执行。默认是1,意味着每次屏幕每次更新之前都会执行一次。

创建CADisplayLink时,需要制定一个runloop和runloop mode。

性能调优

动画的组成部分

动画和屏幕上的图层混合是由一个单独的进程执行,这个进程也叫渲染服务。当运行一段动画的时候,这个过程会被四个分离的阶段打破:

  • 布局。这个是准备视图层级以及设置图层属性的阶段
  • 绘制。这个是图层对应的内容被绘制的阶段。可能涉及到drawRect:和drawLayer:inContext:等方法
  • 准备。这个是Core Animation准备发送动画数据到渲染服务的阶段。这个阶段同时也会进行图片解码。
  • 提交。这个是最后阶段。Core Animation会打包所有图层和动画属性,然后通过IPC发送到渲染服务进行显示

这些步骤是发生在应用程序之内的部分。当动画在屏幕上显示之前仍然有更多的工作。一旦打包的图层和动画达到渲染服务进程,它们会被反序列化来形成另一个叫做渲染树的图层树。使用这个树状结构,渲染服务队动画的每一帧做出如下工作:

  • 对所有的图层属性计算中间值,形状来执行渲染
  • 在屏幕上渲染可可见的三角形

所有一共有六个阶段。最后两个阶段在动画过程中不停地重复。前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。

CPU相关

在动画产生的过程中,CPU的工作更多集中在动画开始之前的准备工作上。所以如果动画效果中有CPU瓶颈,不会影响到帧率,但是会有延迟感。

下面这些CPU操作都会延迟动画的开始时间:

  • 布局计算。如果视图层级结构非常复杂,而且使用了复杂的自动布局约束,那会增加不少CPU的计算量
  • 视图资源的Lazy Loading。这个技术是为了节省内存等资源而产生的,不过在这里临时进行加载时会进行很多IO以及潜在的内存交换工作,也会有很多CPU计算量
  • Core Graphics绘图。
  • 解压图片。
GPU相关

大部分的CALayer的属性都是用GPU来绘制的。如背景以及边框,如果contents是一张图片,也会直接被GPU渲染出来。通过GPU硬件能极大的加速工作速度,不过下面这些因素会增大GPU的工作负担,在开发中应该避免:

  • 视图层级结构非常复杂。现在GPU的性能没什么问题,但因为绘图时图层要通过IPC发送到渲染服务进程,太多的图层会达到CPU的瓶颈,而造成GPU也跟着受牵连。
  • 大量重绘。重叠的半透明图层需要进行混合,这会增加GPU的工作量。如果图层和背景颜色一样,那就可以把图层设置成不透明,省去混合的步骤。
  • 离屏渲染。圆角、遮罩、阴影、图层光栅化等效果都会强制Core Animation提前进行预渲染。
  • 较大的图片。如果图片过大超过GPU的支持范围。需要用CPU进行预处理。
IO相关

有时候影响性能的的主要原因并不在处理器上,而是在IO性能上,所以排查问题时,也不要忘记这个方向。

常用图形性能debug思路

  • 帧率是否是60帧?
  • CPU或GPU是否有性能瓶颈?
  • 有不必要的CPU渲染吗?
  • 是否有太多的离屏渲染?
  • 是否有太多的透明图层,导致需要执行大量的混合操作?
  • 是否用到了不常用的图片格式,或者超大的图片?
  • 是否有高计算量的视觉效果?
  • 是否有异常的视图层次结构,或者有些视图渲染了,但是没有出现在屏幕上?

Instruments工具

Time Profiler

代码执行时间分析能诊断出那个方法正在消耗CPU。发现潜在的造成CPU瓶颈的代码。

View Debugging (以前的Core Animation选项)

主要用来监测Core Animation的性能。以前是放在Instruments工具的Core Animation中,后来从Xcode9.3开始移到Xcode中,在Debug / View Debugging / Rendering 菜单下面就能找到。

  • Color Blended Layers 多个半透明图层叠加会导致混合,增加GPU负担。这里显示出半透明的图层。
  • Color Hits Green and Misses Red 显示光栅化的图层缓存命中情况,如果miss过多需要考虑优化。
  • Color Copied Images 图片数据是通过IPC服务发送到渲染进行,可能是Core Animation自己合成的图片,不是现成的图片资源。
  • Color Layer Formats 把相同类型的Layer用相同颜色标志出来。
  • Color Misaligned Images 高亮一些被拉伸或者缩放以及没有对齐到整型坐标的图层。
  • Color Offscreen-Rendered Yellow 使用离屏渲染的地方会标记为黄色
  • Color Compositing Fast-Path Blue 用于标记由硬件绘制的路径,蓝色越多越好
  • Flash Updated Regions 标记频繁发生重绘的区域,越小越好
  • Color Immediately 一般Core Animation每隔10ms刷新调试图层的颜色状态,对有些调试功能,它可能间隔太长,开启这个选项能每帧都更新这些调试图层的颜色状态。不过可能会引起性能问题,或者造成测试不准的情况。
Metal

监测GPU的利用率,发现潜在的GPU瓶颈。

高效绘图

软绘制

软件绘图通常要比GPU绘制慢得多。不过GPU并不是万能的,在有些情况下还是需要用软绘制。

一般用Core Graphics框架实现的绘制就是软绘制。􏰶􏰋􏱒􏴘当实现CALayer的Delegate的drawLayer:inContext:方法,或者UIView的drawRect方法后,系统就启用了一个drawing context,这个上下文环境保存了屏幕上的绘制信息,即屏幕上所有的像素的色值信息,所以内存的占用量也是巨大的。

综上,软绘制一般尽量避免。

矢量图形

Core Graphics一般用来实现图片或者图层难以达到的效果,如用户的手绘效果等。一般在实现手绘路径都是用UIBezierPath来实现,它在路径复杂时,性能会遇到瓶颈。Core Animation其实为这类需求专门提供了有硬件加速的CALayer,如绘制图形的CAShapeLayer、绘制文本的CATextLayer、绘制渐变的CAGradientLayer。

脏矩形

进一步提高效率的办法就是局部绘制,大部分时候,用户手绘效果都是局部的,所以只用更新局部的内容就行了。所以,加上更新范围监测,调用setNeedsDisplayInRect:方法来进行局部刷新也是个好办法。

异步绘制

CATiledLayer可以把大的layer分割成小块,每个块在不同线程中同时绘制。所以使用CATiledLayer是实现多线程绘制的一种方案。

drawsAsynchronously是CALayer的一个属性,当开启时。它会把接收到的绘制指令保存到一个队列中,等draw方法体执行完毕以后(相当于收集到了所有的绘制指令),再并行执行绘制指令。未开启时这个在需要频繁绘制的视图上有比较好的效果,反之可能效果不大。所以开启这个属性前后需要进行性能测试,保证开启后确实有性能提升。

图像IO

加载和潜伏

图片的加载也会影响到性能,所以在进行性能分析时也需要考虑到这一点。加载是可以用多线程技术,充分利用多核硬件。

缓存

[UIImage imageNamed:]方法以及nib引用的图片是带缓存的,不过它仅用于bundle中的图片。自定义缓存可以用NSCache来实现。

文件格式

一般来说压缩比高的,文件体积小,解压慢;压缩比低的,文件体积大,解压快。

现在的图片都是压缩过的,显示到屏幕上时,都需要提前先解压缩成位图。图片像素尺寸以及像素分布范围还有图片文件格式都会影响到图片的显示速度。所以做性能测试时,这些因素都需要考虑。

图层性能

光栅化

CALayer的shouldRasterize是个很有用的功能。可以很有效地解决图层过多导致的性能问题。不过启用后,系统将会进行离屏渲染,并最终绘制到contents中,这也会有额外的内存和CPU开销,所以一般在内容比较复杂而且也不会频繁变化的图层中使用这个属性。

开启后可以用instrument工具来进行测试,看看光栅化后缓存的命中率怎么样。

离屏渲染

有些较为复杂的图形效果需要多个渲染结果组合起来,渲染的中间结果会保存到一块内存中,不是直接渲染到frame buffer,所以也叫离屏渲染。下面这些效果会产生离屏渲染:

  • Core Graphics相关的API
  • drawRect相关的方法
  • 文本相关的绘制,包括Core Text

  • 圆角(maskToBounds和cornerRadius同时开启)

  • 图层蒙板(圆角其实也是蒙板的一种)
  • 阴影
  • 光栅化
  • group opacity

离屏渲染主要的性能开销在于GPU的上下文切换,切换时会清空流水线以及管线屏障。(有点类似于CPU的jmp语句,会清空指令流水线)有时候如果简单的绘制操作,其切换上下文的时间成本比绘制所花费的时间还高。所以尽量避免大量使用这些效果,或者针对性的做一些优化。

CAShapeLayer

有时候遇到形状复杂的ui需求,用CAShapeLayer是个比较好的选择。

如果有大量使用圆角的情况,为了避免离屏渲染,可以直接用 [UIBezierPath bezierPathWithRoundedRect:cornerRadious:] 这个方法直接指定圆角路径,这样能直接使用硬件加速。

Stretchable Images

使用可拉伸图片也可能实现圆角、以及阴影效果。

ShadowPath

图形如果有圆角或者阴影,使用这个属性能避免离屏渲染。

混合以及过度绘制

GPU每一帧可以绘制的像素有一个最大限制(填充率),一般情况下情况下GPU绘制整个屏幕的所有像素是很轻松的。但如果有很多重叠图层导致需要不停重绘同一区域的话,就可能掉帧。GPU在渲染时会根据z坐标深度计算完全被挡住的部分,这个过程花费的计算量和图层数量成正比。不同图层的透明重叠像素(即混合)到一个图层时,消耗的资源也是相当客观的。所以为了加速处理进程,要尽量减少图层数,并且尽量不要用透明图层。

如果图层内的子图层较多,可以开启光栅化,这样所有图层会合并成一个图像缓存起来,减少GPU的工作量。

减少图层数量

初始化图层,处理图层,打包通过IPC发给渲染引擎,转化成Metal几何图形,这些是一个图层的大致资源开销。减少图层数量能减少CPU的工作量,在做性能优化时,不要忘记这一点。

避免不必要的绘制
  • 在屏幕外,或者父view的边界外
  • 在一个完全不透明的view后
  • 完全透明的图层

这是避免不必要绘制的典型,在编码阶段就要避免。

对象池

使用对象池来复用高成本的UI对象,比如UICollectionView和UITableView的复用机制。

Core Graphics绘制

虽然drawRect方法比较慢,但是如果视图层级结构比较复杂,用这个方法可能还是更快一点。不过这个也要和光栅化做对比着做性能测试,根据实际场景做技术选型。

renderInContext方法

大量的视图或者图层关联到了屏幕上将会是一个大的性能问题,但是没有与屏幕关联的图层树不会被送到渲染引擎,也不会和主线程竞争渲染服务资源。所以一些复杂的UI可以先用renderInContext渲染成单个图片,然后再合适的时机再显示。这样就把原来的负载错峰处理了。

专用图层

使用专用图层的方式有两种,一种是直接实例化专用图层,然后添加到对应的视图树上,这样的好处是比较方便,在轻量级的使用中非常灵活。但是如果遇到布局调整,以及较复杂的调整逻辑时,这种方法就略显不足了。

另外一种方法就是自定义一个UIView的子类,在layerClass方法中返回对应的专用图层类。如:

1
2
3
4
5
@implementation CustomView
+ (Class)layerClass {
return CAShapeLayer.class;
}
@end

这样实例化一个自定义的View后,内部的Layer就是专用的Layer,这样就能方便地进行自定义。

CAShapeLayer

它是一个通过矢量图形来进行绘制的Layer。它直接使用硬件加速、而且它没有对应的contents的bitmap、边界之外的内容也会被绘制。

CATextLayer

显示文本的Layer,如果有自定义文本的需求可以考虑使用它。

CATransformLayer

适用于3D仿射变换较多的情况。

CAGradientLayer

渐变专属。

CAReplicatorLayer

界面需要重复大量的相同图形, 或者对图形进行大量规则的变化,如仿射变换等,就可以用这个Layer提高性能。

CAScrollLayer

自己实现ScrollView的类似功能时,可以在它基础上进行,不用从零开始。

CATiledLayer

在绘制超大图片时,可能会遇到内存问题,这时候可以只显示一小部分内容。使用CATiledLayer是个不错的选择。实现它的代理方法,然后动态绘制当前显示的一小部分内容,这样能节省大量的内存资源。

而且它的绘制方法会异步调用,系统能最大化利用多核CPU加速绘制。

CAEmitterLayer

高性能的粒子引擎,实现粒子动画可以考虑用它。

CAEAGLLayer / CAMetalLayer

调用底层OpenGL和Metal的Layer,由于太过底层,实现具体逻辑的的代码量巨大。一般使用GLKit或者MetalKit,它们对前面提到的Layer都有对应的UIView的封装。

AVPlayerLayer

它是用来播放视频的,MPMoviePlayer的底层实现就是它。