生产中怎样处理masksToBounds离屏渲染带来的性能损耗

551 查看

这篇文章介绍ZYCornerRadius解决生产中圆角带来的离屏渲染问题的思路。
日常生产中app布局离不开美丽的圆角(RounderCorner),特别是用圆角UIImageView来做数据呈现交互,但是这种柔和易于让人接受的视图效果并不仅仅是改变了一个形状那么简单,需要付出一定的性能代价。

相信这已经是总所周知的问题了,日常我们使用layer的两个属性,简单的两行代码就能实现圆角的呈现

由于这样处理的渲染机制是GPU在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作,也就是离屏渲染,这会给我们带来额外的性能损耗,如果这样的圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的的频繁切换,性能的代价会宏观地表现在用户体验上—-掉帧。这也是我亲身体验过的,有一次朋友在玩我手机的时候问我为什么会卡,看了后才发现原来是一个充满圆形头像的TableView。

屏幕的渲染机制这里就不copy了,很多朋友的文章也讨论过这样的问题。这篇文章有深入介绍屏幕显示机制。这里顺便贴一下我笔记里记录的会引发离屏渲染的操作,给大家做个记忆捆绑,正确与否大家可以自己思量。
The following will trigger offscreen rendering:

  • Any layer with a mask (layer.mask)
  • Any layer with layer.masksToBounds / view.clipsToBounds being true
  • Any layer with layer.allowsGroupOpacity set to YES and layer.opacity is less than 1.0
  • Any layer with a drop shadow (layer.shadow*).
  • Any layer with layer.shouldRasterize being true
  • Any layer with layer.cornerRadius, layer.edgeAntialiasingMask, layer.allowsEdgeAntialiasing
  • Text (any kind, including UILabel, CATextLayer, Core Text, etc).
  • Most of the drawing you do with CGContext in drawRect:. Even an empty implementation will be rendered offscreen.

因为这些效果均被认为不能直接呈现于屏幕,而需要在别的地方做额外的处理预合成。具体的检测我们可以使用Instruments的CoreAnimation。

ZYCornerRadius

以下介绍ZYCornerRadius(以Category的方式工作)对UIImageView设置圆角会触发离屏渲染的解决思路,有什么问题和建议还请大家发issues指导更正。

先上一张性能对比图
测试设备6P,屏幕中有40张尺寸为20*20的小图片,使用masksToBounds切角处理时帧率大大下降至20+,使用ZYCornerRadius时帧率保持在57+,性能接近0损耗。

既然我们要避免让GPU触发离屏,那么只能把兵符交给CPU,虽然CPU对图形的处理能力不及GPU,但由于这种处理的难度不大,且代价肯定远小于上下文切换。
其实一开始的想法就是从-drawRect下手,但是看了某篇文章(找不回来了)后打消了这个念头,-drawRect的确存在很多性能坑。

既然不能让控件masksToBounds,ZYCornerRadius就从图片本身下手,我使用在UIKit中对Core Graphics有一定封装的应用层类UIBezierPath,对图片进行破坏性的切角,破坏性仅仅是对切去部分而言,当然这操作是在CPU内完成的,而后我只需要取到处理完成的bitmap(可为UIImage对象)交给GPU显示于屏幕即可。

可见,我对图片进行了切角处理后,将得到的含圆角UIImage通过-setImage传给了UIImageView。操作没有触发GPU离屏渲染,过程在CPU内完成,而后我在Demo中证实了这个方法。

顺便一提这里还存在一个性能问题,==Color Blended Layers==,UIGraphicsBeginImageContextWithOptions(, , )的第二个参数是透明通道的开关,true则为不透明。以下两张图是参数传NO or YES在模拟器中打开了Color Blended Layers Debug所看见的区别:

一些没有被设置为opacity的图层,因为透明通道的存在,系统需要去计算图层堆叠后像素点的真实颜色,在Instruments的测试中也是可以高亮标显出来,这种性能的损耗程度我还没有专门去测试。但是在上图可以看见如果设置为不包含透明通道,我们图片被剪去的部分就没有了颜色(黑漆漆一片),这里使用的解决方案就是在图片上下文中先画一层backgroundColor,缺点就是需要传入:

传入红色的背景颜色,打开Color Blended Layers Debug与原先对比:

prefect!

实际生产

目前我们解决了离屏渲染的问题,可这并不符合实际生产,在app中显示的网络图片我们不可能事先知道并且调用- (void)zy_cornerRadiusWithImage:cornerRadius:rectCornerType:来进行切角,也不可能每次都还要写个SDWedImage的complete回调去做这个操作,我决定用swizzleMethod的办法来处理,关于对swizzleMethod的认识,可以看看我这篇文章

我们把对self.image切角处理放在每次layoutSubviews的时候完成,大家看到这里顿时把我臭骂了一顿。。。在Category里重写-layoutSubviews的致命的,这的确会导致整个项目下所有的UIImageView都会去执行这个山寨的-layoutSubviews,别慌关掉文章,给个机会继续看下去。

首先我们需要将使用者传入的切角参数保存起来,供-layoutSubviews切角时使用,因为category不支持扩展属性,所以我们可以用runtime来做: