正在加载今日诗词....
June 10, 2018

iOS离屏渲染

为了让 UI 显得更好看 应用中有非常多的地方会使用的圆角的图片 但是一般的系统 layer 圆角设置方式通常会导致严重的离屏渲染问题, 尤其是在列表中 本文主要是对seedante的解决方案总结

iOS离屏渲染

阴影离屏渲染示例

img

来自 Demystify and eliminate hitches in the render phase

文章内容主要来自 seedante

1.什么是离屏渲染

谈离屏渲染之前, 首先要明白什么是屏内渲染.

App -> GPU(frame buffer) -> 屏幕

Frame Buffer

Framebuffer 是 RAM 的一部分,包含一份能驱动视图显示的位图. frame buffer 由 GPU 掌管.

obj 中国有一篇文章专门提到了离屏渲染的问题,文章中提到 直接将图层合成到当前显示屏幕的帧缓冲区中,比先在屏幕外面创建新的缓冲区,然后渲染到纹理中,最后将结果渲染到当前显示屏幕的帧缓冲区中,性能要好的多

性能损耗的点:

    1. 创建屏幕外的帧缓冲区
    1. 在 屏幕外的帧缓冲区 切换到 屏幕内的帧缓冲区 (也就是将offscreen rendering 结果 挪到 onscreen 缓冲区内)
    1. GPU 额外工作; 关于iOS离屏渲染的深入研究 GPU的操作是高度流水线化的。本来所有计算工作都在有条不紊地正在向frame buffer输出,此时突然收到指令,需要输出到另一块内存,那么流水线中正在进行的一切都不得不被丢弃,切换到只能服务于我们当前的“切圆角”操作。等到完成以后再次清空,再回到向frame buffer输出的正常流程。
    • 触发屏幕渲染后 这种情况发生在 每一帧 ,界面如果在滚动过程中 有大量的离屏渲染发生会严重影响帧率

其他

  • GPU 渲染有硬件的额外加速, GPU 更擅长处理图片, CPU 擅长计算 ,所以做渲染操作优先GPU处理
  • 其实通过CPU渲染就是俗称的“软件渲染”,而真正的离屏渲染发生在GPU 关于iOS离屏渲染的深入研究

为什么会某些原因会导致离屏渲染, 开启屏幕外缓冲区, 这里不是说 什么能导致, 而是为什么会导致 ?

iOS核心动画高级技巧中有表示: 当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU).
上面的未预合成 ?
猜测有 圆角, 阴影, group opacity , mask, blurEffect等效果 都无法直接生成.

最简单的一个解释阴影 (摘自 关于iOS离屏渲染的深入研究)

shadow,其原因在于,虽然layer本身是一块矩形区域,但是阴影默认是作用在其中”非透明区域“的,而且需要显示在所有layer内容的下方,因此根据画家算法必须被渲染在先。但矛盾在于此时阴影的本体(layer和其子layer)都还没有被组合到一起,怎么可能在第一步就画出只有完成最后一步之后才能知道的形状呢?这样一来又只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到frame buffer,最后把内容画上去(这只是我的猜测,实际情况可能更复杂)。不过如果我们能够预先告诉CoreAnimation(通过shadowPath属性)阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了。

关于圆角的画画比喻,
按照图层树 从父 layer 便利 子layer 的方式处理, 一层一层的绘制到显卡 缓冲区上, 绘制后 原来那层的 layer数据 比如 圆角范围, alpha 都不能访问了, 下一层 layer也就不知道 我自己的内容有没有超出 父视图的圆角区域 ; 所以如果父视图有 圆角, 会开辟缓冲区, 不关心父视图的圆角, 先将视图最终效果画完 再设置圆角.

就是画到缓冲图的 父视图仅仅是 最终的像素数据, 子视图再绘制的时候, 能看到的只是缓冲区最终的 像素而已

2. 导致离屏渲染的行为

  • GPU版本的离屏渲染 ->
    更改 mask,shadow,Group opacity,edge antialiasing

  • CPU 版本的离屏渲染 ->
    使用 Core Graphic 里面的绘制 API 也会触发离屏渲染 ,比如 drawRect:

  • 系统圆角的离屏渲染

view.layer.cornerRadius = aRadius;
view.layer.maskToBounds = true 

这里需要注意的是, 仅仅只是设置 layer.cornerRadius 不会有离屏渲染问题, 但是要是和maskToBounds一起配合layer.cornerRadius使用就会有问题.

如图
off-screen-render

优化是使用CoreGraphic圆角绘制与缓存(memory,disk cache) 配合使用
比如下面:

on-screen-render

3. 圆角解决方案

  • 异步绘制圆角 圆角 code gist
  • 本地图片 让 UI 提供圆角图片
  • 使用遮罩 ,边缘让背景色 和 周边颜色 保持一致 ;中间为透明色

视情况选择以上其中一种!!!

4. 本文中涉及的基础知识

4.1 UIView 与 CALayer 的关系

  • CALayer 负责显示内容 contents
  • UIView 负责为其提供内容,以及负责处理触摸事件,参与响应链
  • CALayers have their own background , border and contents
  • contents 必须为一个 CGImage 才能显示

4.2 UIImageView

  • UIImage 是对CGImage 的一个轻量的封装 (UIImage is a lightweight wrapper around CGImage )
  • CALayer also has CGImage as contents
  • CGImage backed by file or data,eventually by bitmap

4.3 RoundedCorner

  • 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.

只对前景框和背景色起作用 , 很重要
如果仅仅只是让 Layer的背景色 设置圆角, 不处理 content 的内容是不会有离屏渲染的

4.4 重绘方案的优化

  • 将图像重新绘制为圆角图像相当于多了一份,要不要缓存?
  • A 第一次重绘后将这些圆角图像缓存在磁盘里,第二次加载直接使用缓存的圆角图像;
  • B 直接保存在内存里,在内存比较吃紧时显然不是个好选择
  • C 不缓存,和系统圆角一样,每次都重绘,浪费电量

4.5 文本视图类上实现圆角

  • UTextField 自带圆角效果

  • UILabel UITextView 首先保证contents 呈现透明的背景色 ,只需要设置 layer 的 backgroundColor ,再加上 cornerRadius 就 OK

  • 阴影设置 shadowPath ,默认为 nil

其他文章的看法

我个人觉得@已惊雷的说法有部分道理,但不是主要的原因,在WWDC2014上面的Session https://developer.apple.com/videos/play/wwdc2014/419/ 讲过做mask操作确实会比普通的rendering多一些步骤,所以会增加Draw Call的数量。更重要的是Andy Matuschak(UIKit早期成员)在一篇回复中说的:It’s expensive for the GPU to switch contexts from on-screen to off-screen drawing (it must flush its pipelines and barrier), so for simple drawing operations, the setup cost may be greater than the total cost of doing the drawing in CPU via e.g. CoreGraphics would have been. 最耗费性能的是flush its pipelines and barrier,就如在OpenGL中实际调用Draw Call只是将命令放入Command Buffer队列,真正执行是当你调用glFlush一样。所以我觉得离屏渲染导致卡顿的主要原因是有更多的glFlush操作 摘自 OpenGL ES中的offscreen rendering与iOS中的offscreening rendering

示例