CoreAnimationiOS开发iOS Developer

core_animation笔记

2016-11-25  本文已影响131人  猿鹿说

个人博客地址:Lixuzong's Blog

我们所在屏幕上看到的都是Core Animation框架提供的,所以这并不是一个只关于动画的框架,他包含了我们在屏幕上所能看到的一切东西。这里首先说一下CALayer和UIView的关系,UIView是CALayer的管理者,CALayer是我们在屏幕上所能看到的UIView说呈现的。用过Photoshop的都知道,图片可以是很多图层的叠加计算的结果,与之类似,我们在屏幕上所看到的也是CALayer层叠加的结果,UIView就管理着这个相互叠加的过程。再者,UIView还管理着手势的响应。如果只从视图的角度来看的话,UIView只是对CALayer的一层封装。

core_animation_book

首先先放上本书的链接,看的过程中有些翻译不是非常的准确,所以是结合着原版的一起看的。

关于Layer的知识点

图层树

关于图层树,我们看到的图层与UIView的层级是对应的,而UIView的层级与CALayer是对应的,其中CALayer tree又分为呈现树(presentation layer tree)和模型树(model layer tree),因为layer默认是带有隐式动画的,但是我们直接改变layer属性的时候是立即生效的,也就是说layer的属性改变是立即执行的,但是界面上还是依然反应有一个动画的过程,所以这个时候就分成了两个tree,一个是界面上所呈现的层级(presentation layer tree),一个是我们修改的层级(model layer tree)。

Layer的contents

contents是一个id类型的对象,但是接收的类型应该是Core Foundation框架的类型,这里是因为在MAC OS系统上CGImage和NSImage类型值都是可以起作用的。在iOS上的话就是用bridge将CGImage转成Core Foundation的id类型就可以了。下面具体看一下contens的属性。

相关属性

custom drawing

是在不设置contents属性为image的情况下直接画一个,在UIView里面有drawRect:,虽然drawRect是UIView的方法,但是在底层还是通过CALayer安排重绘工作和保存产生的图片的。绘制用到的是CADelegate提供的两个方法,分别是drawLayer:(CALayer)layer inContext:(CGContext)ctx调用这个方法之前CALayer会自动生成一个空的图像(由bounds和contentsSacle决定)和一个Core Graphics的绘制上下文环境,为绘制图做准备。调用的时候与UIView方法setNeedDisplay类似,CALayer的方法是调用displayLayer来触发重绘操作。

图层几何

图层几何是看图层内部是如何根据父图层和兄弟图层来控制位置和尺寸的。另外我们也会涉及如何管理图层的几何结构,以及它是如何被自动调整和自动布局影响的。

-(void)layoutSublayersOfLayer:(CALayer*)layer

当图层的bounds改变或者图层 -setNeedsLayout 方法被调用的时候,这个函数就会执行。在这个方法里面可以重新摆放或者调整子图层的大小。

Layer视觉效果

CGAffineTransform

首先,所有的变换都是基于CGPoint的,仿射变换之后的矩形两对边是相互平行的。

CGTransform3D

3D变换主要是配置一个矩阵来实现每个点的变化,从而达到整个layer实现3D变换的效果。要想做到灭点相同的话,必须要将layer都放在view的中心,然后在平移到指定的位置,改变layer的frame的话灭点也会发生改变。还有一个简单的办法就是使用sublayerTransform属性。我们可以任意的放置子layer的位置,这样的话也会共享一个灭点。

专用图层

CAShapeLayer

CAShapeLayer是一个通过矢量图而不是通过bitmap来绘制的图层子类。指定线宽和颜色等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就会自动的渲染出来。与直接向CALayer绘制路径相比,CAShapeLayer的优点如下:

1、渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
2、高效使用内存。一个CAShapeLayer不需要像普通layer一样创建一个contents图形,所以无论多大都不会占用过多的内存。
3、不会被图层边界裁掉。一个CAShapeLayer可以在边界之外绘制,不会像普通的layer被裁减掉。
4、不会出现像素化。当你给CAShapeLayer做3D变换的时候,他不像一个contents普通图层一样变得像素化。

使用CAShapeLayer主要就是设置path属性,path是CGPathRef类型,但是为了方便管理内存,使用UIKit框架的UIBezierPath来创建,然后转成CGPathRef类型赋值给path属性,这样的话就会渲染出一个path形状的图层。利用之前的蒙版的功能,我们可以直接将contents裁剪成任意形状。例如可以让两个角圆角,另外两个角是正常的,这些都是很容易实现的。

CATextLayer

它以图层的形式包含了UILabel几乎所有的绘制特性,另外还提供了一些新的额外的特性。在ios6之前UILabel是用WebKit来实现的,而CATextLayer是用Core Text支持的,所以渲染速度要快很多,但是iOS7之后文字渲染交给了Text Kit来实现,其底层还是使用Core Text,所以性能上的差距应该没有这么明显了(自己认为的)。CATextLayer可以使用普通文本和富文本。

CATransformLayer

这个属性没有自己的contents属性,而是管理子layer的Transform变化,主要是为了解决CALayer会将其子layer平面化,而CATransformLayer不会平面化其子layer。这样的话就可以在一个layer里面构造两种不同视角的3D物体。

CAGradientLayer

CAGradientLayer是用来生成两种或更多颜色平滑渐变的。用Core Graphics复制一个CAGradientLayer并将内容绘制到一个普通图层的寄宿图也是有可能的,但是CAGradientLayer的真正好处在于绘制使用了硬件加速。

CAReplicatorLayer

其目的是高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并且在每个复制体上应用不同的变换。使用的时候就是将layer加入到CARelicatorLayer类型的layer中,然后可以在CARelicatorLayer中设置复制的数量和变换。可以高效的来做反射的效果。

CAScrollLayer

Core Animation并不处理用户输入,所以CAScrollLayer并不负责将触摸事件转换为滑动事件,既不渲染滚动条,也不实现任何iOS指定行为例如滑动反弹。用法就是直接重写+ (Class)layerClass方法,将UIView的根layer返回成CAScrollLayer类型,这样的话其中的子layer都是可以滑动的。

CATiledLayer

有些时候你可能需要绘制一个很大的图片,常见的例子就是一个高像素的照片或者是地球表面的详细地图。iOS应用通畅运行在内存受限的设备上,所以读取整个图片到内存中是不明智的。载入大图可能会相当地慢,那些对你看上去比较方便的做法(在主线程调用UIImage的-imageNamed:方法或者-imageWithContentsOfFile:方法)将会阻塞你的用户界面,至少会引起动画卡顿现象。
能高效绘制在iOS上的图片也有一个大小限制。所有显示在屏幕上的图片最终都会被转化为OpenGL纹理,同时OpenGL有一个最大的纹理尺寸(通常是2048/2048,或4096/4096,这个取决于设备型号)。如果你想在单个纹理中显示一个比这大的图,即便图片已经存在于内存中了,你仍然会遇到很大的性能问题,因为Core Animation强制用CPU处理图片而不是更快的GPU(见第12章『速度的曲调』,和第13章『高效绘图』,它更加详细地解释了软件绘制和硬件绘制)。
CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入.

CAEmitterLayer

是一个高性能的粒子引擎,被用来创建实时例子动画如:烟雾,火,雨等等这些效果。

CAEAGLLayer

它是CALayer的一个子类,用来显示任意的OpenGL图形。

AVPlayerLayer

AVPlayerLayer是有别的框架(AVFoundation)提供的,它和Core Animation紧密地结合在一起,提供了一个CALayer子类来显示自定义的内容类型。AVPlayerLayer是用来在iOS上播放视频的。他是高级接口例如MPMoivePlayer的底层实现,提供了显示视频的底层控制。AVPlayerLayer的使用相当简单:你可以用+playerLayerWithPlayer:方法创建一个已经绑定了视频播放器的图层,或者你可以先创建一个图层,然后用player属性绑定一个AVPlayer实例。

动画

implicit animation

隐式动画就是系统会自动的给layer做动画,layer位置、颜色等的改变会引起一个0.25秒的动画过程。core animation是根据什么来判断动画类型和时间的那,看起来是自动的设置,实际上是有事务来进行管理的。

事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。即使不显示的调用begin方法,在一次runloop循环中

我们也可以手动的关闭动画,layer tree中,作为UIView的根layer的动画是被关上的,并不是通过layer的开关关闭的,首先看一下CALayer的代理方法。

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

explicit aniamtion

显示动画是使用CAAnimation做的动画,我们来看一下CAAniamtion的子类。

CAPropertyAniamtion(属性动画)

过渡动画(CATransition)

对于没有办法做动画的属性,或者交换一段文本和图片,或者用一段网格来替换,这个时候属性动画是没用作用的。属性动画只对图层可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片,因为CoreAnimation不知道什么时候插入图片),又或者成层级关系中添加或者移除图层,属性动画将不起作用,于是就有了过度的概念。

过度并不像属性动画那样平滑的在两个值之间做动画,而是影响到整个图层的变化。过度动画首先展示之前的图层外观,然后通过一个过度变换到新的外观。

我们使用CATransition来管理过度效果,和别的子类不同,CATransition有一个type和subType来标识变换效果。type是一个NSString类型,提供了四种过度类型,分别是:

kCATransitionFade

kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal

过渡动画和之前的属性动画或者动画组添加到图层上的方式一致,都是通过-addAnimation:forKey:方法。但是和属性动画不同的是,对指定的图层一次只能使用一次CATransition,因此,无论你对动画的键设置什么值,过渡动画都会对它的键设置成“transition”,也就是常量kCATransition。

CATransision可以对图层任何变化平滑过渡的事实使得它成为那些不好做动画的属性图层行为的理想候选。苹果当然意识到了这点,并且当设置了CALayer的content属性的时候,CATransition的确是默认的行为。但是对于视图关联的图层,或者是其他隐式动画的行为,这个特性依然是被禁用的,但是对于你自己创建的图层,这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画。

图层树的动画

CATransition并不作用于指定的图层属性,这就是说你可以在即使不能准确得知改变了什么的情况下对图层做动画,例如,在不知道UITableView哪一行被添加或者删除的情况下,直接就可以平滑地刷新它,或者在不知道UIViewController内部的视图层级的情况下对两个不同的实例做过渡动画。因为它们不仅涉及到图层的属性,而且是整个图层树的改变--我们在这种动画的过程中手动在层级关系中添加或者移除图层。要确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,否则CATransition将会和图层一起被移除。一般来说,你只需要将动画添加到被影响图层的superlayer。

自定义动画

苹果通过UIView +transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。但是这里的可用的过渡选项和CATransition的type属性提供的常量完全不同

动画的时间、缓冲

model layer & presentation layer

我们直接改变layer的position,可以发现position属性会直接发生变化,但是屏幕上我们看到的layer却是有一个渐变的动画,而不是根据我们设置的属性实时的发生变化,所以,详细的划分一下,这里的layer就是model layer,其属性表示的是动画结束之后layer的属性,而屏幕上我们直接看到的就是presentation layer,layer实时在屏幕上的位置。

性能调优

性能陷阱

软件绘图不仅效率低,还会消耗可观的内存。CALayer只需要一些与自己相关的内存:只有它的寄宿图会消耗一定的内存空间。即使直接赋给contents属性一张图片,也不需要增加额外的照片存储大小。如果相同的一张图片被多个图层作为contents属性,那么他们将会共用同一块内存,而不是复制内存块。

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

加载图片消耗内存和占用CPU的原因

一旦图片文件被加载就必须要进行解码,解码过程是一个相当复杂的任务,需要消耗非常长的时间。解码后的图片将同样使用相当大的内存。

用于加载的CPU时间相对于解码来说根据图片格式而不同。对于PNG图片来说,加载会比JPEG更长,因为文件可能更大,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程。JPEG图片更小,加载更快,但是解压的步骤要消耗更长的时间,因为JPEG解压算法比基于zip的PNG算法更加复杂。

图片加载的性能优化(TableView优化)

NSInteger index = indexPath.row;
NSURL *imageURL = [NSURL fileURLWithPath:self.imagePaths[index]]; 
NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES};
CGImageSourceRef source = CGImageSourceCreateWithURL(
(__bridge CFURLRef)imageURL, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0,
(__bridge CFDictionaryRef)options);
UIImage *image = [UIImage imageWithCGImage:imageRef]; CGImageRelease(imageRef);
CFRelease(source);

这里还有第五种方式依然使用UIKit框架来实现立即解压图片,以为绘制图片之前会解压图片,那么就直接将图片画到CGContext里面,从而实现立即解压,这有个好处就是不是必须在主线程中实现

离屏渲染影响性能的原因:「直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。」触发离屏渲染后这种转换发生在每一帧,在界面的滚动过程中如果有大量的离屏渲染发生时会严重影响帧率。

#import "ViewController.h"

@interface ViewController() 

@property (nonatomic, copy) NSArray *imagePaths;
@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    //set up data
    self.imagePaths = [[NSBundle mainBundle] pathsForResourcesOfType:@"png" inDirectory:@"Vacation Photos"];
    //register cell class
    [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return [self.imagePaths count];
}

- (UIImage *)loadImageAtIndex:(NSUInteger)index
{
    //set up cache
    static NSCache *cache = nil;
    if (!cache) {
        cache = [[NSCache alloc] init];
    }
    //if already cached, return immediately
    UIImage *image = [cache objectForKey:@(index)];
    if (image) {
        return [image isKindOfClass:[NSNull class]]? nil: image;
    }
    //set placeholder to avoid reloading image multiple times
    [cache setObject:[NSNull null] forKey:@(index)];
    //switch to background thread
    dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        //load image
        NSString *imagePath = self.imagePaths[index];
        UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
        //redraw image using device context
        UIGraphicsBeginImageContextWithOptions(image.size, YES, 0);
        [image drawAtPoint:CGPointZero];
        image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        //set image for correct image view
        dispatch_async(dispatch_get_main_queue(), ^{ //cache the image
            [cache setObject:image forKey:@(index)];
            //display the image
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem: index inSection:0]; UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
            UIImageView *imageView = [cell.contentView.subviews lastObject];
            imageView.image = image;
        });
    });
    //not loaded yet
    return nil;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    //dequeue cell
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    //add image view
    UIImageView *imageView = [cell.contentView.subviews lastObject];
    if (!imageView) {
        imageView = [[UIImageView alloc] initWithFrame:cell.contentView.bounds];
        imageView.contentMode = UIViewContentModeScaleAspectFit;
        [cell.contentView addSubview:imageView];
    }
    //set or load image for this index
    imageView.image = [self loadImageAtIndex:indexPath.item];
    //preload image for previous and next index
    if (indexPath.item < [self.imagePaths count] - 1) {
        [self loadImageAtIndex:indexPath.item + 1]; }
    if (indexPath.item > 0) {
        [self loadImageAtIndex:indexPath.item - 1]; }
    return cell;
}

@end
上一篇下一篇

猜你喜欢

热点阅读