收藏iosiOS 精品摘录

iOS离屏渲染优化

2017-09-02  本文已影响0人  路飞_Luck
目录
  • 离屏渲染的本质
  • 如何设置圆角(三种方法)
  • Shadow 阴影
  • Mask
  • GroupOpacity
  • EdgeAntialiasing
  • Rasterization
  • 造成离屏渲染的原因
  • 总结
离屏渲染(Offscreen Render)

绘制像素到屏幕上 应该是国内对离屏渲染这个概念推广力度最大的一篇文章了。文章里提到直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。触发离屏渲染后这种转换发生在每一帧,在界面的滚动过程中如果有大量的离屏渲染发生时会严重影响帧率。

苹果官方公开的的资料里关于离屏渲染的信息最早是在 2011年的 WWDC, 在多个 session 里都提到了尽量避免会触发离屏渲染的效果,包括:mask, shadow, group opacity, edge antialiasing

使用 Core Graphics 里的绘制 API 也会触发离屏渲染,比如重写 drawRect:。在 WWDC 2011: Understanding UIKit Rendering 这个 session 里演示了Core Animation Instruments里使用Color Offscreen-Renderd Yellow选项来检测离屏渲染

Designing for iOS: Graphics & Performance这篇文章也提到了使用 Core Graphics API 会触发离屏渲染,随后苹果对这个观点进行了回复,主要意思是:Core Graphics 的绘制 API 的确会触发离屏渲染,但不是那种 GPU 的离屏渲染。使用 Core Graphics 绘制 API 是在 CPU 上执行,触发的是 CPU 版本的离屏渲染。

本文以Color Offscreen-Renderd Yellow为触发离屏渲染的标准,除非还有这个标准无法检测出来的引发离屏渲染的行为。那么 Core Graphics API 是不会触发离屏渲染的,比如重写drawRect:,而除了以上四种效果会触发离屏渲染,使用系统提供的圆角效果也会触发离屏渲染,比如这样:

view.layer.cornerRadius = 5
view.layer.masksToBounds = true
开始之前,先铺垫一点基础的东西。
UIView 和 CALayer 的关系

The Relationship Between Layers and Views的解释很细致但是太啰嗦,简单来说,UIView 是对 CALayer 的一个封装。

Graphics and Animations.png

CALayer 负责显示内容contents,UIView 为其提供内容,以及负责处理触摸等事件,参与响应链。CALayer 的结构如下.出自 Layers Have Their Own Background and Border

CALayer构成.jpg

CALayer 有三个视觉元素,中间的contents属性是这样声明的

var contents: AnyObject?

实际上它必须是一个CGImage才能显示。

当使用let view = UIView(frame: CGRectMake(0, 0, 200, 200))生成一个视图对象并添加到屏幕上时,从 CALayer 的结构可以知道,这个视图的 layer 的三个视觉元素是这样的:contents为空,背景颜色为空(透明色),前景框宽度为0的前景框,这个视图从视觉上看什么都看不到。CALayer 文档第一句话就是:The CALayer class manages image-based content and allows you to perform animations on that content.UIView 的显示内容很大程度上就是一张图片(CGImage)

UIImageView

既然直接对 CALayer 的contents属性赋值一个CGImage便能显示图片,所以 UIImageView 就顺利成章地诞生了。实际上 UIImage 就是对 CGImage(或者 CIImage) 的一个轻量封装。记得我刚接触 iOS 时,搞不懂这两者的区别,有人这样对我说过,没想到出处是这里:

1464163152150338.jpg

UIKit 和 Core Graphics 框架的联系很紧密,UIKit 里带CG前缀属性的类基本上是对应 Core Graphics 框架里的对象的封装,UIKit 里的绘制功能也是 Core Graphics 绘制 API 的封装。Drawing with Quartz and UIKit列举了这些对应关系。界面的内容主要是图像和文字,文字是怎么显示的?也是使用 Core Graphics 框架绘制出来的。

接下来,正式开始本文的话题。
一 设置圆角

RoundedCorner

设置圆角:

view.layer.cornerRadius = 5

文档中cornerRadius属性的说明:

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to YES causes the content to be clipped to the rounded corners.

很明了,只对前景框和背景色起作用,再看 CALayer 的结构,如果contents有内容或者内容的背景不是透明的话,还需要把这部分弄个角出来,不然合成的结果还是没有圆角,所以才要修改masksToBounds为true(在 UIView 上对应的属性是clipsToBounds,在 IB 里对应的设置是「Clip Subiews」选项)。前些日子很热闹的圆角优化文章中的2篇指出是修改masksToBounds为true而非修改cornerRadius才是触发离屏渲染的原因,但如果以「Color Offscreen-Renderd Yellow」的特征为标准的话,这两个属性单独作用时都不是引发离屏渲染的原因,他俩合体(masksToBounds = true, cornerRadius>0)才是。

方案1 重绘

重绘的方式有多种,都是殊途同归。实际中重绘圆角的优化方案需要考虑的是,将图像重新绘制为为圆角图像相当于多了一份拷贝,要不要缓存?A.第一次重绘后将这些圆角图像缓存在磁盘里,第二次加载直接使用缓存的圆角图像;B.直接保存在内存里,在内存比较吃紧时显然不是个好选择;C.不缓存,和系统圆角一样,每次都重绘,浪费电量。

方案2

如果不需要对外部来源的图片做圆角,由设计师直接画成圆角图片是最方便的;

方案3 混合图层

在要添加圆角的视图上再叠加一个部分透明的视图,只对圆角部分进行遮挡。VVebo微博客户端就是这样做的,遮挡的部分背景最好与周围背景相同。多一个图层会增加合成的工作量,但这点工作量与离屏渲染相比微不足道,性能上无论各方面都和无效果持平。下面左侧的图像是 VVebo 里用来制造圆形头像的 mask 图像,实际中有这种需求的基本是制造圆形头像,普通的圆角遮罩需要左二这种,左三是通用型。如果叠加的视图都一样,可以只加载一次遮罩图片以减少内存占用。

- (void)drawRoundedCornerImage {
    UIImageView *iconImgV = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    iconImgV.image = [UIImage imageNamed:@"icon"];
    [self.view addSubview:iconImgV];
    
    [iconImgV mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(iconImgV.size);
        make.top.equalTo(self.view.mas_top).offset(500);
        make.centerX.equalTo(self.view);
    }];
    
    
    UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    [self.view addSubview:imgView];
    
    [imgView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(imgView.size);
        make.top.equalTo(iconImgV.mas_top);
        make.leading.equalTo(iconImgV.mas_leading);
    }];
    
//    imgView.image = [self useUIGraphicsDrawAntiRoundedCornerImageWithRadius:10 outerSize:CGSizeMake(150, 150) innerSize:CGSizeMake(100, 100) fillColor:[UIColor whiteColor]];
    
    // 圆形
    imgView.image = [self drawCircleRadius:100 outerSize:CGSizeMake(200, 200) fillColor:[UIColor whiteColor]];
}

// 绘制圆形
- (UIImage *)drawCircleRadius:(float)radius outerSize:(CGSize)outerSize fillColor:(UIColor *)fillColor {
    UIGraphicsBeginImageContextWithOptions(outerSize, false, [UIScreen mainScreen].scale);
    
    // 1、获取当前上下文
    CGContextRef contextRef = UIGraphicsGetCurrentContext();
    
    //2.描述路径
    // ArcCenter:中心点 radius:半径 startAngle起始角度 endAngle结束角度 clockwise:是否逆时针
    UIBezierPath *bezierPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(outerSize.width * 0.5, outerSize.height * 0.5) radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:NO];
    [bezierPath closePath];
    
    // 3.外边
    [bezierPath moveToPoint:CGPointMake(0, 0)];
    [bezierPath addLineToPoint:CGPointMake(outerSize.width, 0)];
    [bezierPath addLineToPoint:CGPointMake(outerSize.width, outerSize.height)];
    [bezierPath addLineToPoint:CGPointMake(0, outerSize.height)];
    [bezierPath addLineToPoint:CGPointMake(0, 0)];
    [bezierPath closePath];
    
    //4.设置颜色
    [fillColor setFill];
    [bezierPath fill];
    
    CGContextDrawPath(contextRef, kCGPathStroke);
    UIImage *antiRoundedCornerImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return antiRoundedCornerImage;
}

效果如下

image.png

布局如下图所示


image.png
如何在文本视图类上实现圆角

文本视图主要是这三类:UILabel, UITextField, UITextView。其中 UITextField 类自带圆角风格的外型,UILabel 和 UITextView 要想显示圆角需要表现出与周围不同的背景色才行。想要在 UILabel 和 UITextView 上实现低成本的圆角(不触发离屏渲染),需要保证 layer 的contents呈现透明的背景色,文本视图类的 layer 的contents默认是透明的(字符就在这个透明的环境里绘制、显示),此时只需要设置 layer 的backgroundColor,再加上cornerRadius就可以搞定了。不过 UILabel 上设置backgroundColor的行为被更改了,不再是设定 layer 的背景色而是为contents设置背景色,UITextView 则没有改变这一点,实例代码如下

- (void)drawUI {
    
    UILabel *radiusLbe = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 44)];
    radiusLbe.textColor = [UIColor blackColor];
    radiusLbe.text = @"裁剪圆角";
    radiusLbe.textAlignment = NSTextAlignmentCenter;
    // a裁剪圆角
    radiusLbe.layer.backgroundColor = [[UIColor orangeColor] CGColor];
    radiusLbe.layer.cornerRadius = 10;
    [self.view addSubview:radiusLbe];
}

效果如下


image.png
- (void)drawTextF {
    UITextField *textF = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 200, 44)];
    textF.textColor = [UIColor blackColor];
    textF.text = @"裁剪圆角";
    textF.textAlignment = NSTextAlignmentCenter;
    // a裁剪圆角
    textF.layer.backgroundColor = [[UIColor greenColor] CGColor];
    textF.layer.cornerRadius = 10;
    [self.view addSubview:textF];
}

运行效果如下


image.png
- (void)drawTextV {
    UITextView *textV = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, 20, 200)];
    textV.textColor = [UIColor whiteColor];
    textV.text = @"如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n如何在UITextView视图上实现圆角?\n";
    textV.textAlignment = NSTextAlignmentCenter;
    // a裁剪圆角
    textV.layer.backgroundColor = [[UIColor blueColor] CGColor];
    textV.layer.cornerRadius = 10;
    [self.view addSubview:textV];
}

运行效果如下:

1.gif
二 Shadow 阴影

阴影直接合成在视图的下面,视图结构里并没有多出一个视图。在没有指定阴影路径时,阴影是沿着视图的非透明部分扩展的,而且 CALayer 的三个视觉元素至少有一个存在时才会有阴影。

使用阴影必须保证 layermasksToBounds = false,因此阴影与系统圆角不兼容。但是注意,只是在视觉上看不到,对性能的影响依然。通常这样实现一个阴影:

- (void)drawUI {
    UIImageView *iconImgV = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    iconImgV.image = [UIImage imageNamed:@"icon_girl"];
    [self.view addSubview:iconImgV];
    
    [iconImgV mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(iconImgV.size);
        make.centerY.equalTo(self.view);
        make.centerX.equalTo(self.view);
    }];
    
    // 设置阴影
    CALayer *imageViewLayer = iconImgV.layer;
    imageViewLayer.shadowColor = [[UIColor blackColor] CGColor];
    imageViewLayer.shadowOpacity = 1.0; //此参数默认为0,即阴影不显示
    imageViewLayer.shadowRadius = 2.0; //给阴影加上圆角,对性能无明显影响
    imageViewLayer.shadowOffset = CGSizeMake(5, 5);
    //设定路径:与视图的边界相同
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:iconImgV.bounds];
    imageViewLayer.shadowPath = path.CGPath;//路径默认为 nil
}

运行效果如下

image.png
imageViewLayer.cornerRadius = 10;
imageViewLayer.masksToBounds = YES;

运行效果如下:

image.png

在 OffscreenRenderDemo 里,仅开启阴影(没有指定路径,同屏数量10个以上)在滚动时帧率会大幅下降,检测到离屏渲染的黄色特征;指定一个与边界相同的简单路径后离屏渲染特征消失,帧率恢复正常。

下面我们通过代码验证

// 设置阴影
CALayer *imageViewLayer = imgView.layer;
imageViewLayer.shadowColor = [[UIColor blackColor] CGColor];
imageViewLayer.shadowOpacity = 1.0; //此参数默认为0,即阴影不显示
imageViewLayer.shadowRadius = 2.0; //给阴影加上圆角,对性能无明显影响
imageViewLayer.shadowOffset = CGSizeMake(5, 5);

运行效果: 开启离屏渲染监视:模拟器 -> Debug -> Color off-screen rendered

1.gif

FPS 测试结果


image.png

分析:通过运行结果效果图,我们可以发现,在图片阴影处有黄色,说明该处发生了离屏渲染。而且 GPU 使用率较高,会造成卡顿掉帧。

//设定路径:与视图的边界相同
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imgView.bounds];
imageViewLayer.shadowPath = path.CGPath;//路径默认为 nil

运行效果: 开启离屏渲染监视:模拟器 -> Debug -> Color off-screen rendered

1.gif

FPS 测试结果


image.png

分析:通过运行结果效果图,我们可以发现,在图片阴影处没有黄色,说明该处没有发生离屏渲染。而且 GPU 使用率不是很高。

除了指定路径,实现良好性能阴影的方法还有:用圆角优化里混合图层的方法模拟阴影的效果:放一个同样效果的视图在要添加阴影程度的视图的下方;使用 Core Graphics 绘制阴影,不过除非万不得已没人想碰 Core Graphics API。从实现成本来讲,都不如指定路径方便。

三 Mask

Mask 效果与混合图层的效果非常相似,只是使用同一个遮罩图像时,mask 与混合图层的效果是相反的,在 Demo 里使用反向内容的遮罩来实现圆角。实现 mask 效果使用 CALayer 的layer属性,在 iOS 8 以上可以使用 UIView 的maskView属性。

- (void)drawUI {
    UIImageView *iconImgV = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 80, 80)];
    iconImgV.image = [UIImage imageNamed:@"icon_girl"];
    [self.view addSubview:iconImgV];
    
    [iconImgV mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(iconImgV.size);
        make.centerY.equalTo(self.view);
        make.centerX.equalTo(self.view);
    }];
    
    // 设置Mask
    if (@available(iOS 8.0, *)) {
        iconImgV.maskView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"RoundMask"]];
    } else {
        CALayer *maskLayer = [[CALayer alloc] init];
        maskLayer.frame = iconImgV.bounds;
        maskLayer.contents = (__bridge id _Nullable)([[UIImage imageNamed:@"RoundMask"] CGImage]);
        iconImgV.layer.mask = maskLayer;
    }
}

运行效果如下:

image.png

备注:其中icon_girl.pngRoundMask分别为下面两张图片

image.png

如果所有 maskImage 相同的话,使用一个 maskImage 就够了,不然每次生成一个新的 UIImage 也会是一个性能隐患点。注意:可以使用同一个 maskImage,但不能使用同一个 maskView,不然同时只会有一个 mask 效果。

在实战项目中检验

1.gif image.png

通过检测,FPS 有些掉帧,GPU 使用率较高。

1.gif

在圆形图片区域我们观察到有黄色图形,说明发生了离屏渲染。

总结:Mask 效果无法取消离屏渲染,使用混合图层的方法来模拟 mask 效果,性能各方面都是和无效果持平。

使用 mask 来实现圆角时也可以不用图片,而使用 CAShapeLayer 来指定混合的路径。

UIBezierPath *roundedRectPath = [UIBezierPath bezierPathWithRoundedRect:iconImgV.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(40, 40)];
CAShapeLayer *shapLayer = [[CAShapeLayer alloc] init];
shapLayer.path = roundedRectPath.CGPath;
iconImgV.layer.mask = shapLayer;

运行效果如下:

image.png

我们通过设置cornerRadii值来设置角度值,如果为圆角,则为图片尺寸宽高的一半即可。

总结:同样的 mask 效果使用 CAShapeLayer 时相比直接使用 maskImage 在帧率上稍低,CPU 利用率无明显变化,但是 GPU 利用率也低一些。

四 GroupOpacity

首先来看看 GroupOpacity 是什么效果:

1464163639631478.png

GroupOpacity 是指 CALayer 的allowsGroupOpacity属性,UIView 的alpha属性等同于 CALayer opacity属性。开启 GroupOpacity 后,子 layer 在视觉上的透明度的上限是其父 layer 的opacity

从 iOS 7 以后默认全局开启了这个功能,这样做是为了让子视图与其容器视图保持同样的透明度。

GroupOpacity 开启离屏渲染的条件是:layer.opacity != 1.0并且有子 layer 或者背景图。

- (void)drawRedView {
    UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 50)];
    redView.alpha = 0.5;
    redView.backgroundColor = [UIColor grayColor];
    redView.layer.allowsGroupOpacity = NO;
    [self.view addSubview:redView];
    
    [redView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(redView.size);
        make.top.equalTo(self.view.mas_top).offset(100);
        make.centerX.equalTo(self.view);
    }];
    
    // 子视图
    UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 150, 30)];
    subView.backgroundColor = [UIColor whiteColor];
    [redView addSubview:subView];
    
    [subView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(subView.size);
        make.centerX.centerY.equalTo(redView);
    }];
}

运行效果如下:

image.png

图一是关闭了GroupOpacity属性,图二开启了GroupOpacity属性(默认开启的)

这个触发条件并不需要subLayer.opacity != 1.0,非常容易满足。然而在 TableView 这样的视图里设置 cell 或 cell.contentView 的alpha属性小于1并不能检测离屏渲染的黄色特征,性能上也没有明显差别。经过摸索发现:只有设置 tableView 的alpha小于1时才会触发离屏渲染,对性能无明显影响;设置 cell 的alpha属性并不会对整体的透明度产生影响,只有设置 cell.contentView 才有效。

五 EdgeAntialiasing

经过测试,开启 edge antialiasing(旋转视图并且设置layer.allowsEdgeAntialiasing = true) 在 iOS 8 和 iOS 9 上并不会触发离屏渲染,对性能也没有什么影响,也许到现在这个功能已经被优化了。

// EdgeAntialiasing
redView.layer.allowsEdgeAntialiasing = YES;
六 Rasterization

除了 GroupOpacityEdgeAntialiasing,其他效果触发的离屏渲染都会对性能产生严重影响,离屏渲染真的是一无是处吗?不,离屏渲染本来是个优化设计。如何物尽其用?答案是:Rasterization

cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = cell.layer.contentsScale

shouldRasterize = false时,离屏渲染的黄色特征仅限于上述自动触发离屏渲染的效果的部分,shouldRasterize = true后该部分和开启了该属性的 layer 整体(在这里就是 cell 整体)都有黄色特征,所以开启 Rasterization 是手动启动了离屏渲染。

从前面来看,离屏渲染会给 GPU 带来沉重的负担,强制启动岂不是更糟?开启 Rasterization 后,GPU 只合成一次内容,然后复用合成的结果;合成的内容超过 100ms 没有使用会从缓存里移除,在更新内容时还会产生更多的离屏渲染。对于内容不发生变化的视图,原本拖后腿的离屏渲染就成为了助力;如果视图内容是动态变化的,使用这个方案有可能让性能变得更糟。

Core Animation Instruments 有个Color Hits Green and Misses Red的选项,开启 Rasterization 后开启这个选项,屏幕上绿色的部分表示有渲染缓存可用,红色的部分表示无渲染缓存可用。

七 总结
7.1 造成离屏渲染的原因有
7.2 特别注意

以上效果在同等数量的规模下,对性能的影响等级:Shadow > RoundedCorner > Mask > GroupOpacity(迷之效果)。

任何时候优先考虑避免触发离屏渲染,无法避免时优化方案有两种:

本文参考
iOS离屏渲染优化
绘制像素到屏幕上

上一篇 下一篇

猜你喜欢

热点阅读