离屏渲染机制描述及界面优化

GPU渲染机制

CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。


ios_screen_display.png
ios_vsync_runloop.png

GPU屏幕渲染方式

GPU屏幕渲染方式有两种:

  • On-Screen Rendering (当前屏幕渲染)
    指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。

  • Off-Screen Rendering(离屏渲染)
    指的是GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作。

    当前屏幕渲染不需要额外创建新的缓存,也不需要开启新的上下文,相对于离屏渲染性能更好。但是受当前屏幕渲染的局限因素限制(只有自身上下文、屏幕缓存有限等),当前屏幕渲染有些情况下的渲染解决不了的,就需要使用到离屏渲染。

    相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:

    • 创建新的缓冲区
      要想进行离屏渲染,首先要创建一个新的缓冲区。
    • 上下文切换
      离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。

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

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

离屏渲染的触发

以下情况或操作会触发离屏渲染:

  • 1、masks(遮罩),为图层设置遮罩layer.mask
  • 2、图层截取,将图层的layer.masksToBoundsview.clipsToBounds属性设置为true
  • 3、透明设置,将图层layer.allowsGroupOpacity属性设置为YESlayer.opacity小于1.0
  • 4、shadows(阴影),为图层设置阴影layer.shadow
  • 5、开启光栅化,设置layer.shouldRasterizetrue
  • 6、设置圆角layer.cornerRadius
  • 7、设置抗锯齿,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing
  • 8、文本,(任何种类,包括UILabel, CATextLayer, Core Text等)
  • 9、渐变
  • 10、特殊的离屏渲染:CPU渲染
    如果重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内同步的完成,渲染得到的bitmap最后再交由GPU用于显示。CoreGraphic通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程。

光栅化概念

其中shouldRasterize(光栅化)是比较特别的一种:

光栅化概念:将图转化为一个个栅格组成的图像。
光栅化特点:每个元素对应帧缓冲区中的一像素。

shouldRasterize = YES在其他属性出发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayer没有发生改变,在下一帧可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图)。

相当于光栅化是把GPU的操作转到CPU上了,生成位图缓存,直接读取复用。

当你使用光栅化时,你可以开启Color Hits Green and Misses Red来检查该场景下光栅化操作是否是一个好的选择。绿色表示缓存被复用,红色表示缓存在被重复创建。

如果光栅化的层变红的太频繁那么光栅化对优化可能没有多少用处。位图缓存从内存中删除又重新创建得太过频繁。红色表明缓存重建得太迟。可以针对性的选择某个较小而较深的层结构进行光栅化,来尝试减少渲染时间。

注意:对于经常变动的内容,这个时候不要开启,否则会造成性能的浪费。例如我们日常经常打交道的TableViewCell,因为TableViewCell的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可光栅化,则会造成大量的离屏渲染,降低图形性能。

离屏渲染的检测

怎么检测离屏渲染呢?我们可以利用Instruments的Core Animation来检测离屏渲染。通过选择Xcode --> Debug --> View Debugging -->Rendering 选择离屏渲染属性,运行项目即可检测离屏渲染。具体各个属性解释可以看这篇文章:iOS Instrument使用之Core Animation

离屏渲染检测.png

我们来看看跟离屏渲染相关的几个属性设置:

  • Color Offscreen-Rendered Yellow
    开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。
  • Color Hits Green and Misses Red
    如果shouldRasterize被设置成YES,对应的渲染结果会被缓存,如果图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了。

该选择哪种渲染方式?

1、尽量使用当前屏幕渲染

离屏渲染、CPU渲染可能带来性能问题,一般情况下,我们要尽量使用当前屏幕渲染。

2、离屏渲染和CPU渲染

由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染;但如果仅仅是实现一个简单的效果,直接使用CPU渲染的效率又可能比离屏渲染好,毕竟离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。

离屏渲染优化

设置圆角

方法一
我们通常会采用这种方式来设置圆角:

/**
 设置cornerRadius>0且clipToBounds为YES,再添加子视图
 */
- (void)setCorner1{
    self.avatarImageView.layer.cornerRadius = self.avatarImageView.bounds.size.width/2;
    self.avatarImageView.clipsToBounds = YES;
    // 或通过设置 layer.masksToBounds = YES
//    self.avatarImageView.layer.masksToBounds = YES;

    // 如果再添加子视图会触发离屏渲染,不添加则不会
    [self.avatarImageView addSubview:self.titleLabel];
}

我们通常设置圆角会通过设置layer.cornerRadius和layer.masksToBounds = YES来设置。这样设置在视图没有子视图的情况下是不会触发离屏渲染的,有子视图就会触发离屏渲染。有子视图的情况下还需要寻找别的方式来避免离屏渲染。

其实在iOS9.0之前UIimageView跟UIButton像上面这样设置圆角都会触发离屏渲染。

iOS9.0系统优化之后UIButton像上面这样设置圆角还是会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。

方法二
利用CoreGraphics画一个圆形上下文,然后把图片绘制上去,得到一个圆形的图片,达到切圆角的目的。

- (UIImage *)drawCircleImage:(UIImage*)image
{
    CGFloat side = MIN(image.size.width, image.size.height);
    
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(side, side), false, [UIScreen mainScreen].scale);
    CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, side, side)].CGPath);
    CGContextClip(UIGraphicsGetCurrentContext());
    
    CGFloat marginX = -(image.size.width - side) * 0.5;
    CGFloat marginY = -(image.size.height - side) * 0.5;
    [image drawInRect:CGRectMake(marginX, marginY, image.size.width, image.size.height)];
    
    CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathFillStroke);
    
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return newImage;
}

方法三
利用mask设置圆角,利用的是UIBezierPathCAShapeLayer来完成,不过这种方式也会造成离屏渲染。

    CAShapeLayer *mask = [[CAShapeLayer alloc] init];
    mask.opacity = 1.0;
    mask.path = [UIBezierPath bezierPathWithOvalInRect:self.avatarImageView.bounds].CGPath;
    self.avatarImageView.layer.mask = mask;

设置阴影

阴影可以通过设置layer层的shadowXXX属性,就可以很方便的UIView添加阴影效果,但是不同的设置方式可能产生性能方面的问题。下面介绍一下不同方式对性能的影响。

方法一

通过设置下面的4个属性,就可以添加阴影,不过这种方式会造成离屏渲染。因为绘制阴影而不指定阴影路径,在绘制阴影的过程中就会产生大量的离屏渲染,非常消耗性能,从而造成UI卡顿。

如下方式设置阴影造成离屏渲染的原因是:iOS会先绘制目标的阴影,然后绘制目标本身,在没有指定阴影的绘制路径时,iOS视图在每次绘制前都会递归的精确计算每个子层阴影的路径,这会非常消耗性能,也是导致卡顿的根源。

    // 设置阴影颜色
    shadowImgView.layer.shadowColor = [UIColor redColor].CGColor;
    // 设置阴影透明度
    shadowImgView.layer.shadowOpacity = 0.8f;
    // 设置阴影偏移量,默认是(0,-3),向上偏移
    shadowImgView.layer.shadowOffset = CGSizeMake(5, 5);
    // 设置阴影半径
    shadowImgView.layer.shadowRadius = 5.f;

方法二

为了减少因为没有设置shadowPath造成绘制阴影时大量重复绘制的问题,我们可以指定阴影的绘制路径,这样在绘制阴影时,就可以在多个layer层共享同一个路径的阴影,以此来提高性能。

如果不指定路径shadowPath,就会使用layer层的alpha通道的混合,而如果指定阴影的路径,就会在多个layer之间共享同一路径,以此来提高性能。

有关什么是layer层的混合,可以这样理解:iOS在渲染每一帧时,都会计算每一个像素的颜色,如果上层layer不透明,就只取上层layer的颜色;而如果上层layer存在透明度时(alpha通道),则需要混合每一层的颜色来计算最终的颜色。如果layer越多,计算量就越大,也就比较耗性能。所以,在开发中,要尽量减少视图的透明层。具体代码如下:

    // 设置阴影颜色
    shadowImgView.layer.shadowColor = [UIColor redColor].CGColor;
    // 设置阴影透明度
    shadowImgView.layer.shadowOpacity = 0.8f;
    // 设置阴影偏移量,默认是(0,-3),向上偏移
    shadowImgView.layer.shadowOffset = CGSizeMake(5, 5);
    // 设置阴影半径
    shadowImgView.layer.shadowRadius = 5.f;
    // 设置阴影路径
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:shadowImgView.bounds];
    // 如果是圆形view,则使用下面的圆形路径
    //UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:shadowImgView.bounds];
    shadowImgView.layer.shadowPath = path.CGPath;

注意:xib拖出来的控件是没法像上面这样设置阴影的,具体设置阴影的方法看这篇文章iOS xib设置阴影

参考文章:

1.ios中的离屏渲染与相关性能检测优化
2.iOS离屏渲染之优化分析
3.iOS性能优化-离屏渲染
4.iOS"离屏渲染"整理总结
5.iOS的阴影绘制及性能优化

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

推荐阅读更多精彩内容

  • chapter one 顾以凡擦着头发打开浴室门时,被面前闪过的几个黑衣墨镜男吓了一跳。 这是干什么?来...
    sssssex阅读 192评论 0 1
  • 夜半,月落无声 水滴石穿 车轮碾碎旧梦 我是遥远方的一只鹰 ...
    公子晓扬阅读 193评论 0 4
  • 文|颜夕遥 01 喜欢妮妮是因为一个很私人的原因。她和我同一天生日,只是比我年幼了几岁。 看着这个青涩的女孩,有的...
    颜夕遥阅读 4,362评论 2 37