iOS绘制和渲染

绘制和渲染的流程

绘制和渲染流程

运行一段动画的过程可以分为6个阶段:

1> 布局 - 为视图/图层准备层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段。
2> 显示 - 图层的寄宿图片被绘制的阶段。绘制涉及到-drawRect:和-drawLayer:inContext:方法的调用。
3> 准备 - Image decoding, Image conversion(如果图片类型不是GPU所支持的,需要对图片进行转换)。
4> 提交 - Core Animation打包所有的图层和动画,然后通过IPC(进程内通信)发送到渲染服务(render server,一个单独管理动画和图层组合的一个系统进程)。这个步骤是递归的,所以如果layer tree如果比较复杂此步骤代价比较高。

上面4个步骤发生在自己的应用程序内部,动画显示到屏幕之前还有2个步骤的工作:
5> 对所有图层属性计算中间值,设置OpenGL几何形状来执行渲染。
6> 在屏幕上渲染可见的三角形。

前5个阶段都在软件层面处理(通过CPU),只有最后一个阶段被GPU执行。6个阶段中只有布局显示两个阶段是可以被我们控制的,Core Animation框架处理剩下的事务。

CPU vs GPU

在16.67ms内准备好需要渲染的帧

CPU和GPU在屏幕上显示内容扮演了重要的角色,为了达到60fps,CPU和GPU需要在1/60=16.67ms内完成各自的工作。在优化iOS绘制和渲染过程中,需要从CPU和GPU两方面入手,确认是哪一部分达到了性能瓶颈影响了绘制效率。并且在可控制的布局和显示阶段,决定哪些由CPU执行,哪些交给GPU去做。

影响CPU使用率的操作

布局的计算

如果视图层级过于复杂,当视图呈现或者修改的时候,计算图层会消耗一部分时间。(UITableView的动态计算cell高度)

解压图片

图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸。

图片转换

Session 419 WWDC 2014[3]中提到:“If an image is in a color format that the GPU can not directly work with, it will be converted in the CPU.”
也就是说图片的颜色格式不是32bit,那么CPU会先进行颜色格式转换,然后GPU才会进行渲染。最好直接提供32bit颜色格式的图片,避免转换,或者在非主线程中进行格式转换。

可以通过Core Animation Instruments的Color Copied Images选项进行图片颜色格式检测。

绘制
使用CALayer进行绘制:

实现了UIView的-drawRect:或者CALayerDelegate的-drawLayer:inContext:方法,为了支持对图层内容的任意绘制,Core Animation必须创建一个图层宽*图层高*4字节大小的寄宿图,宽高的单位均为像素

CALayer的contents属性就对应于寄宿图,寄宿图是通过backing store来保存的。如果没有实现-drawRect:方法,CALayer的contents为空的。(通过po CALayer会发现,实现了-drawRect:的CALayer的contents有内容,反之则没有。)

比如在iPhoneX的模拟器上创建一个没有实现drawRect的5000*5000的视图:

DrawRectView *drawRectView = [[DrawRectView alloc] initWithFrame:CGRectMake(0, 0, 5000, 5000)];
[self.view addSubview:drawRectView];

// 实现-drawRect:后的CALayer状态
(lldb) po 0x604000239700
<CALayer:0x604000239700; 
position = CGPoint (2500 2500); 
bounds = CGRect (0 0; 5000 5000); 
delegate = <DrawRectView: 0x7fb500410ea0; 
frame = (0 0; 5000 5000); 
layer = <CALayer: 0x604000239700>>; 
contents = <CABackingStore 0x7fb500702100 (buffer [15000 15000] BGRX8888)>; opaque = YES; 
allowsGroupOpacity = YES;
opacity = 1; 
rasterizationScale = 3; 
contentsScale = 3>

此时使用了内存41M;当在DrawRectView中实现一个空的-drawRect:方法时,此时内存还是41M;当给drawRectView设置背景颜色后,此时内存暴涨到了899M

使用CATileLayer进行绘制:

在DrawRectView.m中保留-DrawRect:的同时加入如下代码:

+ (Class)layerClass {
    return [CATiledLayer class];
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [(CATiledLayer *)self.layer setTileSize:CGSizeMake(100 * self.contentScaleFactor,
                                                           100 * self.contentScaleFactor)];
    }
    return self;
}

<CATiledLayer:0x6000004257c0; 
position = CGPoint (2500 2500); 
bounds = CGRect (0 0; 5000 5000); 
delegate = <DrawRectView: 0x7fe31bf19e90; 
frame = (0 0; 5000 5000); 
layer = <CATiledLayer: 0x6000004257c0>>; 
contents = <CAImageProvider 0x7fe31bf04940: 15000 x 15000>; 
opaque = YES; 
canDrawConcurrently = YES; 
allowsGroupOpacity = YES; 
opacity = 1; 
tileSize = CGSize (300 300); 
rasterizationScale = 3; 
contentsScale = 3>

内存使用率又会降低到41M,CATiledLayer中没有寄宿图,contents部分是CAImageProvider。

使用CAShapeLayer进行绘制:

1> 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
2> 高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
3> 不会被图层边界剪裁掉。
4> 不会出现像素化。

一旦绘制结束之后,数据通过IPC传到渲染服务。图层每次重绘的时候都需要抹掉分配的内存来重新分配,在此基础上,Core Graphics绘制就会变得十分缓慢,所以提高绘制性能时需要尽量避免去绘制。

像素对齐

建议总是将layer对象的宽高设置成整数,尽管可以设置成浮点数,但是由于会根据layer的bounds来创建位图图片,Core Animation最终会将layer宽高转换成整数[4]

Core Animation Instruments中的Color Misaligned Images选项会做出一些标记。
洋红色: UIView的frame像素不对齐,即不能换算成整数像素值。
黄色:UIImageView的图片像素大小与其frame.size不对齐,图片发生了缩放。

UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 100, 400.1000023, 100.222221110000001)];
label.text = @"{{100, 100}, {100.1000023, 400.222221110000001}}";
[self.view addSubview:label];

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(50, 250, 200, 300)];
imageView.image = [UIImage imageNamed:@"test.png"];
[self.view addSubview:imageView];
像素不对齐标记
iPhoneX适配遇到的像素对齐问题

如果是使用CATileLayer进行绘制,如果是水平方向等分的方式进行绘制,如下所示:

+ (Class)layerClass {
    return [CATiledLayer class];
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        CGFloat width = [UIScreen mainScreen].bounds.size.width / 2.0f;
        [(CATiledLayer *)self.layer setTileSize:CGSizeMake(width * self.contentScaleFactor,
                                                           100 * self.contentScaleFactor)];
    }
    return self;
}

- (void)drawRect:(CGRect)rect {
    NSLog(@"%@", NSStringFromCGRect(rect));
}

按照我们的期望,-drawRect:中打印的应该是"{{0,0}, {187.5, 100}}和{{187.5,0},{187.5, 100}}"之类的结果,但是真实结果却是这样的:

2018-01-25 11:12:34.508418+0800  {{187.33333333333331, 0}, {187.33333333333331, 100}}
2018-01-25 11:12:34.509249+0800  {{374.66666666666663, 300}, {187.33333333333337, 100}}
2018-01-25 11:12:34.509249+0800  {{187.33333333333331, 300}, {187.33333333333331, 100}}
2018-01-25 11:12:34.509921+0800  {{187.33333333333331, 400}, {187.33333333333331, 100}}
2018-01-25 11:12:34.509921+0800  {{374.66666666666663, 400}, {187.33333333333337, 100}}
2018-01-25 11:12:34.510676+0800  {{187.33333333333331, 500}, {187.33333333333331, 100}}

可以推测出,设置分块图片的宽度为375 / 2 * 3(PixelsPerPoint) = 562.5(像素)。分块绘制的图片在转换成位图时宽度转换为整数变成562像素。在-drawRect:中参数rect对应的分块区域的宽度为:562 / 3 = 187.33333,而不是375/2=187.5
由于iPhoneX之前的机型水平分辨率都是偶数,所以水平均分分块绘制不会出现问题。但是iPhoneX的分辨率是1125*2436,水平方向的像素是奇数,所以可能会出现一些奇怪的现象。所以涉及到像素操作的代码要确保最后得到的像素单位是整数。

影响GPU使用率的操作

通过Instruments GPU Driver查看GPU使用率:

通过GPU Driver查看GPU使用率
图层混合

layer的混合涉及到颜色的计算,两个layer混合后每个混合后的像素颜色计算公式为:R = S + D * (1 - Sa),(Source(top),Destination(lower))。如果Source(top)是不透明的,那么R = S。

如果CALayer上的opaque属性为YES,那么该layer就是不透明,GPU不会做任何合成,只是简单的层拷贝。CALayer上opaque的默认值是NO,UIView的alpha默认为1。

修改opaque属性只是会修改Core Animation的backing store,如果CALayer的contents属性是一张带有alpha通道的图片的话,图片仍然会保留其alpha通道而忽略掉opaque属性的值[CALayer文档]。比如UIImageView虽然有CALayer,但是该图层并没有backing store,而是使用一个CGImageRef作为它的内容,渲染服务会把图片的数据绘制到帧缓冲区[2]

通过开启Core Animation Instruments的Color Blended Layers选项来检测图层混合,发生图层混合会显示红色。

图层混合检测

UILabel在使用时避免图层混合的设置方法:

self.likeLabel.layer.masksToBounds = YES;
self.likeLabel.backgroundColor = SNBThemeColor(SNB_BLK_LV9_COL, YES);
离屏渲染

GPU的屏幕渲染方式有两种:
1> On-Screen Rendering即当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
2> Off-Screen Rendering即离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

离屏渲染的代价:
1> 创建新的缓冲区。
2> 上下文切换。离屏渲染的过程中,会发生上下文:从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕。

为什么需要离屏渲染?
一般情况下,OpenGL会将提交到渲染服务(Render Server)的动画直接渲染,但是对于一些复杂的图像动画不能直接进行叠加渲染显示,而是需要根据Command Buffer分通道进行渲染之后再组合,在组合过程中,有些渲染通道不会直接显示,而这些没有直接显示在屏幕上的通道就是Offscreen Render Pass[3][6]

Offscreen Render需要更多的渲染通道,而不同的渲染通道切换需要耗费一定的时间,这个时间内GPU会闲置,当通道达到一定数量,对性能会有较大的影响。

比如,UIBlurEffect的GPU渲染过程[3]

UIBlurEffect效果实现
通道切换间GPU的闲置

UIBlurEffect需要5个通道才能合成最终的效果图,每一个通道需要上一个通道的输出作为输入。从“通道切换GPU的闲置”这张图能够看到,在16.67ms内,Render的红色部分分成5块,对应着5个通道,由于第一个和最后一个通道对应着全尺寸的图片,所以这两个通道处理的时间比其他3个要多一些,反映在图上也就是宽一些。5个红色Bar中的4个橙色bar是在进行渲染通道的切换,此时GPU处于闲置状态。

使用shouldRasterize强制触发离屏渲染:
将CALayer的shouldRasterize设置为YES,会把CALayer对应的位图放入缓存中。
什么情况下适合图层栅格化?
1> 当CALayer的内容是静态的,也就是CALayer内容不会发生变化。
2> 图层结构比较复杂。
3> 使用该图层的地方比较多,存放进缓存中的位图可以多次命中。

参考文献

[1]: iOS核心动画高级技巧
[2]: 绘制像素到屏幕上
[3]: Advanced Graphics and Animations for iOS Apps
[4]: Improving Animation Performance
[5]: 内存恶鬼drawRect
[6]: 深刻理解移动端优化之离屏渲染

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

推荐阅读更多精彩内容

  • 绘制像素到屏幕上 answer-huang22 Mar 2014 分享文章 一个像素是如何绘制到屏幕上去的?有很多...
    阿狸旅途T恤阅读 1,619评论 0 7
  • 卷首语 欢迎来到 objc.io 的第三期! 这一期都是关于视图层的。当然视图层有很多方面,我们需要把它们缩小到几...
    评评分分阅读 1,755评论 0 18
  • 有很多种framework以及很多种方法的组合可以在屏幕上渲染UI元素,我们在这里讨论这个过程中发生的事情,希望这...
    纵横而乐阅读 4,479评论 4 25
  • 本系列文章的重点是关注在总结iOS图形图像的原理和性能优化的常规解决方案。 事先声明,本文绝大多数概念和内容均来源...
    ac3阅读 3,769评论 10 14
  • 每个人,都有一个五彩斑斓的梦。不管你有多少岁,这个梦,就是我们生活的勇气和希望,不管多久能够实现,亦或者根本不能实...
    ERIN191阅读 183评论 0 0