iOS性能---滑动卡顿优化(2)离屏渲染

离屏渲染产生的原因?

  • 非离屏渲染流程:
  • 离屏渲染流程:
  • 如果触发了离屏渲染,那么在显示这个页面的每一帧都会去做离屏渲染的处理,比如切圆角,显示阴影等,如果每一帧都要重新去处理,那么对于CPU以及GPU都带来很大的负担。所以引入了离屏缓冲区,将处理好的图层直接丢到离屏缓冲区,下次渲染的时候直接拿出来显示到屏幕上
  • 离屏缓存区也是有限制的,
    1. 时间限制:缓存的内容超过100ms没有被使用,内容将会被丢弃;
    2. 空间限制:超过屏幕像素大小的2.5倍,将不能再存储新的数据,离屏渲染会失效

离屏检测?

  • 打开模拟器后,选择菜单栏Debug->Color off-screen rendered,开启后,如果有离屏渲染,则会显示成黄色

触发离屏渲染的场景

  1. layer.cornerRadius + layer.masksToBounds
    • 如果只有一个图层,就算同时设置 cornerRadius + masksToBounds 也并不会触发离屏渲染,只有多个图层发生叠加的时候才会触发离屏渲染
    • 只设置cornerRadius不会触发离屏渲染,因为cornerRadius只会设置backgroundColor和border的圆角,并不会对content设置圆角,CALayer大致分为三层(backgroundColor层、content层、border层),
    • 关于圆角,iOS 9及之后的系统版本,苹果进行了一些优化,只设置contents或者UIImageView的image,并加上圆角+裁剪,是不会产生离屏渲染的。但如果加上了背景色、边框或其他有图像内容的图层,还是会产生离屏渲染。
  2. 为图层设置遮罩layer.mask
  3. layer.allowsGroupOpacity属性设置为YES和layer.opacity小于1.0
  4. 为图层设置阴影(layer.shadow
    • 使用阴影必须保证 layer 的masksToBounds = false,因此阴影与系统圆角不兼容
    • 给阴影指定了路径,就不会触发离屏渲染了,例如,这样设置阴影:
    1
    2
    3
    4
    5
    6
    self.view1.layer.shadowColor = [UIColor greenColor].CGColor;
    self.view1.layer.shadowOffset = CGSizeMake(5, 5);
    self.view1.layer.shadowOpacity = 0.5;
    self.view1.layer.shadowRadius = 3;
    UIBezierPath * path = [UIBezierPath bezierPathWithRect:self.view1.bounds];
    self.view1.layer.shadowPath = path.CGPath;
  5. 使用CGContextdrawRect :方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现

离屏渲染优化

  1. 关于圆角,iOS 9及之后的系统版本,苹果进行了一些优化,只设置contents或者UIImageView的image,并加上圆角+裁剪,是不会产生离屏渲染的。但如果加上了背景色、边框或其他有图像内容的图层,还是会产生离屏渲染。

  2. 利用UIBezierPath(CoreGraphics框架)画出来圆角图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    - (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size{
    /* 当前UIImage的可见绘制区域 */
    CGRect rect = (CGRect){0.f,0.f,size};
    /* 创建基于位图的上下文 */
    UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
    /* 在当前位图上下文添加圆角绘制路径 */
    CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
    /* 当前绘制路径和原绘制路径相交得到最终裁剪绘制路径 */
    CGContextClip(UIGraphicsGetCurrentContext());
    /* 绘制 */
    [self drawInRect:rect];
    /* 取得裁剪后的image */
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    /* 关闭当前位图上下文 */
    UIGraphicsEndImageContext();
    return image;
    }
  3. YYImage对图片圆角的处理方法是值得推荐的,附上实现源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
        - (UIImage *)imageByRoundCornerRadius:(CGFloat)radius
    corners:(UIRectCorner)corners
    borderWidth:(CGFloat)borderWidth
    borderColor:(UIColor *)borderColor
    borderLineJoin:(CGLineJoin)borderLineJoin {

    if (corners != UIRectCornerAllCorners) {
    UIRectCorner tmp = 0;
    if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft;
    if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight;
    if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft;
    if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight;
    corners = tmp;
    }

    UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
    CGContextScaleCTM(context, 1, -1);
    CGContextTranslateCTM(context, 0, -rect.size.height);

    CGFloat minSize = MIN(self.size.width, self.size.height);
    if (borderWidth < minSize / 2) {
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)];
    [path closePath];

    CGContextSaveGState(context);
    [path addClip];
    CGContextDrawImage(context, rect, self.CGImage);
    CGContextRestoreGState(context);
    }

    if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) {
    CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale;
    CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
    CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0;
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, borderWidth)];
    [path closePath];

    path.lineWidth = borderWidth;
    path.lineJoinStyle = borderLineJoin;
    [borderColor setStroke];
    [path stroke];
    }

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
    }
  4. AsyncDisplayKit指定layer阴影效果路径使用异步进行layer渲染

参考文章

iOS圆角的离屏渲染,你真的弄明白了吗
iOS 离屏渲染探究