iOS 离屏渲染分析/优化

开始前的提问:
1.离屏渲染是什么?
2.离屏渲染在哪一步进行的?
3.离屏渲染的影响在哪?
4.设置圆角一定会触发离屏渲染吗?
5.如何优化离屏渲染?

深入理解了上面几个问题足以回答面试官的问题。

iOS中图像渲染流程

UIKit其实就是CoreGraphicsCoreAnimation的高度集成。
我们通过UIKit实现可视化的控件布局其实就是CoreGraphics绘制图层,但是它显示的部分和它的动画其实是CoreAnimation来完成。

CoreGraphics它用来处理在运行前创建的图像。比如说在工程中导入的图片/资源文件。它可以对现成的文件进行高效的处理,既可以在CPU也可以在GPU上执行

CoreAnimation用来做核心动画,图形图像显示。
CoreImage做一些滤镜处理操作

图像渲染框架.png

在Application阶段,用CPU处理。CPU创建视图;计算视图的frame;进行图片解码、绘制纹理等等。再交由我们的GPU。
再由顶点着色器去确定图形在我们硬件上的具体显示位置。
进行图元装配,光栅化。
由片元着色器去确定计算每一个像素点的颜色值。
会找到对应像素点的范围,把每一个像素点的颜色显示上去,然后再放入我们的帧缓存区里frameBuffer。
最后由显示系统将帧缓存区里的数据显示出来。

图像渲染流程..png
渲染路线.png

视频控制器是怎么把帧缓存区的数据显示出来的?
通过逐行扫描的方式确定一帧画面的显示。再回到初始的点逐行扫描确定下一帧的图像。

我们的显示器通常都是固定的形式刷新的。像我们苹果手机刷新频率每秒60Hz。我们做屏幕优化的时候都是以fps作为指标。那么它的值越接近60说明屏幕流畅度越好

image.png

当VSync垂直同步信号的时候,GPU它还没有把这一帧的数据放入frameBuffer,我们这一个画面帧就会被丢失。等待下一个垂直同步信号过来的时候,再来显示我们前面的内容。

丢帧原因.png

离屏渲染是什么?

如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frameBuffer(帧缓存区)作为像素数据存储区域,然后由显示器把帧缓存区的数据显示到屏幕上。
如果有时因为面临一些限制,比如说阴影/遮罩等等,GPU无法吧渲染结果直接写入frameBuffer,而是先暂时把中间的一个临时状态存入另外新开辟的内存区域,之后再写入frameBuffer,这个过程被称之为离屏渲染

离屏渲染产生.png

当视图层级结构比较复杂的时候,就需要开辟临时内存区域。

离屏渲染会有什么影响呢?

GPU计算出的复杂的渲染图层,它会开辟一个临时内存区域;
离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
由于垂直同步的机制,如果在一个 HSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

既然离屏渲染这么耗性能,为什么有这套机制呢?

有些效果被认为不能直接呈现于屏幕,而需要在别的地方做额外的处理预合成。图层属性的混合体没有预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。

CALayer产生GPU离屏渲染的操作与相应的优化手段

UIViewCALayer关系:
UIView继承自UIResponder,可以处理系统传递过来的事件,如:UIApplication、UIViewController、UIView,以及所有从UIView派生出来的UIKit类。每个UIView内部都有一个CALayer提供内容的绘制和显示,并且作为内部RootLayer的代理视图。

CALayer继承自NSObject类,负责显示UIView提供的内容contents。CALayer有三个视觉元素:背景色backgroundColor、内容contents、边缘borderWidth&borderColor构成,其中,内容的本质是一个CGImage。

CALayer结构.png

以下离屏渲染操作,按对性能影响等级从高到低进行排序: shadows(阴影)圆角mask(遮罩)allowsGroupOpacity(组不透明)edge antialiasing(抗锯齿)

离屏渲染分析

如何查看渲染出来的UI是否经过离屏渲染过程呢?
打开模拟器工具栏 -> Debug -> Color Off-screen Rendered
图层变化成黄色,说明是经过离屏渲染。

模拟器.png

代码部分很简单,新建一个工程写一个tableView,在tableView上面添加一个ImageView和一个UILabel即可。接下来逐个分析出现离屏渲染的情况。

Instruments使用

利用Core Animation来分析应用的性能问题

Instruments.png

Color Blended Layers
这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮(也就是多个半透明图层的叠加)。由于重绘的原因,混合对GPU性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一

Color Hits Green and Misses Red
当设置shouldRasterizep属性为YES的时候,耗时的图层绘制会被缓存,然后当做一个简单的扁平图片呈现。当缓存再生的时候这个选项就用红色对栅格化图层进行了高亮。如果缓存频繁再生的话,就意味着栅格化可能会有负面的性能影响了

Color Offscreen-Rendered Yellow
开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题

当然Debug还有其它的选项,来分析不同的性能问题,如有需求,请参考其它资料。

光栅 - 触发离屏渲染
    private func wj_shouldRasterize() {
        // 缓存机制 -- bitmap -- cpu直接从缓存取数据 -- gpu渲染 -- 可提供性能 -- 缓存在100ms内  慎用!!!
        // 面试尽量别提光栅化。若当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。
        imageV.layer.shouldRasterize = true
        imageV.layer.rasterizationScale = imageV.layer.contentsScale
    }
image.png
遮罩Mask - 触发离屏渲染
    private func wj_mask() {
        // mask是添加在imageV.layer的上层
        let layer = CALayer()
        layer.frame = CGRect(x: 0, y: 0, width: imageV.bounds.size.width, height: imageV.bounds.size.height)
        layer.backgroundColor = UIColor.red.cgColor
        imageV.layer.mask = layer
    }
阴影 - 触发离屏渲染
    private func wj_shadows() {
        // shadow是在imageV.layer的下层
        imageV.layer.shadowColor = UIColor.red.cgColor
        imageV.layer.shadowOpacity = 0.1
        imageV.layer.shadowRadius = 5
        imageV.layer.shadowOffset = CGSize(width: 10, height: 10)
    }
阴影优化 - 不会离屏渲染
    private func wj_shadowsOptimize() {
        imageV.layer.shadowColor = UIColor.red.cgColor
        imageV.layer.shadowOpacity = 0.1
        imageV.layer.shadowRadius = 5
        // CoreAnimation - 阴影的几何形状
        imageV.layer.shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: imageV.bounds.size.width+10, height: imageV.bounds.size.height+10)).cgPath // 10是阴影偏移量
    }
抗锯齿 - 触发离屏渲染
    private func wj_edgeAntialiasing() {
        let angel = CGFloat.pi/20.0
        imageV.layer.transform = CATransform3DRotate(imageV.layer.transform, angel, 0, 0, 1);
        imageV.clipsToBounds = true
        imageV.layer.allowsEdgeAntialiasing = true
    }
视图组不透明 - 触发离屏渲染
// 视图组不透明 alpha 只要有子视图并设置父视图的alpha<1就会离屏渲染
    private func wj_allowsGroupOpacity() {
        // 没有子视图view是不会离屏渲染
        let view = UIView(frame: CGRect(x: 10, y: 10, width: 20, height: 20))
        view.backgroundColor = .green
        imageV.addSubview(view)
        
        imageV.alpha = 0.5
        imageV.layer.allowsGroupOpacity = true // 设置view与imageV透明度一样
    }
圆角

圆角就一定会触发离屏渲染吗?
先告诉你答案是否定的!重点看分析:

圆角案例一
    // 不会触发离屏渲染
    private func wj_radius() {
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
    }
案例一不会触发.png
圆角案例二
    // 四个角就会离屏渲染
    // 设置borderWidth、borderColor只要Color不为clear,四个角就会离屏渲染
    private func wj_radius() {
        imageV.layer.borderWidth = 1
        imageV.layer.borderColor = UIColor.red.cgColor
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
    }
圆角案例二触发.png
圆角案例三
    // 添加子视图 - 四个角就会离屏渲染
    private func wj_radius() {
        let view = UIView(frame: CGRect(x: 30, y: 30, width: 30, height: 30))
        view.backgroundColor = .green
        imageV.addSubview(view)
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
    }
圆角案例三触发.png
圆角案例四:重点分析
image.png
    private func wj_radius() {
         // 加了背景颜色会触发离屏渲染, 其实设置layer的backgroundColor
        imageV.backgroundColor = .red
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true

新增一行代码给imageV添加背景色,它就会触发离屏渲染。
因为imageV.backgroundColor其实设置的是layerbackgroundColor

此时再添加一行代码,将image设置为nil,它就不会触发离屏渲染了:

    private func wj_radius() {
         // 加了背景颜色会触发离屏渲染, 其实设置layer的backgroundColor
        imageV.backgroundColor = .red
         // 实际是把contents层内容设置为nil
        imageV.image = nil
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
    }
image.png

这是因为 imageV.image = nil 实际上是把imageVcontents层内容设置为nil

此时再添加一行代码设置layer的背景色

    private func wj_radius() {
         // 加了背景颜色会触发离屏渲染, 其实设置layer的backgroundColor
        imageV.backgroundColor = .red
         // 实际是把contents层内容设置为nil
        imageV.image = nil
        // 实际是contents层的背景设置为蓝色
        imageV.layer.backgroundColor = UIColor.blue.cgColor
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
    }
image.png

这就验证了上面图层的结构了,contents在layer的上层。

特殊的Label

上面尝试仅仅给imageV添加背景色做圆角处理,它会触发离屏渲染,而对于label它是不会触发离屏渲染的:

    private func wj_radius() {
         // 加了背景颜色会触发离屏渲染, 其实设置layer的backgroundColor
        imageV.backgroundColor = .red
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
        
        // 它设置的是contents这个backgroundColor
        label.backgroundColor = .red
        label.layer.cornerRadius = 15
        label.clipsToBounds = true
    }
image.png

其实label.backgroundColor = .red其实设置的是contents层的背景色,这与imageV不一样了,如何看出呢?我们设置label.layer的背景色看看输出:

    private func wj_radius() {
         // 加了背景颜色会触发离屏渲染, 其实设置layer的backgroundColor
        imageV.backgroundColor = .red
        imageV.layer.cornerRadius = 20
        imageV.clipsToBounds = true
        
        // 它设置的是contents这个backgroundColor
        label.backgroundColor = .red
        // 它设置的是layer的backgroundColor
        label.layer.backgroundColor = UIColor.blue.cgColor
        label.layer.cornerRadius = 15
        label.clipsToBounds = true
    }
image.png

可以看出label的背景色依旧是红色,并且这个时候label是通过离屏渲染过程的。(另外label设置边框颜色和宽度也会离屏渲染 自行调试)

圆角离屏渲染总结:设置圆角要触发离屏渲染条件:去操作contents

圆角优化 - 贝塞尔曲线
    private func wj_radiusBezier() {
        imageV.backgroundColor = .red        
        UIGraphicsBeginImageContextWithOptions(imageV.bounds.size, false, 0.0)
        UIBezierPath(roundedRect: imageV.bounds, cornerRadius: imageV.bounds.size.height/2).addClip()
        imageV.draw(imageV.bounds)
        imageV.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }

然而这种优化方案不是最优异的解决办法。根据实际情况,可以让设计师去切一个圆形的遮罩图,当然这种方式并不能在所有场景都适用。大家灵活运用。

圆形的遮罩图.png

特殊的离屏渲染

视图在重写drawRect方法的时候,系统是通过CoreGraphics框架去渲染,它是由cpu去计算工作的。重写drawRect方法会另外开启一个内存空间去生成另一块画布。
重写drawRect方法是无法通过检测器去检测到的。所以它是特殊的离屏渲染。

本文重在对离屏渲染分析。demo链接
喜欢的铁铁给个star支持一下,感谢收看。

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

推荐阅读更多精彩内容