iOS UI 优化 - Core Animation 实现探索

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

屏幕快照 2019-01-15 23.12.22.png

注:上图分别是从 2014 年的 WWDC 讲述 Advanced Graphics and Animations for iOS Apps 中截取 和 2018 年最新描述 About Core Animation 文档获取Core Animation 在架构中实现。

由上面显示具体信息我们可以得出下面结论:

1、Core Animation 作为主要内容载体承接对 iOSOS 具体显示的绘制;
2、UIKit 框架从初始化到通过 GPU 生成纹理显示在屏幕上是依靠 Core AnimationCore GraphicsOpenGL ES(iOS 11) 和 Matel(iOS 12) 来实现;
3、在 iOS 8.0 中苹果官方尝试使用 Metal 来替代 OpenGL ES 在目前最新的官方文档中实现在绘制生成纹理底层架构依据 Metal 进行。

想要深入了解一个架构我们就从最基本的系统 API 着手,下面是小编对系统 API 根据分类来对具体类的 API 进行整理。

屏幕快照 2019-01-14 22.50.21.png

看过上图我们首先要明白在 Core Animation 不仅仅满足我们 UIKit 移动端 iOS 使用,而且在 OS 中的 AppKit 同样也是对其进行再次封装。但是这里我们仅仅讨论在 iOS 平台具体实现情况。

数据表格可以看出 Core AnimationiOS 架构中负责 Layer 的内容绘制和动画的执行。执行绘制实现的类:CATypeLayerCATypeAnimation 及其在实现动画实现参数例如:Animation GroupAnimation Timing

Core Animation 绘制

细细想来我们在做应用实际上展示给用户内容,而在展示内容基础上我们多数是采用 UIKit 中的继承于 UIView 非线程安全的控件来做内容展示。这里我们从 Core Animation 绘制开始剖析怎么实现?

UIView VS Core Animation

Core AimationAPI 的数据表中我们可以看到 Layer Basics 中有三个类分别是:CALyerCALayerDelegateCAAction。既然 iOS 中界面的初始化均是以父类 UIView来实现,那么 UIView 和三者有什么关系呢?

UIViewCALayer

Layer 层作为 Core Animation 主要绘制基类为 UIKitView 提供内容绘制的容器,动画实现基础。下面通过实际例子来进行解答:

通过实现 LJView 继承 ViewLJLayer 继承 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 layerClass])

//1、调用 View 中给定的 Frame 来创建 Layer
LJCAStructure[39931:4249519] (-[LJLayer setBounds:])

//2、创建 Layer 后通过调用栈创建 View
LJCAStructure[39931:4249519] (-[LJView setFrame:])

//3、View 回调中心点 center 实际委托给 Layer 的 position 返回当前的中心点
LJCAStructure[39931:4249519] (-[LJView center])
LJCAStructure[39931:4249519] (-[LJLayer position])

//4、通过上面 LJView 设置 Frame 和 Center 后,Layer 层再次重新对其 Frame、position 和 bounds 进行设置,
//这里也可以看到 Frame 其实是有 position 和 bounds 来决定
LJCAStructure[39931:4249519] (-[LJLayer setFrame:])
LJCAStructure[39931:4249519] (-[LJLayer setPosition:])
LJCAStructure[39931:4249519] (-[LJLayer setBounds:])

//5、View 在实现布局后,再次获取其 center 再次委托与 Layer position 来获取中心点
LJCAStructure[39931:4249519] (-[LJView center])
LJCAStructure[39931:4249519] (-[LJLayer position])

上面我们可以看出在实现 LJView 在实现初始化的过程时,实际会委托给 Layer 层的 Frame | BoundsPosition 来进行具体实例化的过程。在实例化过程有下面几个步骤:

1、View 通过 Layer 中的 boundsposition 来确定其的 frame
2、然后 Layer 根据上面的值再次对其 framepositionbounds 进行再次设置;
3、最后 View 再次委托 Layer 获取当前的中心点。

下面我们在根据每部具体操作在看下操作具体的调用栈

(-[LJLayer setBounds:])调用栈:

屏幕快照 2019-01-18 00.27.56.png

上面我们可以看出在实际中的调用栈是:

a、[UIView init] 初始化
b、[UIView initWithFrame] 调用 initWithFrame 方法
c、UIViewCommonInitWithFrame 采用 Common frame 初始
d、[UIView _createLayerWithFrame] 根据 frame 创建 Layer

1、UIView 初始化时创建 Layer 然后调用其方法 setBounds:

(-[LJView setFrame:])调用栈:

屏幕快照 2019-01-18 00.28.16.png

上面我们可以看出在实际中的调用栈是:

a、[UIView init] 初始化
b、[UIView initWithFrame] 调用 initWithFrame 方法
c、UIViewCommonInitWithFrame 采用 Common frame 初始

2、UIView 在初始创建 Layer 后对其 Frame 进行初始化调用 setFrame: 方法。

(-[LJView center])调用栈:

屏幕快照 2019-01-18 00.28.30.png

(-[LJLayer position])调用栈:

屏幕快照 2019-01-18 00.28.52.png

上面我们可以看出在实际中的调用栈是:

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 基础上接着调用 ViewCenter 中心点实际是委托与 Layerposition 继续调用

(-[LJLayer setFrame:])调用栈:

屏幕快照 2019-01-18 00.29.11.png

(-[LJLayer setPosition:])调用栈:

屏幕快照 2019-01-18 00.29.27.png

上面我们可以看出在实际中的调用栈是:

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 的基础上通过设置 LayerFrame 实际设置当前的 position 中心点。

(-[LJLayer setBounds:])调用栈:

屏幕快照 2019-01-18 00.29.41.png

上面我们可以看出在实际中的调用栈是:

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 的基础上通过设置 LayerFrame 实际设置当前的 bounds 中心点,也就可以看出 LayerFrame 在设置情况实际是根据 Layerboundsposition 来实际决定其 Frame

(-[LJView center])调用栈:

屏幕快照 2019-01-18 00.29.55.png

(-[LJLayer position])调用栈:

屏幕快照 2019-01-18 00.30.09.png

上面我们可以看出在实际中的调用栈是:

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 中心点委托给 Layerposition 来获取。

上面在初始化 [[LJView alloc] init] 在实际调用过程中我们可以看到首先是通过 [UIView _createLayerWithFrame] 创建 Layer 然后根据 Layer 的属性 PoitionBounds 决定 UIViewFrame 确定含有 UIView 在视图控制器或者是父 UIVIew 的布局。

接下来我们对初始化后实例 _ljView 对其 Frame 赋值:

_ljView.frame = CGRectMake(0, 0, LJScreenWidth(), 200); 打印的日志如下:

LJCAStructure[39931:4249519] (-[LJView setFrame:])
LJCAStructure[39931:4249519] (-[LJView center])
LJCAStructure[39931:4249519] (-[LJLayer position])
LJCAStructure[39931:4249519] (-[LJLayer setFrame:])
LJCAStructure[39931:4249519] (-[LJLayer setPosition:])
LJCAStructure[39931:4249519] (-[LJLayer setBounds:])
LJCAStructure[39931:4249519] (-[LJView center])
LJCAStructure[39931:4249519] (-[LJLayer position])

在打印的具体调用栈我们可以看出再具体赋值的过程中实际的调用方法就是我们在进行 [[LJView alloc] init] 创建其 Layer 后调用方式:
1、UIView 委托其包含的 Layer 获取中心点来做判断;
2、通过 Layer 来通过 BoundsPosition 来确定其 Frame;
3、再次获取 UIView 委托其包含的 Layer 获取中心点来做判断当前中心点。

UIViewCALayerDelegateCAAction

我们在使用 UIView 布局中不少有些动画效果,在 iOS 里给出非常简洁的动画 API

下面我们给出一个最简单的例子:

[UIView animateWithDuration:2 animations:^{
    self.view.center = CGPointMake(LJScreenWidth()/2, 300);
}];

在上述的运行之前我们在 LJView 重写下面方法:

1、 CALayerDelegate 方法 - (id)actionForLayer:forKey:
2、LJLayer 中方法 - (void)addAnimation: forKey: 在其中实现 CAAnimationDelegate 委托给 LJLayer 然后重写 - (void)animationDidStart:- (void)animationDidStop: finished:

运行上面的代码获取下面的打印日志:

//设置当前的 center 中心点 | 这里也可以看出 UIView 实际是由 Layer 来进行设置
LJCAStructure[2471:50545] (-[LJView setCenter:])
LJCAStructure[2471:50545] (-[LJLayer position])
LJCAStructure[2471:50545] (-[LJLayer setPosition:])

//通过实现 CALayerDelegate 委托 | 返回当前的 CAAction 的协议给 Layer 
//CAAction 
//- (void)runActionForKey:event object:anObject arguments:dict;
LJCAStructure[2471:50545] (-[LJView actionForLayer:forKey:])

//判断 Layer 层 position 是否存在 | 获取当前 position 
LJCAStructure[2471:50545] (-[LJLayer position])
LJCAStructure[2471:50545] (-[LJLayer position])

//Layer 获取动画添加
LJCAStructure[2471:50545] anim : 
LJCAStructure[2471:50545] (-[LJLayer addAnimation:forKey:])

//再次通过 View 来获取 actionForLayer:forKey: 判断是否动画再次天机
LJCAStructure[2471:50545] (-[LJView actionForLayer:forKey:])
LJCAStructure[2471:50545] (-[LJLayer position])
LJCAStructure[2471:50545] anim : (null)
LJCAStructure[2471:50545] (-[LJLayer addAnimation:forKey:])

//执行动画
LJCAStructure[2471:50545] (-[LJLayer animationDidStart:])

//动画结束
LJCAStructure[2471:50545] (-[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: 

//在当前的 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"];
        }
    }
}

通过上面在简单的通过 UIViewAPI 实现动画,我们可以看出当前在执行动画的过程中具体实现流程如下:

1、我们通过当前的 UIView 在对其属性进行赋值时,UIView 委托给 Layer 进行属性设置;
2、UIView 通过实现 CALayerDelegate 委托方法,通过 iOS 系统来查找对应的 CAAction 来传递给 CALayer
3、Layer 层获取在 UIViewCAAimation 数据,添加到当前执行动画中;
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 默认执行动画的 ViewCenter 即实际 Layer 层的 Position 中心点位置指定开始位置到终点中心位置的动画。
CAKeyframeAnimation 作为 CAPropertyAnimation 的子类。同样是把 Center 作为默认动画标点,通过 keyTimestimingFunctions 来对动画设置在总动画时间,时间间隔对应的动画执行方式。或者是直接使用 values 然后通过计算时间间隔来分隔动画实现多种动画效果叠加。通过 path 来生成路径动画。
CASpringAnimation 作为 CABasicAnimation 子类。
CATransition 作为 CAAnimation 的子类,实例简单的动画效果。例如:fademoveInpushreveal 设置进入方向类型。
CAAnimationGroup 作为 CAAnimation 子类。对 CAAnimation 进行栈整合,实现动画实现栈动画组的栈执行。

在好久之前就实现根据写过简单的动画实现在动画的介绍中链接实现基础实现类,具体实现项目AnimationDome。

下面这里主要针对 CAKeyFrameAnimation 系统缓冲和自定义函数缓冲实现,及在实现缓冲过程中我们通过定义 timingFundationsvaluespath 三者之间对应的关系。

CAMediaTimingFundation 和 Path 关系

上面我们可以看出采用系统定义的 kCAMediaTimingFunctionEaseInkCAMediaTimingFunctionEaseInEaseOutkCAMediaTimingFunctionDefault 生成对应的效果图。

Gif 视频文件

CAMediaTimingFuncation-System-Interface.gif

三者直接对用 CADeiaTimingFundation 对应的 Path 路径绘制

注:黄色是出发点,亮青是终点,蓝色是控制点。

屏幕快照 2019-01-27 16.59.10.png

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

屏幕快照 2019-01-27 17.22.59.png

上面自定义缓冲类型对应的代码实现:

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 介绍。来对 UIKitUIViewQuartzCore 中的 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
写于:广州市天河公园家里

你可能感兴趣的:(iOS UI 优化 - Core Animation 实现探索)