Hulk' Den

in-depth thinking and keep moving.

iOS优化界面渲染实践中的几点经验

一、圆角切割

离屏渲染这个词已经老生常谈了,常说的圆角(切割)、遮罩mask、阴影shadow都会导致GPU离屏渲染,好在apple在iOS9以后优化了圆角切割,不再会导致离屏渲染了,但事实并不是这样的。

场景

比如我们直播间上方的观众列表中的圆形头像,在iOS10.3.2系统上用Instruments查看,还是有用黄色阴影标记的离屏渲染,而且由于直播间这种特殊场景,背景视频始终在渲染,这些圆角也就始终在离屏渲染,让人捉急。

我各种猜想及google均无果(有人说设置imageView的backgroundColor会引起但我们的观众头像并没有设置),最后发现是imageView的contentMode属性在作怪,把contentMode恢复成默认的ScaleToFill就好了,当然改动是在不影响显示效果的情况下。同时我也尝试了设置imageView的backgroundColor同样也会引起圆角切割的离屏渲染。

结论

所以得出结论,在iOS9+上使用UIImageView并做了圆角切割的时候,contentMode属性和backgroundColor属性尽量使用默认值来避免GPU离屏渲染。

二、遮罩mask

场景

同样老生常谈,发现问题的场景是我们直播间的某个大动画,播放期间满屏的离屏渲染,看了下代码发现用了好多的mask。捋了下动画中有关某个元素的mask效果,是想用mask做一个元素渐渐出现的效果,其实这个效果用一个不透明的图层盖在元素上面也可以做到,不过没有mask使用起来方便,但是对于GPU来说合成图层比离屏渲染要轻松很多。

结论

所以得出结论,遮罩mask尽量避免使用,用图层合成的方式来代替,但必须承认有些效果是图层合成代替不了的。

三、hidden VS opacity

场景

这是一个莫名图层引发的血案,场景是这样的,直播间每当有进场动画的时候,都会从屏幕的左边划出一个离屏渲染的莫名图层。经过尝试发现造成这个问题的原因是CALayer的hidden属性,在给layer做动画的同时,- (void)animationDidStart:(CAAnimation *)anim触发了layer.hidden属性的变化,造成离屏渲染莫名图层。

关于hidden造成离屏渲染,其实挺有意思,可以自己写个小demo,就单纯的点击一个按钮把一个layer的hidden设为NO(之前为YES),就可以观察到离屏渲染,而且是以屏幕的左上角为原点,一直延伸到这个layer所在的区域,也可以推断出系统处理hidden属性的机制,就是离屏渲染这片区域。

那么如何避免设置hidden属性呢,第一个想到的是在需要的时候add上,不需要时直接remove掉,但是尝试过后发现还是会引起离屏渲染,只是渲染的区域不一样了(只渲染layer的superLayer的区域,比设置hidden造成的影响小),因此可以推测hidden本质上也是remove。

再有一个思路就是用设置opacity来代替设置hidden,经过测试发现离屏渲染消失了!来自stackoverflow的答案也可以佐证:This is the best choice, because setting hidden to true removes view from the render loop. While setting alpha to 0 just makes view transparent.

结论

所以得出结论,用设置opacity来代替设置hidden性能更佳。

四、CALayer VS drawRect

这其实是一个平衡的思想,GPU与CPU与内存之间的平衡。我的经验是APP瓶颈往往在CPU,所以总是会想一些办法去转移本该在CPU上的工作。

场景

场景是这样的,我们直播间连送动画中的”x次数”的文本目前是通过重写UILabel的drawRect方法实现的,drawRect这个方法其实是最不建议使用的,它既牵扯到开辟新的内存空间(而且似乎还是挺大的空间),也牵扯到过多的利用CPU去绘制。

我猜想当初选择这个方法实现的原因可能是文字有渐变色的要求,还有阴影的要求。其实这样的需求可以通过CAGradientLayer和CATextLayer再配合一层CALayer做阴影效果来实现(下面有实现代码),这个方案相对于之前的方案在CPU和内存的表现上很不错,原因就是apple提供的各种CAxxxLayer是做过专门优化(硬件加速)的,所谓硬件加速应该就是充分利用了GPU的计算能力吧。

改进后的方案如下:

#define mColor(hex, a) [UIColor colorWithRed:(((hex)>>16 & 0xff) / 255.f) green:(((hex)>>8 & 0xff) / 255.f) blue:(((hex)&0xff) / 255.f) alpha:(a)]

    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = CGRectMake(50, 100, 200, 50);
    gradientLayer.colors = @[(id)mColor(0xFFEE71, 1).CGColor, (id)mColor(0xF4B60F, 1).CGColor];;
    
    CATextLayer *textLayer = [CATextLayer layer];
    textLayer.frame = gradientLayer.bounds;
    textLayer.font = (__bridge CFTypeRef _Nullable)(font);
    textLayer.fontSize = 27;
    textLayer.contentsScale = [UIScreen mainScreen].scale;
    textLayer.string = @"Hulk Rong";
    
    gradientLayer.mask = textLayer;
    
    CALayer* containerLayer = [CALayer layer];
    containerLayer.shadowColor = [UIColor blackColor].CGColor;
    containerLayer.shadowOffset = CGSizeMake(0, 1);
    containerLayer.shadowRadius = 0;
    containerLayer.shadowOpacity = 0.3;
    [containerLayer addSublayer:gradientLayer];
    [self.view.layer addSublayer:containerLayer];

还有一个常见场景是有时我们需要使用贝塞尔曲线(UIBezierPath)去绘制,也是下意识的去重写drawRect方法,其实设置CAShapeLayer的path属性交由GPU来渲染就可以了,同样会在CPU和内存上有不错的表现。

但是其实,如果能用一张设计给的图片来代替我们写代码去绘制是最好的了!

Tips

在做实验的时候还有个有趣的发现,通过CALayer这种方式,在不断的使用相同的图层的时候,发现一段时间后内存会升到跟直接用CPU绘制时的内存差不多的情况,我猜想这也是系统做的优化,把不断重复使用的图层缓存起来了。

结论

所以得出结论,瓶颈在CPU或者内存的时候,尽量通过各种CALayer的实现来代替重写drawRect方法,把工作转移到GPU上去。

五、光栅化shouldRasterize

这个特性也牵扯到平衡的思想所以顺带一提,它是通过消耗内存来减轻GPU的计算压力,至于能不能减轻CPU的压力还是要看具体的场景。

原理

光栅化是CPU将目标layer(及其所有的子layer渲染后的图层)生成一张bitmap缓存起来,再次用到时直接渲染缓存的bitmap就行,省去了CPU频繁绘制或者GPU的合成计算。这个特性适用于在使用期间不会变化的layer(变化的话缓存就不会被命中,Instruments有专门检测这个缓存是否命中的工具),如果在子layer层级很多或者有文本需要CPU绘制的情况下优化效果会好一些,可以减少GPU的图层合成计算,减少CPU的绘制工作(但是需要CPU来生成缓存的bitmap),转嫁到内存上。

场景

直播间的公聊区域需要展示大量的文本,iOS中凡是牵扯到文本的其实都是CPU利用Core Text这个库来绘制的,包括系统的UILabel、UITextField等控件。那么公聊区域就会大量消耗CPU的资源。分析一下我们的公聊区域,首先满足一条消息的图层在展示过程中是不变的,其次消息也就是文本(还会嵌入图片),需要CPU频繁绘制文本,因为没有复杂的图层本身不需要GPU做太多合成计算所以忽略GPU,那么打开shouldRasterize或许可以为CPU减压。

结论

上面之所以说是或许,我觉得是掌握这个属性的关键所在,因为根据原理推断在满足一定条件的前提下,打开shouldRasterize是一定能够减少GPU对于图层合成的计算量,但是对于CPU来说,有减少也有增加,最后净减少是正还是负还需要依据具体的场景通过Instruments来实际测量。

总结

我觉得每一篇分享都是抛砖引玉,分享了简单的原理和碰到过的场景,但是还有很多使用场景需要集思广益,很多新的细节需要我们去发现,希望我们多多交流,共同进步。

最近的文章

类似逐帧动画或者tableView列表中有大量图片展示需求的场景我们可以做哪些优化?

背景最近公司在调研后打算替换掉老的识别库,据说新库在识别率上做的更好一些。但是在集成后实际压测发现并没有什么提升。(由于分析过程可能牵扯到公司业务,所以此处省略分析发现问题的过程,见谅)……….那么思路捋到这里我们可以总结出来两个至关重要的优化点: 避免频繁的解压资源包操作,每次解压绝对是件耗费的操作。这个是比较低级但在俯瞰全局时容易忽略的点,本篇不做讨论。 逐帧动画的图片数量比较大质量比较高,能否把PNG/JPEG图片解码成位图后保存到本地,避免CPU进行重复频繁的解码图片操作?基于...…

iOS | 优化继续阅读
更早的文章

iOS UI Tests 实现方案分析

UI Tests是一个可以对UI进行测试的框架。为什么需要UI Tests呢?如果客户端分为基础层和业务层的话,业务层最终都是负责界面展示的,通常是MV(C/P/VM),(C/P/VM)中的逻辑并不适合用Unit Tests来覆盖。对于具体的界面操作流程,UI Tests是合适的选择。集成UI Tests下图为在Xcode中为项目添加UI Tests的步骤: 新建Target 新建Case ClassWriting UI TestsRecordingXcode为我们提供了把整套操作转化...…

iOS | Test继续阅读