iOS UI 优化 - Core Animation 实现探索
小编回顾粗略的写完 19 年 想要写的系列博文中 iOS UI
优化总纲中第一篇博文 Core Animation
一直在想怎么样展示自己的理解,就先从我们熟知的 Core Animation
在整个 APP
提供架构方面来进行对比。

注:上图分别是从 2014 年的
WWDC
讲述 Advanced Graphics and Animations for iOS Apps 中截取 和 2018 年最新描述 About Core Animation 文档获取Core Animation
在架构中实现。
由上面显示具体信息我们可以得出下面结论:
1、
Core Animation
作为主要内容载体承接对iOS
和OS
具体显示的绘制;
2、UIKit
框架从初始化到通过GPU
生成纹理显示在屏幕上是依靠Core Animation
、Core Graphics
、OpenGL ES
(iOS
11) 和Matel
(iOS
12) 来实现;
3、在iOS 8.0
中苹果官方尝试使用Metal
来替代OpenGL ES
在目前最新的官方文档中实现在绘制生成纹理底层架构依据Metal
进行。
想要深入了解一个架构我们就从最基本的系统 API
着手,下面是小编对系统 API
根据分类来对具体类的 API
进行整理。

看过上图我们首先要明白在 Core Animation
不仅仅满足我们 UIKit
移动端 iOS
使用,而且在 OS
中的 AppKit
同样也是对其进行再次封装。但是这里我们仅仅讨论在 iOS
平台具体实现情况。
数据表格可以看出 Core Animation
在 iOS
架构中负责 Layer
的内容绘制和动画的执行。执行绘制实现的类:CATypeLayer
和 CATypeAnimation
及其在实现动画实现参数例如:Animation Group
和 Animation Timing
。
Core Animation 绘制
细细想来我们在做应用实际上展示给用户内容,而在展示内容基础上我们多数是采用 UIKit
中的继承于 UIView
非线程安全的控件来做内容展示。这里我们从 Core Animation
绘制开始剖析怎么实现?
UIView
VS Core Animation
在 Core Aimation
的 API
的数据表中我们可以看到 Layer Basics
中有三个类分别是:CALyer
、CALayerDelegate
和 CAAction
。既然 iOS
中界面的初始化均是以父类 UIView
来实现,那么 UIView
和三者有什么关系呢?
UIView
和 CALayer
Layer
层作为 Core Animation
主要绘制基类为 UIKit
中 View
提供内容绘制的容器,动画实现基础。下面通过实际例子来进行解答:
通过实现
LJView
继承View
和LJLayer
继承Layer
,并且重写LJView
中的方法+ (Class)layerClass;
如下:
+ (Class)layerClass {
NSLog(@"<%@:%p>(%s)", self.class, self, __func__);
return [LJLayer class];
}
然后重写下面的方法:
1、
LJView
的方法:- (CGPoint)center;
、- (void)setBounds:(CGRect)bounds;
、- (void)setCenter:(CGPoint)center
和- (void)setFrame:(CGRect)frame;
2、LJLayer
的方法:- (CGPoint)position;
、- (void)setBounds:(CGRect)bounds;
、- (void)setPosition:(CGPoint)position;
、 和- (void)setFrame:(CGRect)frame
。
初始化 _ljView = [[LJView alloc ] init];
我们可以看到打印的日志如下:
//把 LJView 的 layer 加载 LJLayer
LJCAStructure[39931:4249519] <LJView:0x10ca54190>(+[LJView layerClass])
//1、调用 View 中给定的 Frame 来创建 Layer
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer setBounds:])
//2、创建 Layer 后通过调用栈创建 View
LJCAStructure[39931:4249519] <LJView:0x7f9ae840c220>(-[LJView setFrame:])
//3、View 回调中心点 center 实际委托给 Layer 的 position 返回当前的中心点
LJCAStructure[39931:4249519] <LJView:0x7f9ae840c220>(-[LJView center])
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer position])
//4、通过上面 LJView 设置 Frame 和 Center 后,Layer 层再次重新对其 Frame、position 和 bounds 进行设置,
//这里也可以看到 Frame 其实是有 position 和 bounds 来决定
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer setFrame:])
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer setPosition:])
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer setBounds:])
//5、View 在实现布局后,再次获取其 center 再次委托与 Layer position 来获取中心点
LJCAStructure[39931:4249519] <LJView:0x7f9ae840c220>(-[LJView center])
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer position])
上面我们可以看出在实现 LJView
在实现初始化的过程时,实际会委托给 Layer
层的 Frame
| Bounds
和 Position
来进行具体实例化的过程。在实例化过程有下面几个步骤:
1、
View
通过Layer
中的bounds
和position
来确定其的frame
;
2、然后Layer
根据上面的值再次对其frame
、position
和bounds
进行再次设置;
3、最后View
再次委托Layer
获取当前的中心点。
下面我们在根据每部具体操作在看下操作具体的调用栈
<LJLayer:0x6040002212a0>(-[LJLayer setBounds:])
调用栈:

上面我们可以看出在实际中的调用栈是:
a、[UIView init] 初始化
b、[UIView initWithFrame] 调用 initWithFrame 方法
c、UIViewCommonInitWithFrame 采用 Common frame 初始
d、[UIView _createLayerWithFrame] 根据 frame 创建 Layer
1、UIView
初始化时创建 Layer
然后调用其方法 setBounds:
。
<LJView:0x7f9ae840c220>(-[LJView setFrame:])
调用栈:

上面我们可以看出在实际中的调用栈是:
a、[UIView init] 初始化
b、[UIView initWithFrame] 调用 initWithFrame 方法
c、UIViewCommonInitWithFrame 采用 Common frame 初始
2、UIView
在初始创建 Layer
后对其 Frame
进行初始化调用 setFrame:
方法。
<LJView:0x7f9ae840c220>(-[LJView center])
调用栈:

<LJLayer:0x6040002212a0>(-[LJLayer position])
调用栈:

上面我们可以看出在实际中的调用栈是:
a、[UIView init] 初始化
b、[UIView initWithFrame] 调用 initWithFrame 方法
c、UIViewCommonInitWithFrame 采用 Common frame 初始
d、[LJView setFrame] 调用具体 View 设置 Parent frame 布局
e、[UIView(Geometry) setFrame]
f、[LJView center] 调用 View 中 Center 委托给 Layer 的 position
g、[LJLayer position]
3、这里可以看出在 2 基础上接着调用 View
的 Center
中心点实际是委托与 Layer
的 position
继续调用
<LJLayer:0x6040002212a0>(-[LJLayer setFrame:])
调用栈:

<LJLayer:0x6040002212a0>(-[LJLayer setPosition:])
调用栈:

上面我们可以看出在实际中的调用栈是:
a、[UIView init] 初始化
b、[UIView initWithFrame] 调用 initWithFrame 方法
c、UIViewCommonInitWithFrame 采用 Common frame 初始
d、[LJView setFrame] 调用具体 View 设置 Parent frame 布局
e、[UIView(Geometry) setFrame] 调用系统 View 的 Frame 设置
f、[LJLayer setFrame] 调用拥有 Layer 的 Frame
g、[CALayer setFrame] 调用系统 Layer 的 Frame 设置
h、[LJLayer setPosition] 设置 Layer 的中心点
4、在第 2 的基础上通过设置 Layer
的 Frame
实际设置当前的 position
中心点。
<LJLayer:0x6040002212a0>(-[LJLayer setBounds:])
调用栈:

上面我们可以看出在实际中的调用栈是:
a、[UIView init] 初始化
b、[UIView initWithFrame] 调用 initWithFrame 方法
c、UIViewCommonInitWithFrame 采用 Common frame 初始
d、[LJView setFrame] 调用具体 View 设置 Parent frame 布局
e、[UIView(Geometry) setFrame]
f、[LJLayer setFrame]
g、[CALayer setFrame]
h、[LJLayer setBounds] 设置 Layer 的 bounds 实际布局
5、在第 2 的基础上通过设置 Layer
的 Frame
实际设置当前的 bounds
中心点,也就可以看出 Layer
的 Frame
在设置情况实际是根据 Layer
的 bounds
和 position
来实际决定其 Frame
。
<LJView:0x7f9ae840c220>(-[LJView center])
调用栈:

<LJLayer:0x6040002212a0>(-[LJLayer position])
调用栈:

上面我们可以看出在实际中的调用栈是:
a、[UIView init] 初始化
b、[UIView initWithFrame] 调用 initWithFrame 方法
c、UIViewCommonInitWithFrame 采用 Common frame 初始
d、[LJView setFrame] 调用具体 View 设置 Parent frame 布局
e、[UIView(Geometry) setFrame]
f、[LJView center] 调用 View 中 Center 委托给 Layer 的 position
g、[LJLayer position]
6、在第 2 基础上设置完相关通过 Vi ew
中心点委托给 Layer
的 position
来获取。
上面在初始化
[[LJView alloc] init]
在实际调用过程中我们可以看到首先是通过[UIView _createLayerWithFrame]
创建Layer
然后根据Layer
的属性Poition
和Bounds
决定UIView
的Frame
确定含有UIView
在视图控制器或者是父UIVIew
的布局。
接下来我们对初始化后实例 _ljView
对其 Frame
赋值:
_ljView.frame = CGRectMake(0, 0, LJScreenWidth(), 200);
打印的日志如下:
LJCAStructure[39931:4249519] <LJView:0x7f9ae840c220>(-[LJView setFrame:])
LJCAStructure[39931:4249519] <LJView:0x7f9ae840c220>(-[LJView center])
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer position])
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer setFrame:])
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer setPosition:])
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer setBounds:])
LJCAStructure[39931:4249519] <LJView:0x7f9ae840c220>(-[LJView center])
LJCAStructure[39931:4249519] <LJLayer:0x6040002212a0>(-[LJLayer position])
在打印的具体调用栈我们可以看出再具体赋值的过程中实际的调用方法就是我们在进行
[[LJView alloc] init]
创建其Layer
后调用方式:
1、UIView
委托其包含的Layer
获取中心点来做判断;
2、通过Layer
来通过Bounds
和Position
来确定其Frame
;
3、再次获取UIView
委托其包含的Layer
获取中心点来做判断当前中心点。
UIView
和 CALayerDelegate
及 CAAction
我们在使用 UIView
布局中不少有些动画效果,在 iOS
里给出非常简洁的动画 API
。
下面👇我们给出一个最简单的例子:
[UIView animateWithDuration:2 animations:^{
self.view.center = CGPointMake(LJScreenWidth()/2, 300);
}];
在上述的运行之前我们在 LJView
重写下面方法:
1、
CALayerDelegate
方法- (id<CAAction>)actionForLayer:forKey:
;
2、LJLayer
中方法- (void)addAnimation: forKey:
在其中实现CAAnimationDelegate
委托给LJLayer
然后重写- (void)animationDidStart:
和- (void)animationDidStop: finished:
运行上面的代码获取下面的打印日志:
//设置当前的 center 中心点 | 这里也可以看出 UIView 实际是由 Layer 来进行设置
LJCAStructure[2471:50545] <LJView:0x7ff306d0d6f0>(-[LJView setCenter:])
LJCAStructure[2471:50545] <LJLayer:0x60400023f380>(-[LJLayer position])
LJCAStructure[2471:50545] <LJLayer:0x60400023f380>(-[LJLayer setPosition:])
//通过实现 CALayerDelegate 委托 | 返回当前的 CAAction 的协议给 Layer
//CAAction
//- (void)runActionForKey:event object:anObject arguments:dict;
LJCAStructure[2471:50545] <LJView:0x7ff306d0d6f0>(-[LJView actionForLayer:forKey:])
//判断 Layer 层 position 是否存在 | 获取当前 position
LJCAStructure[2471:50545] <LJLayer:0x60400023f380>(-[LJLayer position])
LJCAStructure[2471:50545] <LJLayer:0x60400023f380>(-[LJLayer position])
//Layer 获取动画添加
LJCAStructure[2471:50545] anim : <CABasicAnimation: 0x60400003eb40>
LJCAStructure[2471:50545] <LJLayer:0x60400023f380>(-[LJLayer addAnimation:forKey:])
//再次通过 View 来获取 actionForLayer:forKey: 判断是否动画再次天机
LJCAStructure[2471:50545] <LJView:0x7ff306d0d6f0>(-[LJView actionForLayer:forKey:])
LJCAStructure[2471:50545] <LJLayer:0x60400023f380>(-[LJLayer position])
LJCAStructure[2471:50545] anim : (null)
LJCAStructure[2471:50545] <LJLayer:0x60400023f380>(-[LJLayer addAnimation:forKey:])
//执行动画
LJCAStructure[2471:50545] <LJLayer:0x60400023f380>(-[LJLayer animationDidStart:])
//动画结束
LJCAStructure[2471:50545] <LJLayer:0x60400023f380>(-[LJLayer animationDidStop:finished:])
我们可以看到上面👆在 UIView
设置当前 center
赋值的时候,LJView
调用当前 actionForLayer:forKey:
,下面我们在 [UIView animationWithDuration:animations:]
重写下面方法:
NSLog(@"actionForLayer:forKey: %@", [self.ljView.layer.delegate actionForLayer:self.ljView.layer forKey:@"position"]);
[UIView animateWithDuration:2 animations:^{
NSLog(@"actionForLayer:forKey: %@", [self.ljView.layer.delegate actionForLayer:self.ljView.layer forKey:@"position"]);
self.ljView.center = CGPointMake(LJScreenWidth()/2, 300);
}];
在打印的具体内容:
//在 UIView block 外面设置返回 null
LJCAStructure[5104:110746] actionForLayer:forKey: <null>
//在当前的 UIView block 中进行设置获取 animationAction 对象
LJCAStructure[5104:110746] actionForLayer:forKey: <_UIViewAdditiveAnimationAction: 0x604000038cc0>
我们根据上面打印的日志在 LJLayer
中写下下面动画的伪代码:
- (void)setPosition:(CGPoint)position {
NSLog(@"<%@:%p>(%s)", self.class, self, __func__);
[super setPosition:position];
if ([self.delegate respondsToSelector:@selector(actionForLayer:forKey:)]) {
id obj = [self.delegate actionForLayer:self forKey:@"position"];
if (!obj) {
//隐式动画
}else if ([obj isKindOfClass:[NSNull class]]) {
//直接绘制
}else {
// CAAnimation
CAAnimation *ainmaion;
[self addAnimation:ainmaion forKey:@"position"];
}
}
}
通过上面在简单的通过 UIView
的 API
实现动画,我们可以看出当前在执行动画的过程中具体实现流程如下:
1、我们通过当前的
UIView
在对其属性进行赋值时,UIView
委托给Layer
进行属性设置;
2、UIView
通过实现CALayerDelegate
委托方法,通过iOS
系统来查找对应的CAAction
来传递给CALayer
;
3、Layer
层获取在UIView
的CAAimation
数据,添加到当前执行动画中;
4、执行动画。
Core Animation 绘制 - 特有 Layer
特有 Layer | Advantages VS Disadvantages |
---|---|
CATextLayer | 纯文本带有文本或者是是文本字符串在 UIKit 框架中 UILabel 代替,采用是的 Core Text 直接进行绘制。但是在使用过程中会发现在设置 居中显示默认是 x 方向居中 y 轴显示距离 top 但是可以使用 NSMutableAttributeString 来进行设置 |
CAShaperLayer |
CAShaperLayer 通过矢量图形来进行绘制在使用绘制时不用像使用 CALayer 生成相应的寄宿图片不消耗内存,可以结合 Path 来进行各种内容的绘制工作。使用最广的场景就是我们在 APP 实现画笔或者是内容的绘制工作。 |
CAGradientLayer | 一个使用硬件加速可以绘制出很炫的 API 接口可以实现多种色彩平滑的过渡,多种颜色的间隔。如果作为 mask 可以做出在一个 UILabel 或者是 CATextLayer 上面做出平滑过的颜色效果。 |
CAEmitterLayer | 是一个基于核心动画粒子发射的系统。这里可以控制粒子生成以及粒子的开始位置实现很好粒子展示效果。 |
CAScrollLayer | 管理着多个子层的使用,类似于在 UIKit 中可滑动的控件 UIScrollView 。可以说是在没有对点击相应的情况下 UIScrollView 很好的图层替代。 |
CATiledLayer | 对图片实现分割显示,在 UI 界面上我们可以操作和看到的图片都会通过 OpenGL ES 生成纹理最后绘制在屏幕上。当显示的图片过大或者超出需要绘制的显示内容上,会在显示时对图片进行加压生成原生数据显示这时可能会出现卡顿现象。 |
CATransformLayer | 实现 3D 层次展示,多使用在地图类。 |
CAReplicatorLayer | 要自动复制一个或多个子层时使用。replicator为您创建副本,并使用您指定的属性来更改副本的外观或属性。 |
CAMetalLayer | 主要是管理 Metal Texture Pool 渲染 MTL Texture 到显示窗口。 |
Core Animation 动画 - 探索
CAAnimation API | Function |
---|---|
CAAnimation | 作为 QuartzCore 其他动画的父类,实现动画的基本接口。可以通过系统 CAMediaTimingFunction 动画或者二次本塞尔曲线设置动画。 |
CAPropertyAnimation | 作为 CAAnimation 的子类,提供属性设置来实现显示动画。 |
CABasicAnimation | 作为 CAPropertyAnimation 默认执行动画的 View 的 Center 即实际 Layer 层的 Position 中心点位置指定开始位置到终点中心位置的动画。 |
CAKeyframeAnimation | 作为 CAPropertyAnimation 的子类。同样是把 Center 作为默认动画标点,通过 keyTimes 和 timingFunctions 来对动画设置在总动画时间,时间间隔对应的动画执行方式。或者是直接使用 values 然后通过计算时间间隔来分隔动画实现多种动画效果叠加。通过 path 来生成路径动画。 |
CASpringAnimation | 作为 CABasicAnimation 子类。 |
CATransition | 作为 CAAnimation 的子类,实例简单的动画效果。例如:fade ,moveIn ,push 和 reveal 设置进入方向类型。 |
CAAnimationGroup | 作为 CAAnimation 子类。对 CAAnimation 进行栈整合,实现动画实现栈动画组的栈执行。 |
在好久之前就实现根据写过简单的动画实现在动画的介绍中链接实现基础实现类,具体实现项目AnimationDome。
下面👇这里主要针对 CAKeyFrameAnimation
系统缓冲和自定义函数缓冲实现,及在实现缓冲过程中我们通过定义 timingFundations
,values
和 path
三者之间对应的关系。
CAMediaTimingFundation 和 Path 关系
上面我们可以看出采用系统定义的 kCAMediaTimingFunctionEaseIn
,kCAMediaTimingFunctionEaseInEaseOut
和 kCAMediaTimingFunctionDefault
生成对应的效果图。
Gif
视频文件

三者直接对用 CADeiaTimingFundation 对应的 Path 路径绘制
注:黄色是出发点,亮青是终点,蓝色是控制点。

下面我们参考系统的 CAMediaTimingFundation
来自定义缓冲,参考资料

上面自定义缓冲类型对应的代码实现:
easeInQuint = CAMediaTimingFunction(controlPoints: 0.6, 0.04, 0.98, 0.335)
easeInOutQuint = CAMediaTimingFunction(controlPoints: 0.86, 0, 0.07, 1)
easeInOutBack = CAMediaTimingFunction(controlPoints: 0.68, -0.55, 0.265, 1.55)
在上面的博文中小编根据一些博文和 WWDC
介绍。来对 UIKit
中 UIView
和 QuartzCore
中的 Layer
两者之间细节做探讨,来验证 Core Animation
在实际应用中为 UI
控件提供绘制的依据。可以看出在实际绘制的过程中系统会通过 GPU
来计算布局也就是在 iOS UI 优化博文大纲-Core Animation 作用域 Layout
中布局的计算。这个也是 ASDK
作者认为可以优化的地方。
来自定义缓冲函数实现帧动画的效果。
参考资料:
Dynamic Visuals
About Core Animation
Advanced Animation Tricks
Changing a Layer’s Default Behavior
Improving Animation Performance
Layer Style Property Animations
iOS Core Animation: Advanced Techniques
苹果的官方视频:
Layer-Backed Views: AppKit + Core Animation
Advanced Graphics and Animations for iOS Apps
Optimizing 2D Graphics and Animation Performance
Designing with Animation
作者: JackJin Bai
第一次修改时间: 2019/1/6 20:37:26
写于:广州市天河公园家里
第二次修改时间: 2019/1/18 02:02:44
写于:广州市天河公园家里