• Core Animation学习总结


    文件夹:
    1. The Layer Beneath
      1. The Layer Tree(图层树)
      2. The Backing Image(寄宿层)
      3. Layer Geometry(图层几何学)
      4. Visual Effects(视觉效果)
      5. Transforms(变换)
      6. Specialized Layers(专有图层)
    2. Setting Things in Motion
      1. Implicit Animations(隐式动画)
      2. Explicit Animations(显式动画)
      3. Layer Time(图层时间)
      4. Easing(缓冲)
      5. Timer-Based Animation(基于定时器的动画)
    3. The Performance of a Lifetime
      1. Tuning for Speed(性能调优)
      2. Efficient Drawing(高效画图)
      3. Image IO(图像IO)
      4. Layer Performance(图层性能)



    Core Animation 之 CALayer
    The Layer Tree(图层树)

    1、图层与视图的差别:
    • 视图是以树的数据结构来管理层级关系的。而图层相同也是以树的数据结构来管理层级关系
    • 视图在程序中以UIView及其子类来表示。而图层以CALayer及其子类专用 图层来表示
    • UIView总是与CALayer是一一相应的关系,所以本质上,iOS上界面上的内容的呈现与动态实际上是通过CALayer来实现的,而UIView是封装了CALayer的基础上加入了事件响应、布局等高级功能。


    2、关于视图层级、图层树、呈现树、渲染树
    • 视图层级:主要负责实现事件响应、布局等功能,由于视图封装图层。图层向视图暴露部分编程接口与属性,于是通过改动视图的效果能够间接改动图层的属性。

    • 图层树:负责定义界面图形的绘制、动画效果,iOS是有一个绘制周期的——60FPS,也就是说图层树负责保存这个周期内的相关属性的改动,而到了周期结束要进行绘制的时候,才把图层树的数据模型更新到呈现树中。
    • 呈现树:确定当前屏幕上界面图形的详细效果。用于保存当前屏幕的图形的显示属性,每隔一个绘制周期和图层树同步一次。
    • 渲染树:iOS系统会专门创建一个进程来运行图形渲染的任务,当随意APP(包含系统App)须要图形渲染的时间,它们就会把渲染任务发送到该线程去运行渲染。

    所以以上四层。每一层都负责不同的任务,其数据流是视图-》图层-》呈现树-》渲染树。

    而且绘制周期60FPS也是会随着系统的状态而变化的,若系统资源超载,而会通过降低绘制次数来确保系统能稳定执行。


    3、视图动画
    视图动画是显式,由于UIView默认把CALayer的隐式动画禁止掉了。所以要通过动画Block的形式才干取消动画的限制。(动画block实际上就是曾经的动画栈)。
    而且视图能实现的动画仅仅是图层所开放的一部分,都是2D动画,所以视图动画的效果会稍稍简单。

    4、图层动画
    图层动画是隐性动画。仅仅要改动图层的属性。其变化都会以默认的渐变形式呈现出来(详细原理见《隐式动画》)。而且图层动画支持很多其它视图动画无法实现的效果。如:
    • 阴影、圆角、带颜色的边框
    • 3D变化
    • 非矩形范围
    • 透明遮罩
    • 多级非线性动画。

    5、更适宜使用CAlayer呈现内容的场景
    由于UIView与CALayer都能够呈现内容。尽管CALayer不能直接实现事件响应。但开发人员也能够通过hit-test机制的原理来自己实现事件响应,那什么场景更加适合用CALayer而不是UIView呢?例如以下所看到的:
    • 开发同一时候能够在MAC、iOS上执行的跨平台应用
    • 使用多种CALayer的子类(专有图层),而且不想创建额外的UIView去封装
    • 做一些对性能特别挑剔的工作,如对UIView一些可忽略不计的操作都会引起显著的不同(也能够通过OpenGL来解决)。


    The Backing Image(寄宿层)

    1、寄宿层&Contents属性
    CALayer有一个名曰Contents的属性,这个属性是与寄宿层相相应了,而contents属性指向的对象必须为CGImageRef类型(一个指向CGImage结构的指针),所以寄宿层是用来展示图片所用的。例如以下情况会调用寄宿层:
    • 显示图片
    • core Graphics
    core Graphics能够实现自己定义绘制,但通常不建议那么做,由于core Graphics绘制会默认生成一个绘制专用的存储空间,而这空间有十多M那么多。会占用大量内存,所以Apple不建议实现空的core Graphics更不建议在core Graphics实现不属于该方法的代码。

    2、contentGravity属性
    与UIView的contentMode属性相相应,用于调整图片的布局,支持一下常量值:
    • kCAGravityCenter
    • kCAGravityTop
    • kCAGravityBottom
    • kCAGravityLeft
    • kCAGravityRight
    • kCAGravityTopLeft
    • kCAGravityTopRight
    • kCAGravityBottomLeft
    • kCAGravityBottomRight
    • kCAGravityResize
    • kCAGravityResizeAspect
    • kCAGravityResizeAspectFill

    3、contentsScale
    contentsScale属性定义了寄宿层的像素尺寸和视图大小比例,默认是1.0(一个点一个像素),在retina屏幕得设置在2.0(一个点两个像素)。在plus设备上得设备为3.0(一个点3个像素)。
    但若contentGravity设置了如kCAGravityResizeAspectFill自己主动适配大小的属性后,contentsScale会不起作用。所以不能依靠contentsScale来做缩放的操作,缩放还是得交给transform或者affineTransform。


    3、maskToBounds
    该属性与UIView的clipsToBounds属性相相应。都是应用于将超出图层边界的子图层的内容进行裁剪。

    这里须要一点须要注意的,但我们实现radiusCorner时,实际上就是通过设置背景颜色来实现的。而与maskToBounds结合才是真正把边角内容裁剪掉,但radiusCorner+maskToBounds结合使用会引发离屏渲染。所以在radiusCorner满足要求。就不要再调用maskToBounds。

    4、contentsRect
    CALayer的contentsRect同意我们在图层边框内显示寄宿图的一个子域,而contentsRect是一个相对值属性,如{0,0。1,1}表示整个寄宿图,这样的坐标系统也叫单位。例如以下简述iOS下的三种坐标系统单位:
    • 点:点是虚拟的像素,也叫逻辑像素。在不同的设备上一个点所代表的像素点是不一样的。如普通屏幕一个点就是一个像素,而retina屏幕一个点就是两个像素。
    • 像素:实际的物理像素坐标。
    • 单位:一种相对的坐标,优点就是应用相对值,方便移植。
    contentsRect有一种经典的使用方法——image sprites(图片拼接,经常使用于游戏),这种使用方法就是用来实现一次性载入多张图片的。由于每一张图片的使用前都要经过,载入——》压缩——》呈现,的一个过程当中载入和压缩是十分耗时的,载入消耗IO,压缩算法的复杂会添加CPU的消耗,所以想游戏这种App须要载入解压大量的图片时,能够把全部图片整合成一张图片来载入解压,然后通过contentsRect裁剪出子图,并显示,能够优化载入解压的过程。

    5、contentsCenter
    contentsCenter的名字事实上有一点的误导性,事实上际上与UIView的Stretching属性相相应。用来实现寄宿层的缩放时的呈现效果。也是一个相对值单位。


    6、customs drwaing
    除了通过CGImage设置到contents的方式来实现寄宿层。还能够通过Core Graphics来直接绘制寄宿层,但这样的方式十分消耗内存不建议使用。
    CALayer有一个可选的Delegate属性。实现了CALayerDelegate,但CALayer须要又一次绘制,则调用它的displayLayer方法(与UIView的setNeedDispaly相相应),就会调用其代理方法drawLayer:inContext(与UIView的drawRect相相应)。

    若在视图层中,仅仅要用户实现了drawRect那么仅仅要屏幕须要又一次绘制那么该方法都会被默认调用。


    Layer Geometry(图层几何学)

    1、布局
    UIView的三个重要布局属性:frame、bounds、center
    CALayer的三个重要的布局属性:frame、bounds、position
    能够看出UIView与CALayer的布局属性是一一相应的,而唯一有啥区别的就是锚点的命名,UIView为center,而CALayer为position,但锚点最開始都是默觉得图形的中心。
    当中frame实际上就是一个虚拟属性,其由bounds与center/position计算所得。所以改动bounds与center/position也会影响到frame的数值。
    还有不管是视图还是图层。在屏幕上不管怎么变形,本质上其还是一个矩形。但在旋转的情况下。frame的数值是会事实变动的,例如以下图:



    2、锚点
    锚点属性是用来指定图层相对于父图层的位置。也能够理解为利用图层的句柄。默觉得图层的中心但也能够改动。在图层中改动了锚点会产生移动(隐性动画),相同的锚点也是一个相对值。
    例如以下。为锚点的实验效果图:

    图一:时分秒针直接以图层的中点为锚点,由于旋转时也以其为中心,效果例如以下:


    图二:将锚点改动成指针的尾端


    图三:改动锚点后的旋转效果。


    3、坐标系
    由于每一个图层都有自己的坐标系,所以CALayer也提供了一系列的方法用于一个点在不同的坐标系之间转换,这些方法特别适合于子图层与父图层之间的坐标转换。




    事实上UIView与CALayer都有zPosition。但因为UIView只支持2D变换,所以UIView的zPosition只适合于用来作图层的调整,但更加建议用UIView提供的视图数组管理方法来调整视图的绘制顺序来调整。而CALayer的zPosition是一个重要的三维坐标系。

    4、Hit Testing
    Hit Testing是iOS中一个十分重要的机制,用于检索点击了的目标图标,与响应链条相互搭配的话。就会将终于目标图标设置为第一响应者。Hit Testing提供了两个核心的方法:
    • containPoint:(用于直接推断某个点是否属于某个图层)
    • -hitTest(通过递归遍历子对象的方式来直接返回目标图层)

    5、自己主动布局
    对于UIView。通过UIView暴露出来的UIViewAutoresizingMask和NSLayoutConstraint来实现自己主动布局。

    但对于CALayer,则须要手动来操作实现,当中最为简单的方法就是实现CALayerDelegate的例如以下函数:
    - (void)layoutSublayersOfLayer:(CALayer *)layer;
    在该方法内依据当前layer的属性来计算调整来实现自己主动布局。

    由于CALayer实现自己主动布局不方便,所以这也是为什么更加建议使用UIView来实现界面的构建。

    Visual Effects(视觉效果)——本节探讨可以通过CALayer实现的附加视觉效果

    1、圆角
    CALayer有一个叫做cornerRadius的属性控制图层角的曲率,这个曲率仅仅影响背景颜色而不影响图片或者子图片。圆角的计算原理就是一个以CALayer的中点,曲率所设的半径的原与矩形的交集就是目标图形。
    另一个名曰maskToBounds属性,就是实现将图层里面边界外的图形所有分割丢弃。
    所以通过cornerRadius+maskToBounds组合能够实现圆角,但同一时候会引发离屏渲染,若是小量的离屏渲染还好,但如滚动栏的场景,开会有大量的离屏渲染的任务产生,就会严重影响性能,这一点也是注意优化的。详细效果例如以下图所看到的:




    2、图层边框
    通过设置CALayer的borderWidth与borderColor两个属性能够改动边框的效果。

    效果例如以下:





    3、阴影
    阴影是一种能达到图层深度暗示效果的装饰。

    CALayer提供下面參数来定制阴影的效果:

    • shadowOpacity:设置一个大于零的数值,那么阴影就会显示在图层以下(默觉得0)。
    • shadowColor:设置阴影的颜色(默认黑色)
    • shadowOffset:设置阴影的偏移量(默认{0。-3})
    • shadowRadius:设置阴影模糊度,数值越大。阴影的模糊度越高(默觉得0)。曲率越大,阴影效果越明显。
    shadowRadius的设置效果图例如以下:


    有一点须要注意的,就是阴影的绘制是属于离屏绘制。是比較效果资源的。所以一定要降低同一屏幕进行大量阴影绘制的情况。


    4、阴影裁剪
    图层的阴影很多其它是继承于内容的外形,而不是依据边界和角半径来确定。为了计算出阴影的形状。core animation会将寄宿层也考虑在内。

    但若直接maskToBounds来裁剪,会把阴影效果也裁减掉。

    为了解决以上问题。能够通过专门引入一个阴影图层来解决,详细效果例如以下:

    图一:直接maskToBounds裁剪会把阴影效果一并裁剪掉。


    图二:通过加入一个另外的阴影图层在以下,然它来实现阴影。而详细的内容图层就直接maskToBounds裁剪。


    图三:终于既能实现裁剪,又能实现阴影。


    PS:不管是阴影还是圆角裁剪都会引发离屏渲染,大量的该类型的图形须要渲染的话。会减低帧率。


    5、shadowPath 属性
    阴影并一定是方形的。我们也能够通过shadowPath来定制自己想要的阴影形状。详细效果例如以下所看到的:




    6、图层蒙版
    通过masksToBounds属性。我们能够实现边界裁剪,但我们还须要更加灵活的裁剪需求的时候就能够通过图层蒙版来实现。CALayer提供一个mask的属性来解决一个问题。而mask本来就是指向还有一个图层的指针。其详细原理例如以下图所看到的:




    从上图效果能够知道mask舒心仅仅关心形状的交集,而颜色还是由原来的图形的颜色决定。

    另一点须要注意的是图层蒙版也会引发离屏渲染,会带来性能问题,所以使用的时候也得多加注意。


    7、拉伸过滤
    当我们视图显示一个图片的时候。都应该以正确的比例,正确的1:1像素显示在屏幕上。原因例如以下:
    • 可以显示更好的画质,像素既没有被压缩也没有被拉伸
    • 能更好使用内存,由于这就是你要存储的东西
    • 最好的性能表现,CPU不须要为此额外的计算 
    但有时候我们就须要缩略图。所专门另外存储缩略图这对内存来说。

    这时就能够通过CALayer的minificationFilter(缩小滤波器)和magnificationFilter(方法滤波器)属性实现图形的缩放算法。当中提供下面三种默认的缩放算法:

    • kCAFilterLinear(双线性滤波算法,默认算法)
    • kCAFilterTrilinear(三线性滤波算法)
    • kCAFilterNearest(近期滤波算法)
    对于大图的缩放,採用kCAFilterLinear、kCAFilterTrilinear,效果会比較好。

    对于小图、较少斜线的图的缩放,。採用kCAFilterNearest效果比較好。

    图一:採用kCAFilterLinear的方法滤波器算法的效果。


    图二:採用kCAFilterNearest的放大滤波器算法的效果:




    8、透明组
    UIView有一个alpha属性类确定视图的透明度。而CALayer有一个与之相应的属性——opacity。这两个属性都会影响到子图层。例如以下图所看到的:
    当我们将view2的alpha数值设置为0.5,这时候,对于label所处的点的颜色计算公式为:50%label + 25%view + 25%background,所以就是灰色偏白的颜色。
    若想整个图层的色调保持一致,能够通过将shouldRasterize属性设置为YES,那么图层及其子图层将被整合成一个总体的图片。效果例如以下:

    图一:shouldRasterize属性设置为默认值NO的效果。


    图二:shouldRasterize属性设置为默认值YES的效果。




    Transforms(变换)

    1、仿射变换
    UIView相应的transfrom属性是一个CGAffineTransfrom类型。用于实现二维空间旋转、平移、缩放、斜切。
    而CALayer的transform属性是一个CATransforms3D。能应用3维空间进行旋转、平移、缩放、斜切等效果。
    当中,仿射变化的数学原理例如以下:


    只是iOS为了运算的方便。已经提供了三个标准方法分别用于实现旋转、缩放。平移,而斜切得靠原生运算来实现。


    当中有一点须要注意的,就是旋转的angle是以弧度制为单位的。

    iOS系统提供下面方法来实现混合变换,能同一下面的组合函数实现复杂的变形模式:

    图一:创建一个空的变换结构体。




    图二:不断叠加变换结构体


    图三:直接将两个变换结构体合并


    剪切变换的效果与事实上现代码:




    2、3D变化
    3D变化与2D变化是十分相似的,不同的仅仅是3D变换是3个维度的,加入了Z轴。其数学原理例如以下:


    这样的矩阵运算还是挺麻烦的。所以iOS系统提供给我们便捷的方法:


    相同也会有变换叠加的方法,当中旋转的时候一定要注意当前的旋转的參考轴,顺时针旋转为正值,逆时针旋转为负值。




    3、透视投影
    在真实的世界中,当物体远离我们。因为视角的原因,其会变小。所以为了让3D效果更佳真实,须要引入投影变换,尽管core animation并没有提供给我们,但十分简单,其数学实现例如以下:


    仅仅要将m34 = -1/d, d= 500~1000,(d越小越失真,d越大透视效果越弱)。


    图3.1:没实现透视投影的旋转效果。



    图3.2:实现了透视投影的旋转效果。



    4、灭点
    在人的视觉中。当物体离人越远则会变得越小,当接近无穷远的时候就会汇聚成一个不可见的点,这个点就叫灭点。现实生活中,这个点通常就是中心点,所以在程序中我们也要让图形的灭点集中在屏幕的中点,例如以下所看到的:


    而实现方式则是。首先加入图形的时候先以屏幕中点为锚点加入,然后通过transfrom来讲图层移动到恰当的位置。


    5、sublayerTransform
    sublayerTransform是一种将全部对应的配置统一设置到该图层的全部子图层的便捷属性。

    6、3D坐标下的图层背面
    在3D坐标下,图层的背面也是会绘制的,并且是正面的镜像。
    我们能够通过CALayer的doubleSided属性来决定是否运行双面绘制。


    7、扁平化图层
    假设父图层的坐标发生变换,那么子图层也会随着父图层而进行相同的变换,如7.1图所看到的。但有时我们仅仅想变换父图层并不想变换子图层。这时就得做相对变换来抵消掉父图层的变换。让视觉效果看起来子图层是精巧的。例如以下图所看到的:
    图7.1:2D相对旋转理论图


    图7.2:2D相对旋转效果图


    图7.3:3D相对旋转理论图


    图7.4:3D相对旋转实际效果图


    我们能够看出,在2D坐标系下。相对变换是合理的。但在3D坐标系下,相对变换的结果并不如预想的一样,为什么呢?
    这是由于core Animation图层尽管存在于3D空间之内,但并非每个图层都存在于同一个3D空间之间。

    而用户是以当前window所在的3D坐标系为參考的,这时7.4图的3D坐标系与window的已经不重合的,所以在我们的视觉里面,就是看不到预想的效果,这就是图层扁平化。

    图层扁平化让core animation创建复杂的3D场景变得十分困难,由于非常难使用图层树去创建一个3D结构的层级关系——将全部的场景下的3D都保持同样的3D坐标系。
    只是CALayer提供一个专用图层——CATransformLayer来解决问题,全部加入到该图层内的3D图形都共享一个标准坐标系。



    8、3D场景下的光亮和阴影
    略。临时不懂。

    9、3D场景下的点击事件
    在处理点击事件,一定要注意点击响应是由UIView中的图层结构来决定的,而不是CALayer在屏幕上的呈现效果,所以得注意事件拦截的顺序。
    能够通过设置userInteractionEnabled以及简单的视图覆盖来实现点击事件正确的传递。


    Specialized Layers(专有图层)

    1、CAShapeLayer


    2、CATextLayer


    3、CATransformLayer


    4、CAGradientLayer


    5、CAReplicatorLayer


    6、CAScrollLayer


    7、CATiledLayer


    8、CAEmitterLayer


    9、CAEAGLlayer


    10、AVPlayerLayer


    Core Animation 之 动画
    Implicit Animations(隐式动画)

    1、隐式动画&事务
    Core Animation基于一个如果,就是屏幕上的全部图形都能够做动画,并且这样的动画不须要开发人员去设置,会自己主动实现。这也是为什么直接改动CALayer的属性会以一种渐变的形式来更新到一个新的值。
    这样的仅仅须要改动图层的数值就会引发动画的形式来更新到新数值的动画称为隐式动画。
    而这样的定义了动画的详细呈现效果的机制,就称为事务。
    因为CALayer是默认开启了隐式动画的。而若我们在CALayer上调用显式动画的话,就考虑到隐式动画对效果的影响,进而决定是否须要取消隐式动画。

    2、动画Block
    UIView封装了CALayer。但UIView禁止了隐性动画,所以开发人员须要在UIView上实现显性动画。

    在iOS4.0之前,UIView採用动画栈来管理显示。后来提供了动画Block这个更加便捷的方式。但原理都是一样的。


    3、图层行为
    我们把改变CALayer属性时自己主动应用的动态称为行为。一组行为的运行步骤例如以下所看到的:
    1. CALayer调用-actionForKey:方法,传递属性名称。
    2. 检查CALayer是否持有实现了CALayerDelegate的代理对象(须要实现actionForLayer:forKey方法),若有直接调用并返回结果,若无继续下面步骤。
    3. 检查包括属性名称相应行为映射的actions字典。若有则返回,无则继续
    4. 在style字典接着搜索属性名。若有这返回,无则继续
    5. 调用属性的标准行为defaultActionForKey方法
    经过以上的整个流程,要么actionForKey返回nil则无动画效果,要么就是返回CAAction,然后CALayer利用它实现动画。
    因此能够推动出UIView是怎样禁止隐式动画的,就是讲CALayer的delegate设置为自身。然后在actionForLayer:forKey方法中返回nil。

    • 禁止隐式动画的两个方法:
      • 依据图层行为的原理,实现代理方法actionForLayer:forKey强制返回nil。
      • 通过设置[CATransaction setDisableActions:YES]来实现禁止隐式动画

    4、呈现与模型
    在iOS中。屏幕每秒钟又一次刷新屏幕60次。

    所以Core Animaiton就须要在设置一次新值和新值生效之间,对屏幕上的图层进行又一次组织。

    正是以为上面的机制的存在。所以才会有iOS才会有图层以及呈现层之间的关系,图层更像是model,而呈现层就是view,Core Animation就是Controller,所以在一个绘制周期内,图层负责收集与保存用户对属性的改动,到绘制时刻时。就将图层的数据覆盖到呈现树。

    我们能够通过CALayer的 -presentationLayer方法来获取正在屏幕上显示的呈现树的数据,尽管一般不须要。但在下面情况下会比較有作用
    • 做基于定时器的动画时。而不不过基于事务的动画。这时准备地知道当前时刻的图层的显示位置是十分实用的
    • 假设你想做图层的响应输入,能够通过-hitTest方法来推断制定图层是否被触摸。这时候对呈现图层调用-hitTest会更加有效,由于呈现树就是当前屏幕上的图形实际的位置。


    Explicit Animations(显式动画)

    1、Core Animation的类图架构


    • CAAnimation实现了CAMediaTiming协议。本身并没有做什么工作。主要实现动画的时间相关的属性以及计算函数(缓冲)
    • CATrasition描写叙述变换的对象
    • CAAnimationGroup封装属性动画的队列。
    • CABasicAnimation基础属性动画
    • CAKeyFrameAnimation关键帧动画

    2、基础属性动画
    基础动画主要由CABasicAnimation实现,由上图可知道。而CABasicAnimation继承于CAPropertyAnimation属性动画,通过设置下面三个数值以及其它的选项,就可以实现自己主动动画:
    • fromValue
    • toValue
    • byValue
    当中toValue与byValue不能同一时候使用,toVaule是绝对值,byValue是相对值。

    属性动画都是针对关键帧的动画。也就是说仅仅关心出发点与结束点,中间的过渡能够自己生成也能够默认。


    3、关键帧动画
    CAKeyFrameAnimation用于实现关键帧动画,能通过设置其animations数组来定义每一个关键帧的位置,也能够直接通过path属性来定义运动的路径。


    4、affineTransform属性
    若我们让一个图形沿着曲线运动。能够通过设置affineTransform。让其能沿着曲线的切线运动从而让运动更加真实。前后效果图例如以下图所看到的:



    5、虚拟属性
    属性动画实际上是针对关键路径而不是一个键的。这就意味着能够对子属性甚至虚拟属性做动画。

    如若我们想实现旋转的效果,本来我们须要在keyPath上设置“transform”,
    若使用虚拟属性。能够直接在keyPath上直接设置“transfrom.rotation”,这样在设置formValue与toValue时就能够直接设置弧度叫的值。
    事实上transfrom.rotation这个属性是并不存在的,而是core aniamiton自己主动依据CAValueFunction来又一次计算transform的数值所得,正由于如此才称之为虚拟属性。


    6、动画组
    CAAnimationGroup是一种组合动画的解决方式。通过CAAnimationGroup加入沿曲线运动与颜色变化的动画。



    7、过渡
    略。


    8、在动画的过程中取消动画
    Core Animation提供了一部分的方法来实现动画加入以及移除。例如以下图所看到的:
    - (CAAnimation *)animationForKey:(NSString *)key;  //实现动画的加入,当中key不仅能够用来訪问动画,还能够用来移除动画
    - (void)removeAnimationForKey:(NSString *)key;  //移除Key指定的动画
    - (void)removeAllAnimations;  //移动CALayer已经加入的全部动画
    Layer Time(图层时间)

    1、CAMediaTiming协议
    CAMediaTiming协议定义了在一段动画内控制逝去时间的属性集合。

    CALayer和CAAnimaition都实现了该协议,所以时间能够被随意图层或者一段动画的类所控制。


    CAMediaTiming协议下定义的一些核心属性:
    • duration:定义一段动画的持续时间
    • repeatCount:定义一段动画的反复次数
    • repeatDuration:制定动画反复一段制定的时间
    • autoreverses:制定在每次间隔交替循环的过程中自己主动回放
    能够通过将repeatCount或者repeatDuration设置为INFINITY来实现动画无限循环播放。但不能同一时候使用这两个属性。


    2、相对时间
    在Core Animation中,时间是相对的,每一个动画都有自己的描写叙述时间,能够独立的加速、延迟或者偏移,当中有下面关键属性:
    • beginTime:动画的開始时间。但不是绝对时间,而是一个相对时间,就是从该动画加入到可见层的那一刻開始測量,默认是零。
    • speed:时间倍速,默认是1.0,若设置为2.0而二倍速前进。那么对于一个duration为1.0的动画,实际上0.5s就已经完毕。
    • timeOffset:让动画直接快进到某个时间点进行,使用它要注意是否有speed的參与而改变的duration
    • duration:动画的播放时间。

    3、fillMode
    当一个图层产生动画,实际上就是呈现层在运行动画。那么。当呈现层运行完动画是否要讲当前属性的值覆盖会图层。这样的行为就称为fill mode,这个由开发人员决定:
    • kCAFillModeForwards:(保持结束后的值)
    • kCAFillModebackwards:(保持结束前的值)
    • kCAFillModeBoth:(包含以上两种情况)
    • kCAFillModeRemoved:(默认。当不在播放动画时,则显示回图层的属性值)


    4、全局时间与本地时间
    CALayer或者CAGroupAnimation调整durationrepeatCount/repeatDuration属性并不会影响到子动画。可是beginTimetimeOffsetspeed属性将会影响到子动画。
    每一个CALayerCAAnimation实例都有自己本地时间的概念。是依据父图层/动画层级关系中的beginTimetimeOffsetspeed属性计算。

    CoreAnimation有一个全局时间的概念。也就是所谓的马赫时间(“马赫”实际上是iOS和Mac OS系统内核的命名)。
    CACurrentMediaTime函数来訪问马赫时间:
    CFTimeInterval time = CACurrentMediaTime();
    该值返回的是一个相对值,与现实的时间无关。但能够通过它来比对不同动画之间的时间差,iOS提供下面方法来进行不同图层之间本地时间的转化:
    - (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l; 
    - (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
    通过以上知识能够实现同步不同图层的speed,timeOffset、beginTime的动画。


    5、暂停、倒回和快进的实现
    设置动画的speed属性为0能够暂停动画。但不能再加入后再改动。否则会报错。
    但我们能够通过下面的设置来实现一些调试:
    self.window.layer.speed = 100;

    6、手动动画
    将speed设置为0让动画停止播放。然后改动timeOffset来切换不时间的动画序列。从而实现手动切花动画的效果。

    Easing(缓冲)——让动画更加平滑自然

    1、CALayer、Animation的缓冲动画与CAMediaTimingFucntion
    首先须要设置CAAnimationtimingFunction属性,是CAMediaTimingFunction类的一个对象。假设想改变隐式动画的计时函数,相同也能够使用CATransaction+setAnimationTimingFunction:方法。来实现缓冲函数。当中有下面默认的缓冲函数:
    kCAMediaTimingFunctionLinear            //线性
    kCAMediaTimingFunctionEaseIn            //缓进
    kCAMediaTimingFunctionEaseOut           //缓出
    kCAMediaTimingFunctionEaseInEaseOut     //缓进缓出
    kCAMediaTimingFunctionDefault           //默认的效果与kCAMediaTimingFunctionEaseInEaseOut类似,但效果更佳不明显

    2、UIView的缓冲动画
    通过设置UIView的options參数加入例如以下常量也能够实现缓冲动画的效果:
    UIViewAnimationOptionCurveEaseInOut 
    UIViewAnimationOptionCurveEaseIn 
    UIViewAnimationOptionCurveEaseOut 
    UIViewAnimationOptionCurveLinear


    3、缓冲和关键帧动画
    CAKeyframeAnimation有一个NSArray类型的timingFunctions属性。我们能够用它来对每次动画的步骤指定不同的计时函数。可是指定函数的个数一定要等于keyframes数组的元素个数减一,由于它是描写叙述每一帧之间动画速度的函数。


    4、自己定义缓冲函数
    临时不讨论。


    Timer-Based Animation(基于定时器的动画)——我们能够通过事务来实现动画。设置关键帧,让中间的过渡自己主动计算得出。也能够基于定时器来设置每个绘制周期的变换来实现动画。


    iOS提供一下两种形式来实现基于定时器的动画:
    • NSTimer  :并非依据实际帧率来运行,所以CADispaly是更好的解决方式
    • CADispaly  :依据实际的帧率来运行。更加适合

    但不管是NSTimer还是CADispaly本质上都是基于runloop,NSTImer把每一个时间触发的事件注冊到runloop里面。以待制定。

    而CADisplay则把任务直接嵌套进绘制操作之前。



    Core Animation 之 性能优化
    Tuning for Speed(性能调优)

    1、CPU &  GPU
    CPU(中央处理器)和GPU(图形处理器)都是能用来处理。

    总的来说。我们能够用软件(使用CPU)做不论什么事情,可是对于图像处理,通经常使用硬件会更快,由于GPU使用图像对高度并行浮点运算做了优化。

    由于某些原因,我们想尽可能把屏幕渲染的工作交给硬件去处理。问题在于GPU并没有无限制处理性能。并且一旦资源用完的话,性能就会開始下降了(即使CPU并没有全然占用)。

    性能优化的本质就会合理地利用CPU与GPU,使他们不会超出负荷。


    Core Animation处于iOS的核心地位,不管是应用内还是应用外都会用到它。所以iOS特别设计了一个进程来运行渲染相关的任务,也叫渲染服务。渲染服务管理动画和屏幕上组合的图层。
    当执行一段动画的时候。这个过程会被切分为六个阶段,包含应用内的四个阶段,与应用外的2个阶段。当中应用内的阶段例如以下:
    • 布局(CPU):这是准备你的视图/图层的层级关系。以及设置图层属性(位置,背景色。边框等等)的阶段。
    • 显示(CPU):这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的-drawRect:-drawLayer:inContext:方法的调用路径。
    • 准备(CPU):这是Core Animation准备发送动画数据到渲染服务的阶段。这同一时候也是Core Animation将要运行一些别的事务比如解码动画过程中将要显示的图片的时间点。

    • 提交(CPU):这是最后的阶段,Core Animation打包全部图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。
    当数据被打包到渲染服务进程。会将其反序列化形成还有一个渲染树。

    使用这个渲染树对动画的每一帧做出例如以下工作:

    • 生成渲染树:(CPU)对全部的图层属性计算中间值。设置OpenGL几何形状(纹理化的三角形)来运行渲染
    • 渲染(GPU):在屏幕上渲染可见的三角形

    减少GPU图层绘制的部分场景:
    • 太多的几何结构 - 这发生在须要太多的三角板来做变换,以应对处理器的栅格化的时候。现代iOS设备的图形芯片能够处理几百万个三角板。所以在Core Animation中几何结构并非GPU的瓶颈所在。但因为图层在显示之前通过IPC发送到渲染server的时候(图层实际上是由非常多小物体组成的特别重量级的对象),太多的图层就会引起CPU的瓶颈。这就限制了一次展示的图层个数(见本章兴许“CPU相关操作”)。
    • 重绘 - 主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比率)是有限的,所以须要避免重绘(每一帧用相同的像素填充多次)的发生。在现代iOS设备上,GPU都会应对重绘;即使是iPhone 3GS都能够处理高达2.5的重绘比率。并任然保持60帧率的渲染(这意味着你能够绘制一个半的整屏的冗余信息,而不影响性能),而且新设备能够处理很多其它。
    • 离屏绘制 - 这发生在当不能直接在屏幕上绘制。而且必须绘制到离屏图片的上下文中的时候。

      离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会减少GPU性能。对于特定图层效果的使用,比方圆角,图层遮罩。阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。

      但这不意味着你须要避免使用这些效果,仅仅是要明确这会带来性能的负面影响。

    • 过大的图片 - 假设视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必须要用CPU在图层每次显示之前对图片预处理,相同也会减少性能。

    减少CPU图层绘制的部分场景:
    • 布局计算 - 假设你的视图层级过于复杂,当视图呈现或者改动的时候,计算图层帧率就会消耗一部分时间。特别是使用iOS6的自己主动布局机制尤为明显。它应该是比老版的自己主动调整逻辑加强了CPU的工作。
    • 视图惰性载入 - iOS仅仅会当视图控制器的视图显示到屏幕上时才会载入它。

      这对内存使用和程序启动时间非常有优点,可是当呈现到屏幕上之前,按下button导致的很多工作都会不能被及时响应。比方控制器从数据库中获取数据,或者视图从一个nib文件里载入,或者涉及IO的图片显示(见兴许“IO相关操作”)。都会比CPU正常操作慢得多。

    • Core Graphics绘制 - 假设对视图实现了-drawRect:方法,或者CALayerDelegate-drawLayer:inContext:方法,那么在绘制不论什么东西之前都会产生一个巨大的性能开销。为了支持对图层内容的随意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染server。在此基础上,Core Graphics绘制就会变得十分缓慢。所以在一个对性能十分挑剔的场景下这样做十分不好。
    • 解压图片 - PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。可是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。

      为了节省内存。iOS通常直到真正绘制的时候才去解码图片(14章“图片IO”会更具体讨论)。

      依据你载入图片的方式,第一次对图层内容赋值的时候(直接或者间接使用UIImageView)或者把它绘制到Core Graphics中。都须要对它解压。这种话,对于一个较大的图片,都会占用一定的时间。

    2、Instruments实现App性能优化,操作顺序通常例如以下:
    • 加入优化工具
    • 选择福选项。设置感兴趣的数据
    • 对照測试得结果
    Efficient Drawing(高效画图)

    1、软件绘制
    软件画图意为不借助GPU的图形绘制。在iOS中通常就是由Core Graphics来实现。但其对照Core Animation和OpenGL,Core Graphics要慢不少,并且也十分消耗内存。
    软件画图不仅效率低,还会消耗可观的内存。CALayer仅仅须要一些与自己相关的内存:仅仅有它的寄宿图会消耗一定的内存空间。即使直接赋给contents属性一张图片,也不须要添加额外的照片存储大小。假设同样的一张图片被多个图层作为contents属性,那么他们将会共用同一块内存,而不是复制内存块。

    可是一旦你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(事实上就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文须要的大小的内存可从这个算式得出:图层宽*图层高*4字节。宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说。这个内存量就是 2048*1526*4字节,相当于12MB内存,图层每次重绘的时候都须要又一次抹掉内存然后又一次分配。

    软件画图的代价昂贵,除非绝对必要。你应该避免重绘你的视图。

    提高绘制性能的秘诀就在于尽量避免去绘制。



    2、矢量图形

    我们用Core Graphics来画图的一个通常原因就是仅仅是用图片或是图层效果不能轻易地绘制出矢量图形。矢量画图包括一下这些:

    • 随意多边形(不不过一个矩形)
    • 斜线或曲线
    • 文本
    • 渐变
    文章中实现了例如以下效果的demo:


    事实上现思路就是每当有touch event发生,则向UIBezierPath中加入一条直线。但在对于屏幕中每当有一个touch event事件发生,意味着整个屏幕都要又一次绘制一遍,当须要又一次绘制的内容越来越多,则会引起帧率的下降。

    这时候我们能够通过脏矩阵来对这个问题进行优化。


    3、脏矩形
    脏矩形是一个能制定又一次绘制区域的机制。

    在程序中则是通过用setNeedDispalyInRect来替代setNeedDispaly方法来实现绘制详细的区域,详细实现见文章。


    4、异步绘制
    UIKit的单线程天性意味着寄宿图通常要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,可是假设能避免用户等待绘制完毕就好多了。

    针对这个问题,core Animation提供了一下两种方案来实现异步绘制,提高界面的响应效率:
    • CATiledLayer(详细实现可见第六章)
    • drawsAsynchronously(????)



    Image IO(图像IO)——研究怎样优化从闪存或者网络中载入和显示图片

    1、载入与潜伏
    画图实际消耗的时间通常并非影响性能的因素,并且若把图片直接存储在内存,会损耗大量的资源,所以须要在应用执行的时候周期性地载入和卸载图片。
    图片文件的载入速度同一时候受到CPU及IO(输入/输出)延迟的影响。

    iOS设备中的闪存已经比传统硬盘快非常多了。但仍然比RAM慢将近200倍左右,这就须要慎重地管理载入。以避免延迟。

    有时候图片也须要从远程网络连接中下载,这将会比从磁盘载入要消耗很多其它的时间,甚至可能因为连接问题而载入失败(在几秒钟尝试之后)。你不能在主线程中载入网络,并在屏幕冻结期间期望用户去等待它。所以须要后台线程。


    2、异步线程载入
    因为图片的载入是十分耗时间的,若把该操作放置在主线程中运行会减少CPU的利用率甚至拖慢帧率。能够通过GCD或者NSOperationQueue来实现异步载入最后再在主线程中同步发起渲染就可以。

    3、延迟解压
    一旦图片文件被载入使用。就必需要经过解码(解压)的过程,解码过程是一个相当复杂的任务。耗时长也占大量的内存。
    对于PNG:载入相对长。文件相对更大,但解码比較快。
    对于JPEG:载入快,图片小,但解码算法复杂耗时长。

    因为iOS系统会让载入完毕的图片不会马上解压,而是到须要用的时刻才正式解压。所以会影响性能。

    • iOS系统提供下面三种方式来实现绕过延迟解压的机制
      • imageName方法是能避免延迟载入。并且该方法在载入后会马上解压,但仅仅相应用资源束有效。
      • 将载入图片设置为图层内容。如UIImageView的iamge属性。但这须要在主线程运行。所以不会对性能有大的提升
      • 使用ImageIO框架

    4、使用CATiledLayer实现异步载入和显示大型图片
    略。


    5、分辨率交换
    略。


    6、使用imageNamed实现缓存
    imageName方法是能避免延迟载入,并且该方法在载入后会马上解压,但仅仅相应用资源束有效。所以网络图片无效。
    之前我们提到使用[UIImage imageNamed:]载入图片有个优点在于能够立马解压图片而不用等到绘制的时候。可是[UIImage imageNamed:]方法有还有一个很显著的优点:它在内存中自己主动缓存了解压后的图片。即使你自己没有保留对它的不论什么引用。

    所以也要注意不能用于载入大图片,不然会占用大量的内存资源。


    • 对于下面场景不能使用imageName。须要自己实现缓存机制:
      • [UIImage imageNamed:]方法只适用于在应用程序资源束文件夹下的图片,可是大多数应用的很多图片都要从网络或者是用户的相机中获取,所以[UIImage imageNamed:]就没法用了。
      • [UIImage imageNamed:]缓存用来存储应用界面的图片(button,背景等等)。假设对比片这样的大图也用这样的缓存,那么iOS系统就非常可能会移除这些图片来节省内存。

        那么在切换页面时性能就会下降,由于这些图片都须要又一次载入。对传送器的图片使用一个单独的缓存机制就行把它和应用图片的生命周期解耦。

      • [UIImage imageNamed:]缓存机制并非公开的。所以你不能非常好地控制它。比如,你没法做到检測图片是否在载入之前就做了缓存。不可以设置缓存大小,当图片无用的时候也不能把它从缓存中移除。

    7、自己定义缓存

    • 若须要实现自己的缓存机制。通常得从下面四个方面进行考虑:
      • 选择一个合适的缓存键 - 缓存键用来做图片的唯一标识。

        假设实时创建图片,通常不太好生成一个字符串来区分别的图片。

        在我们的图片传送带样例中就非常easy,我们能够用图片的文件名称或者表格索引。

      • 提前缓存 - 假设生成和载入数据的代价非常大。你可能想当第一次须要用到的时候再去载入和缓存。

        提前载入的逻辑是应用内在就有的。可是在我们的样例中,这也非常好实现,由于对于一个给定的位置和滚动方向。我们就能够精确地推断出哪一张图片将会出现。

      • 缓存失效 - 假设图片文件发生了变化。如何才干通知到缓存更新呢?这是个非常困难的问题(就像菲尔 卡尔顿提到的),可是幸运的是当从程序资源载入静态图片的时候并不须要考虑这些。对用户提供的图片来说(可能会被改动或者覆盖),一个比較好的方式就是当图片缓存的时候打上一个时间戳以便当文件更新的时候作比較。
      • 缓存回收 - 当内存不够的时候,如何推断哪些缓存须要清空呢?这就须要到你写一个合适的算法了。

        幸运的是,对缓存回收的问题。苹果提供了一个叫做NSCache通用的解决方式



    8、NSCache
    NSCache和NSDictionary类似。都是直接通过键值进行訪问,但不同的是,NSCache所持有的对象在内存不足的时候。会自己主动将其释放。
    • 当然开发人员能够通过下面设置来粗颗粒度地进行缓存管理的约束
      • -setCountLimit:方法设置缓存大小
      • -setObject:forKey:cost:来对每一个存储的对象指定消耗的值来提供一些暗示
      • -setTotalCostLimit:方法来指定全体缓存的尺寸

    9、文件格式与载入性能
    略。

    Layer Performance(图层性能)


    1、隐形绘制
    • 寄宿层能够通过下面方式显示绘制
      • Core Graphics
      • 给contents属性赋值图片
      • 在屏幕外事先绘制CGContext

    • 当发生下面场景会触发隐式绘制
      • 使用特性的图层属性
      • 特定的视图
      • 特定视图的子类

    2、文本
    CATextLayer与UILable都是直接将文本绘制在图层的寄宿层内,所以要避免频繁的修改,若该文本须要频繁修改,能够先将其放在一个子图层上,通过contentMode来等比例缩放寄宿层。


    3、光栅化
    我们提到了CALayershouldRasterize属性(光栅化)。它能够解决重叠透明图层的混合失灵问题。
    启用shouldRasterize属性会将图层绘制到一个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的contents和子图层。

    假设有非常多的子图层或者有复杂的效果应用,这样做就会比重绘全部事务的全部帧划得来得多。

    可是光栅化原始图像须要时间。并且还会消耗额外的内存。

    当我们使用得当时,光栅化能够提供非常大的性能优势(如你在第12章所见),可是一定要避免作用在内容不断变动的图层上,否则它缓存方面的优点就会消失。并且会让性能变的更糟。
    为了检測你是否正确地使用了光栅化方式,用Instrument查看一下Color Hits Green和Misses Red项目。是否已光栅化图像被频繁地刷新(这样就说明图层并非光栅化的好选择,或则你无意间触发了不必要的改变导致了重绘行为)。
    总结:会占用较多内存,要避免反复绘制。所以一旦应用要尽量降低又一次绘制,而多利用快照缓存。

    4、离屏渲染
    • 当图层的下面属性被改动会触发离屏渲染
      • 圆角(当和maskToBounds一起使用时)
      • masks(图层蒙板)
      • shadows(阴影)
      • shouldRasterize(光栅化)
      • edge antialiasing(抗锯齿)
      • group opacity(不透明)
      • 渐变

    屏幕外渲染和我们启用光栅化时相似。除了它并没有像光栅化图层那么消耗大。子图层并没有被影响到,并且结果也没有被缓存,所以不会有长期的内存占用。可是,假设太多图层在屏幕外渲染依旧会影响到性能
    对于那些须要动画并且要在屏幕外渲染的图层来说。你能够用CAShapeLayercontentsCenter或者shadowPath来获得相同的表现并且较少地影响到性能。

    5、混合和过度绘制
    开发人员应该尽量降低重叠图层的反复渲染,由于对于用户看来某些遮挡的图层的内容是无关紧要的,但渲染就会消耗资源。所以不管不论什么场景都建议例如以下操作:
    • 给视图的backgroundColor属性设置一个固定的,不透明的颜色
    • 设置opaque属性为YES
    • 明智地使用shouldRasterize属性,能够将一个固定的图层体系折叠成单张图片,这样就不须要每一帧又一次合成了,也就不会有由于子图层之间的混合和过度绘制的性能问题了。
    当然光栅化是是详细场景二选择使用,不然也会引入别的性能问题,如占用大量内存。


    6、降低图层数量
    初始化图层,处理图层,打包通过IPC发给渲染引擎,转化成OpenGL几何图形,这些是一个图层的大致资源开销。其实,一次性可以在屏幕上显示的最大图层数量也是有限的。

    确切的限制数量取决于iOS设备。图层类型。图层内容和属性等。

    可是总得说来能够容纳上百或上千个,以下我们将演示即使图层本身并没有做什么也会遇到的性能问题。


    7、对象回收
    处理巨大数量的相似视图或图层时另一个技巧就是回收他们。对象回收在iOS颇为常见。UITableViewUICollectionView都实用到,MKMapView中的动画pin码也实用到,还有其它非常多样例。




    Q&A:
    • Q:图层没有寄宿层,是否等于无法显示内容?(寄宿层)
      • A:不是,还有通过矢量图绘制的方式来实现图形呈现。如通用的如今应该就是通过算来来绘制矢量图的,仅仅有须要显示图片或者调用core graphics的时候才会生成寄宿层。所以若非必要不要实现draw方法。不然系统会默认加入一个寄宿层。

    • Q:组透明的点颜色计算公式?(视觉效果)
      • A:若某子view是透明的。那么它的颜色公式为:child_color*alpha + super_color*(1-alpha)。

    • Q:3D效果与共享灭点的详细操作?(变换)
      • A:在做3D效果时要尽量将全部图形的灭点设置在屏幕的中心。所以建议先在屏幕的中心创建图像。然后通过transform的形式来移动图层。那么就能保证灭点在屏幕中心。
    • Q:3D变化计算?(变换)
      • A:提供了平移、旋转、缩放,但每次操作都是基于二维的,所以注意当前的參考坐标系,剩下的计算与二维一样。
    • Q:立方体的光亮与阴影原理?(变换)
      • A:通过GLK库的函数计算垂直,并加入阴影层。
    • Q:3D点击事件处理?(变换)
      • A:在3D图层中。由于一个点击的点,实际上可能会穿过两个图层的点,这时事件拦截的顺序由subviews数组的顺序认为,也就是谁最靠近用户视觉,谁有限响应。
    • Q:常规的界面绘制是通过矢量绘制还是寄宿图绘制实现?(专用图层)
      • A:常规界面是矢量绘制完毕的。
    • Q:平面化3d层级结构的意思?(专用图层)
      • A:
    • Q:CATiledLayer解决大图载入(专用图层)
      • A:

    0
    0

    查看评论
    * 以上用户言论仅仅代表其个人观点,不代表CSDN站点的观点或立场
  • 相关阅读:
    *CodeForces 1325E
    CodeForces 1325D
    线性基
    分块
    RabbitMQ学习笔记(四、RabbitMQ队列)
    RabbitMQ学习笔记(三、生产者与消费者)
    RabbitMQ学习笔记(二、RabbitMQ结构)
    RabbitMQ学习笔记(一、消息中间件基础)
    SpringCloud学习笔记(十、SpringCloud Sleuth)
    SpringCloud学习笔记(九、SpringCloud Stream)
  • 原文地址:https://www.cnblogs.com/cynchanpin/p/8257899.html
  • Copyright © 2020-2023  润新知