离屏渲染

[离屏渲染]

一、Off-Screen Rendering#

离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

离屏渲染的数量才是影响 app 交互性能的根源。

离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。原因主要在于创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。

二、上下文切换#

不管是在 GPU 渲染过程中,还是熟悉的进程切换,上下文切换在哪里都是一个相当耗时的操作。

首先要保存当前屏幕渲染环境,然后切换到一个新的绘制环境,申请绘制资源 -> 初始化环境 -> 开始一个绘制 -> 绘制完毕后销毁这个绘制环境,如需要切换到 On-Screen Rendering 或者再开始一个新的离屏渲染重复之前的操作。

下图描述了一次 mask 的渲染操作。

<center style="margin: 0px; padding: 0px;">
26

</center>

一次 mask 发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换,一次 mask 需要渲染三次才能在屏幕上显示,这已经是普通视图显示 3 倍耗时,若再加上下文环境切换,一次 mask 就是普通渲染的 30 倍以上耗时操作。

三、出现场景#

下面的情况或操作会引发离屏渲染:

  1. 为图层设置遮罩(layer.mask)
  2. 将图层的 layer.masksToBounds/view.clipsToBounds 属性设置为 true
  3. 将图层的 layer.allowsGroupOpacity 属性设置为 YES 和 layer.opacity < 1.0
  4. 为图层设置阴影(layer.shadow*)
  5. 为图层设置 layer.shouldRasterize = true
  6. 具有 layer.cornerRadius、layer.edgeAntialiasingMask、layer.allowsEdgeAntialiasing 的图层
  7. 文本(任何种类,包括 UILabel、CATextLayer、CoreText 等)
  8. 使用 CGContext 在 drawRect: 方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现

四、优化方案#

官方对离屏渲染产生性能问题也进行了优化:iOS 9.0 之前 UIImageView 跟 UIButton 设置圆角都会触发离屏渲染;iOS 9.0 之后 UIButton 设置圆角会触发离屏渲染,而 UIImageView 设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。

4.1 圆角优化#

在 APP 开发中圆角图片还是经常出现的。如果一个界面中只有少量圆角图片或许对性能没有非常大的影响,但是当圆角图片比较多的时候就会 APP 性能产生明显的影响。

我们设置圆角一般通过如下方式:

<pre style="margin: 10px 0px; padding: 0px; white-space: pre !important; overflow-wrap: break-word; position: relative !important;">

Copy

imageView.layer.cornerRadius = CGFloat(10); imageView.layer.masksToBounds = YES;</pre>

这样处理的渲染机制是 GPU 在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作,也就是离屏渲染,这会带来额外的性能损耗,如果这样的圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的的频繁切换,出现掉帧现象。

  1. 使用贝塞尔曲线 UIBezierPath 和 Core Graphics 框架画出一个圆角。

    <pre style="margin: 10px 0px; padding: 0px; white-space: pre !important; overflow-wrap: break-word; position: relative !important;">

    Copy

    `UIImageView * imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    UIImage * image = [UIImage imageNamed:@"myImg"];

    // 开始对 imageView 进行画图
    UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, [UIScreen mainScreen].scale);
    // 使用贝塞尔曲线画出一个圆形图
    [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
    [image drawInRect:imageView.bounds];
    imageView.image = UIGraphicsGetImageFromCurrentImageContext();
    // 结束画图
    UIGraphicsEndImageContext();

    [self.view addSubview:imageView];`</pre>

    HJCornerRadius

  2. 视图上添加一个子 layer 到最上层,用于遮盖该视图及其子视图,设置 layer 的图片为刚好能够遮盖成所需圆角样子,并且图片颜色刚好是该视图父视图的背景颜色就达到想要的效果。

    弊端:如果该父视图的颜色不是纯色,此时该方式就不适用了,同样,如果父视图的颜色会变化,那实现起来的代码也不那么优雅。

    <center style="margin: 0px; padding: 0px;">
    [图片上传失败...(image-fc9a5a-1583574576970)]
    </center>

  3. 通过修改 layer.mask,首先通过贝塞尔曲线创建基于矢量的路径,传递给 CAShapeLayer 进行渲染。路径闭环,再把绘制出的 Shape 赋值给 layer.mask,在 Mask 范围之外的 Layer 将不被显示从而达到圆角效果。代码实现很简单,如下:

    <pre style="margin: 10px 0px; padding: 0px; white-space: pre !important; overflow-wrap: break-word; position: relative !important;">

    Copy

    UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(130, 330, 100, 100)]; [btn setBackgroundColor:[UIColor colorWithRed:(226.0 / 255.0) green:(113.0 / 255.0) blue:(19.0 / 255.0) alpha:1]]; [backgroundImageView addSubview:btn]; //绘制曲线路径 UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:btn.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:btn.bounds.size]; CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init]; //设置大小 maskLayer.frame = btn.bounds; //设置图形样子 maskLayer.path = maskPath.CGPath; btn.layer.mask = maskLayer;</pre>

    <center style="margin: 0px; padding: 0px;">
    [图片上传失败...(image-54fb3-1583574576970)]
    </center>

4.2 shadow 优化#

对于 shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置 shadowPath 来优化性能,能大幅提高性能。示例如下:

<pre style="margin: 10px 0px; padding: 0px; white-space: pre !important; overflow-wrap: break-word; position: relative !important;">

Copy

imageView.layer.shadowColor = [UIColor grayColor].CGColor; imageView.layer.shadowOpacity = 1.0; imageView.layer.shadowRadius = 2.0; UIBezierPath * path = [UIBezierPath bezierPathWithRect:imageView.frame]; imageView.layer.shadowPath = path.CGPath;</pre>

当使用阴影的视图形状发生变化时,即 shadowPath 并不会跟随 CALayer 的 bounds 属性进行变化,所以在 layer 的 bounds 产生变化以后需要手动更新 shadowPath 才能让其适配新的 bounds。具体推荐看这篇文章

我们还可以通过设置 shouldRasterize 属性值为 YES 来强制开启离屏渲染。其实就是光栅化(Rasterization)。既然离屏渲染这么不好,为什么我们还要强制开启呢?当一个图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能。当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。但是如果图层发生改变的时候就会重新产生位图缓存。所以这个功能一般不能用于 UITableViewCell 中,cell 的复用反而降低了性能。最好用于图层较多的静态内容的图形。而且产生的位图缓存的大小是有限制的,一般是 2.5 个屏幕尺寸。在 100ms 之内不使用这个缓存,缓存也会被删除。所以我们要根据使用场景而定。

4.3 其他的一些优化建议#

  1. 当我们需要圆角效果时,可以使用一张中间透明图片蒙上去
  2. 使用 ShadowPath 指定 layer 阴影效果路径
  3. 使用异步进行 layer 渲染(Facebook 开源的异步绘制框架 AsyncDisplayKit)
  4. 设置 layer 的 opaque 值为 YES,减少复杂图层合成
  5. 尽量使用不包含透明(alpha)通道的图片资源
  6. 尽量设置 layer 的大小值为整形值
  7. 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
  8. 很多情况下用户上传图片进行显示,可以让服务端处理圆角
  9. 使用代码手动生成圆角 Image 设置到要显示的 View 上,利用 UIBezierPath(CoreGraphics框架)画出来圆角图片

4.4 Core Animation 工具检测离屏渲染#

对于离屏渲染的检测,苹果为我们提供了一个测试工具Core Animation。可以在Xcode->Open Develeper Tools->Instruments中找到,如下图:

<center style="margin: 0px; padding: 0px;">
image

</center>

Core Animation工具用来监测Core Animation性能,提供可见的FPS值,并且提供几个选项来测量渲染性能。如下图:

<center style="margin: 0px; padding: 0px;">
image

</center>

下面我们来说明每个选项的功能:

  • Color Blended Layers:这个选项如果勾选,你能看到哪个 layer 是透明的,GPU 正在做混合计算。显示红色的就是透明的,绿色就是不透明的。

  • Color Hits Green and Misses Red:如果勾选这个选项,且当我们代码中有设置shouldRasterize为YES,那么红色代表没有复用离屏渲染的缓存,绿色则表示复用了缓存。我们当然希望能够复用。

  • Color Copied Images:按照官方的说法,当图片的颜色格式GPU不支持的时候,Core Animation 会拷贝一份数据让 CPU 进行转化。例如从网络上下载了 TIFF 格式的图片,则需要 CPU 进行转化,这个区域会显示成蓝色。还有一种情况会触发Core Animation的copy方法,就是字节不对齐的时候。

  • Color Immediately:默认情况下 Core Animation 工具以每毫秒 10 次的频率更新图层调试颜色,如果勾选这个选项则移除 10ms 的延迟。对某些情况需要这样,但是有可能影响正常帧数的测试。

  • Color Misaligned Images:勾选此项,如果图片需要缩放则标记为黄色,如果没有像素对齐则标记为紫色。像素对齐我们已经在上面有所介绍。

  • Color Offscreen-Rendered Yellow:用来检测离屏渲染的,如果显示黄色,表示有离屏渲染。当然还要结合 Color Hits Green and Misses Red 来看,是否复用了缓存。

  • Color OpenGL Fast Path Blue:这个选项对那些使用 OpenGL 的图层才有用,像是 GLKView 或者 CAEAGLLayer,如果不显示蓝色则表示使用了 CPU 渲染,绘制在了屏幕外,显示蓝色表示正常。

  • Flash Updated Regions:当对图层重绘的时候回显示黄色,如果频繁发生则会影响性能。可以用增加缓存来增强性能。

五、UITableView 优化#

  1. 使用tableView的复用机制

    作用:减少内存资源的消耗。

    注意:cell被重用时,它内部绘制的内容并不会被自动清除,因此你可能需要调用setNeedsDisplayInRect: 或 setNeedsDisplay 方法。

  2. 提前预估高度

    提前计算并缓存好高度(布局),因为 heightForRowAtIndexPath: 是调用最频繁的方法。

  3. cell 内部有图片

    此时需要异步加载图片,防止卡顿(此时的 SDWebImage 的每个 cell 中都创建一个子线程吗?)但是内部开启的线程过多也会影响主线程的性能

    解决办法:

    • 在 scrollerView 的代理方法中,didEndDragging,didEndDeceleratiing 方法中,才开始异步加载,其它时刻不进行加载
    • 在 didEndDragging,didEndDeceleratiing 方法中实现方法:获取屏幕上显示出来的 cell 的 indexPath 数组,然后通过遍历 indexPath 的数组,在数据源中,如果已经加载了,就不需要再异步加载,反之进行异步加载,然后再cellForRow方法中也进行一次判断:当self.tableView.dragging == NO && self.tableView.decelerating == NO 的时候执行异步加载图片的方法
  4. 尽量少用 addView 给 Cell 动态添加 View

    可以初始化时就添加,相对于一些固定的视图在初始化时就布局好,学会用 hidden 属性来控制是否显示。

  5. 减少子视图的数目

    当 cell 上面的子视图数量过多时,会影响滑动性能,当子视图太多的时候,对适当的视图进行绘制。

  6. 使用不透明视图

    不透明的视图可以极大地提高渲染的速度。因此如非必要,可以将 table cell 及其子视图的 opaque 属性设为 YES(默认值)。

  7. 预渲染图像和离屏渲染

    你会发现即使做到了上述几点,当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是预渲染图像,在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕,详细做法可见《利用预渲染加速iOS设备的图像显示》

    离屏渲染就是在 tableView 中展示多张需要切圆形的图片,此时不要使用 setCornerRadius 的方法,这样耗损性能,用 Core Graphics 绘制圆角,然后返回图片,在 SDWebImage 处理我的分类返回的图片,并进行缓存。

  8. UIImage:本地图片加载方式本地图片加载常用方法有两种:

    • [UIImage imageNamed:@”xx.png”] 图片多次使用时使用,需要使用此方式加入缓存。
    • [[UIImage alloc] initWithContentsOfFile:@”xx.png”] 图片不常使用时,不使用缓存。
  9. 避免对象创建时过多消耗资源

    例如:日期处理,将保持日期对象全局唯一。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • 一、概述 OpenGL ES是一套多功能开放标准的用于嵌入系统的C-based的图形库,用于2D和3D数据的可视化...
    半岛夏天阅读 288评论 0 2
  • 屏幕显示图像的原理: 高中物理应该学过显示器是如何显示图像的:需要显示的图像经过CRT电子枪以极快的速度一行一行的...
    青火阅读 27,415评论 18 104
  • 目录 离屏渲染的本质如何设置圆角(三种方法)Shadow 阴影MaskGroupOpacityEdgeAntial...
    路飞_Luck阅读 1,927评论 0 9
  • 相比于当前屏幕渲染,离屏渲染的代价是很高的,这也是iOS移动端优化的必要部分。 OpenGL中,GPU屏幕渲染有以...
    一个人在路上走下去阅读 8,838评论 0 74
  • 屏幕渲染的原理: 需要显示的图像经过CRT电子枪以极快的速度一行一行的扫描,扫描出来就呈现了一帧画面,随后电子枪又...
    一条鱼的星辰大海阅读 553评论 1 4