iOS UIView和CALayer

iOS程序中,视图可以用UIViewCALayer来创建,下面就以它们为导火索来进一步学习总结iOS程序显示性能优化。

一、UIView和CALayer的区别
  1. UIViewUIKit框架中的, 继承于UIRespond,可以响应触摸事件;CALayerQuartzCore框架里面CoreAnimation中的,继承自NSObject, 不响应事件。
  2. UIView有个只读的layer属性,称为根Layer,UIView的图层是由layer来生成的,所以对UIView的frame等属性的修改本质上是对layer的属性的修改。
  3. 我们可以给UIView添加子layerself.layer addSublayer:,效果像addSubview一样显示,但sublayer在修改positionsizeopacity等属性时会产生隐式动画(默认有个动画效果), 这就没有操作subview方便。
  4. 使用系统提供的layer和自定义layer都可添加为subLayer,但需要调用[layer setNeedsDisplay]。 对于自定义layer方式有三个:设置layer代理自定义CALayer子类
    /// 方式一:添加layer,设置代理,调用 [layer setNeedsDisplay];
    CALayer *layer = [CALayer layer];// 测试设置代理方式用这个
    ZLLayer *layer = [ZLLayer layer]; // 测试自定义layer用这个
    layer.delegate = self;
    layer.bounds = CGRectMake(0, 0, 200, 200);
    layer.backgroundColor = UIColor.blueColor.CGColor;
    layer.position = CGPointMake(100, 100);
    layer.anchorPoint = CGPointZero;
    [self.view.layer addSublayer:layer];
    [layer setNeedsDisplay];
// 代理方法的实现
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    NSLog(@"%s", __func__);
    CGContextSetRGBFillColor(ctx, 1, 0, 0, 1);
    CGContextAddEllipseInRect(ctx, CGRectMake(10, 10, 80, 80));
    CGContextFillPath(ctx);
}

/// 方式二:自定义子类(自定义的ZLLayer可以直接添加到layer上,也可以创建自定义view类,在+ (Class)layerClass中返回)
@implementation ZLLayer
- (void)display{
    NSLog(@"%s", __func__);
    [super display];
    NSLog(@"%@", self.contents);
}
- (void)drawInContext:(CGContextRef)ctx{
    NSLog(@"%s", __func__);
    CGContextAddEllipseInRect(ctx, CGRectMake(10, 10, 80, 80));
    CGContextSetRGBFillColor(ctx, 1, 0, 0, 1);
    CGContextFillPath(ctx);
}
@end

  1. UIView根layer的代理。
二、UIView从创建到显示主要经过了哪些步骤?

我们通过上述自定义的ZLLayer类, 在各个方法中打印调用方法顺序及上下文,可以验证下面的过程。

  1. 当视图(UIView或CALayer)第一次展示到窗口或者手动调用-setNeedsDisplay时,当前图层Layer会被标记为 dirty(多次调用-setNeedsDisplay效果是一样的), 接着当渲染系统准备好时会调用图层layer的-display方法。

  2. -display方法中, [super dispalay]中的实现是:

  • 首先会创建上下文(如果已有上下文则使用之前的).
  • 接下来会判断layer的delegate有没有实现- (void)displayLayer:(CALayer *)layer, 如果实现了则系统会认为layer.content由这个方法里面生成,系统会调用这个方法并且不会去绘制内容(即
    不会调用layer内部- (void)drawInContext:(CGContextRef)ctx, 也不会调用代理的- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx)
  • 如果layer的delegate没有实现- (void)displayLayer:(CALayer *)layer, 则系统会认为需要手动绘制内容,首先判断Layer中有没有实现- (void)drawInContext:(CGContextRef)ctx方法,实现了则调用; 否则调用代理的- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx去完成内容绘制。
  1. UIView是Layer的代理,UIView中实现了代理方法- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx,内部会调用UIView的- (void)drawRect:(CGRect)rect方法。
  2. 上面方法调用完后,GPU根据上下文环境进行纹理的渲染,渲染完成后放入帧缓存中。
  3. 当显示器发出VSync(垂直同步信号)时,视频控制器从帧缓存中读取一帧,按照水平同步信号显示到屏幕。
    APP卡顿优化学习总结
    综上,我们创建视图应该是这样的:
  • 首先对于不复杂的视图内容,则正常使用UIView、layer。
  • 如果像八角形、股票曲线等复杂内容,需要手动使用UIKit中的UIBezierPath等API或者Core Graphics中的API的,可以根据需要选择
    • 第一种方法: 创建CALayer,设置delegate,然后在代理类实现方法- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx,将内容绘制到这个ctx中。
    • 第二种方法:创建自定义CALayer子类,重写方法- (void)drawInContext:(CGContextRef)ctx,将内容绘制到这个ctx中。
    • 第三种方法,将自定义view作为展示视图,重写view的- (void)drawRect:(CGRect)rect方法,将内容绘制到这个ctx中。
    • 第四种方法:使用UIKit中的API,里面会默认获取在栈顶的上下文,将内容绘制到上下文中。

比如绘制一个八角形的代码:

  • 通过 UIKit 能做到这一点
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
[path addLineToPoint:CGPointMake(0.4, 18.05)];
[path addLineToPoint:CGPointMake(18.8, -0.47)];
[path addLineToPoint:CGPointMake(37.21, 18.05)];
[path addLineToPoint:CGPointMake(34.31, 20.83)];
[path addLineToPoint:CGPointMake(20.88, 7.22)];
[path addLineToPoint:CGPointMake(20.88, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 7.22)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];
  • 通过 Core Graphics, 需要手动获取上下文
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
CGContextAddLineToPoint(ctx, 0.4, 18.05);
CGContextAddLineToPoint(ctx, 18.8, -0.47);
CGContextAddLineToPoint(ctx, 37.21, 18.05);
CGContextAddLineToPoint(ctx, 34.31, 20.83);
CGContextAddLineToPoint(ctx, 20.88, 7.22);
CGContextAddLineToPoint(ctx, 20.88, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 7.22);
CGContextClosePath(ctx);
CGContextSetLineWidth(ctx, 1);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokePath(ctx);
三、隐式动画

我们对UIView中非根layer修改属性如:posion、bounds、opacity时会自动有一个动画效果,如果不想要这个效果可以这么做:

// 使用UIView的api去除隐式动画,如果不是手动添加的layer可起作用
[UIView performWithoutAnimation:^{
    [collectionView reloadItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:index inSection:0]]];
}];
// 假如self.frameLayer是手动添加到view中的,使用下面的方式去除隐式动画
- (void)layoutSubviews{
    [super layoutSubviews];
    
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    self.frameLayer.frame = self.frameView.bounds;
    [CATransaction commit];
}

隐式动画产生的原因:
可阅读iOS动画-CALayer隐式动画原理与特性

  • Core Animation在每个RunLoop周期会自动开始一次新的事务,即使你不显式的使用[CATranscation begin]开始一次事务。
  • 在一次RunLoop运行循环中改变的属性会被集中执行默认0.25秒的动画。
  • 事务通过CATransaction类来做管理的:
//1.动画属性的入栈
+ (void)begin;
//2.动画属性出栈
+ (void)commit;
//3.设置当前事务的动画时间
+ (void)setAnimationDuration:(CFTimeInterval)dur;
//4.在动画结束时提供一个完成的动作
+ (void)setCompletionBlock:(nullable void (^)(void))block;
四、提高显示性能
pixels-software-stack.png
  1. GPU 是一个专门为图形高并发计算而量身定做的处理单元, 能高效的将不同纹理合成起来;
  2. GPU Driver 使不同的GPU在下一个层级(如 OpenGL/OpenGL ES)更为统一;
  3. OpenGL 和 GPU 密切的工作以提高GPU的能力,并实现硬件加速渲染
  4. iOS程序中,UIKit框架中控件的图层部分是由CALayer来完成,而CALayer是Core Animation中的。
  5. 一个纹理就是一个包含 RGBA 值的长方形,在 Core Animation 世界中这就相当于一个 CALayer。

从GPU工作,可以做的优化:
每一个 layer 是一个纹理,所有的纹理都以某种方式堆叠在彼此的顶部,GPU 需要合成堆叠的纹理来得到屏幕上像素对应具体的 RGB 值。GPU计算时分像素和纹理对齐、不对齐两种情况:

  • 在像素对齐的情况下,如果纹理不透明,那么GPU会取最上层的那个纹理的色值;如果是有透明度,则需要计算多个纹理共同作用下的色值。
  • 在像素不对齐的情况下,主要有两个原因可能会造成不对齐。第一个便是缩放;当一个纹理放大缩小的时候,纹理的像素便不会和屏幕的像素排列对齐。另一个原因便是当纹理的起点不在一个像素的边界上。在这两种情况下,GPU 需要再做额外的计算。它需要将源纹理上多个像素混合起来,生成一个用来合成的值。当所有的像素都是对齐的时候,GPU 只剩下很少的工作要做。Core Animation 工具和模拟器有一个叫做 color misaligned images 的选项,当这些在你的 CALayer 实例中发生的时候,这个功能便可向你展示。

因此我们需要尽量减少视图层次、减少透明视图、layer的放大缩小.

从CPU工作来看,可以做的优化:

  1. 可一次计算好子控件的frame后保存,减少计算量。
  2. 对于加载图片等耗时操作,开启子线程去执行,比如SDWebView用的自己创建的最大并发数为6的并发队列来安排下载任务。
  3. 图片下载完后,设置给UIImageView时,如果我们不在子线程完成图片的解码,那么解码操作会在主线程完成,容易会造成卡顿。解决办法是在子线程将PNG\JPG\WebP等格式图片画到图形上下文当中后生成CGImage,再回调到主线程将这个CGImage设置给UIImageView;另外, 在处理高分辨率大图时直接解码操作会让内存暴增,这时则需要将图片分段绘制到上下文。
    具体可参考 SDWebImage的图片解码源码阅读
  4. 尽量避免离屏渲染。
  5. 你可以使用可变尺寸的图像来降低绘图系统的压力。让我们假设你需要一个 300×50 点的按钮插图,这将是 600×100=60k 像素或者 60kx4=240kB 内存大小需要上传到 GPU,并且占用 VRAM。如果我们使用所谓的可变尺寸的图像,我们只需要一个 54×12 点的图像,这将占用低于 2.6k 的像素或者 10kB 的内存,这样就变得更快了。
    Core Animation 可以通过 CALayer 的 contentsCenter 属性来改变图像,大多数情况下,你可能更倾向于使用,-[UIImage resizableImageWithCapInsets:resizingMode:]
    同时注意,在第一次渲染这个按钮之前,我们并不需要从文件系统读取一个 60k 像素的 PNG 并解码,解码一个小的 PNG 将会更快。通过这种方式,你的程序在每一步的调用中都将做更少的工作,并且你的视图将会加载的更快。
五、离屏渲染

屏幕外的渲染会渲染图层树中的一部分到一个新的缓冲区中,这需要消耗更多的CPU和GPU时间,从而导致卡顿。我们常见的操作代码会导致的有:
阴影:设置阴影会导致离屏渲染。

self.bgView.layer.shadowColor = [UIColor blackColor].CGColor;//shadowColor阴影颜色
self.bgView.layer.shadowOffset = CGSizeMake(0,0);//shadowOffset阴影偏移,x向右偏移2,y向下偏移6,默认(0, -3),这个跟shadowRadius配合使用
self.bgView.layer.shadowOpacity = 0.3;//阴影透明度,默认0
self.bgView.layer.shadowRadius = 4;//阴影半径,默认3

避免的方式:设置shadowPath

//参数依次为大小,设置四个角圆角状态,圆角曲度  设置阴影路径可避免离屏渲染
self.bgView.layer.shadowPath = 
[UIBezierPath bezierPathWithRoundedRect:self.bgView.bounds 
byRoundingCorners:UIRectCornerAllCorners 
cornerRadii:CGSizeMake(self.bgView.layer.cornerRadius, self.bgView.layer.cornerRadius)].CGPath;

圆角:设置cornerRadius同时设置masksToBounds=YES会触发离屏渲染,解决办法是设置将图片画到有圆角的rect生成新图片后再进行设置。解决常见的masksToBounds

self.imageView.layer.cornerRadius = 5;
self.imageView.layer.masksToBounds = YES;

遮罩:如果你将mask应用到一个layer上,Core Animation 为了应用这个 mask,会强制进行屏幕外渲染。避免方式:如果不是很必要则不使用。

光栅化: 如果你的程序混合了很多图层,并且想要他们一起做动画,GPU 通常会为每一帧(1/60s)重复合成所有的图层。当使用离屏渲染时,GPU 第一次会混合所有图层到一个基于新的纹理的位图缓存上,然后使用这个纹理来绘制到屏幕上。现在,当这些图层一起移动的时候,GPU 便可以复用这个位图缓存,并且只需要做很少的工作。需要注意的是,只有当那些图层不改变时,这才可以用。如果那些图层改变了,GPU 需要重新创建位图缓存。你可以通过设置 shouldRasterize 为 YES 来触发这个行为。

    self.xxview.layer.shouldRasterize = YES;
    self.xxview.layer.contentsScale = [UIScreen mainScreen].scale;
测试离屏渲染的发生

如下图所示,如果是触发了离屏渲染的layer会有黄颜色。


离屏渲染.png

绘制像素到屏幕的过程

你可能感兴趣的:(iOS UIView和CALayer)