UIKit性能调优实战讲解

在使用UIKit的过程当中,性能优化是永恒的话题。不少人都看过度析优化滑动性能的文章,但其中很多文章只介绍了优化方法却对背后的原理避而不谈,或者是晦涩难懂并且读者缺少实践体验的机会。不妨思考一下下面的问题本身是否有一个清晰的认识:ios

  1. 为何要把控件尽可能设置成不透明的,若是是透明的会有什么影响,如何检测这种影响?
  2. 为何cell中的图片,尽量要使用正确的大小、格式,若是错误会有什么影响,如何检测这种影响?
  3. 为何设置阴影和圆角有可能影响滑动时流畅度?
  4. shouldRasterize和离屏渲染的关系是什么,什么时候应该使用?

本文会结合Instrument分析影响性能的因素,提出优化方案并解释背后的原理,项目初始demo的下载地址在个人Github,强烈建议每一位读者下载下来随着我一步一步调试、优化。若是以为对本身有帮助,能够给一个Star表示支持。后面的图片较多,流量党慎入。git

基本概念

打开项目后,只要CustomTableCell.swift文件便可,它实现了自定义的UITableViewCell以及内部的UI布局,由于重点在于性能优化,代码实现的就比较随意。github

首先按下Command + I打开Instrument,本文主要用到的是Core Animation工具:swift

打开Core Animation调试

注意这个调试必须使用真机,点击左上角的红色圆圈就会开始录制。新手可能不太熟悉,这里简单介绍一下调试界面:缓存

调试界面

咱们须要了解两个两个区域:性能优化

  1. 这里记录了实时的fps数值,有些地方是0是由于屏幕没有滑动
  2. 这是重中之重,接下来我会带你们逐个理解、体验这些调试选项

有过游戏经验的人也许对fps这个概念比较熟悉。咱们知道任何屏幕老是有一个刷新率,好比iphone推荐的刷新率是60Hz,也就是说GPU每秒钟刷新屏幕60次,所以两次刷新之间的间隔为16.67ms。这段时间内屏幕内容保持不变,称为一帧(frame),fps表示frames per second,也就是每秒钟显示多少帧画面。对于静止不变的内容,咱们不须要考虑它的刷新率,但在执行动画或滑动时,fps的值直接反映出滑动的流畅程度。bash

调试、优化

图层混合

首先咱们要明白像素的概念,屏幕上每个点都是一个像素,像素有R、G、B三种颜色构成(有时候还带有alpha值)。若是某一块区域上覆盖了多个layer,最后的显示效果受到这些layer的共同影响。举个例子,上层是蓝色(RGB=0,0,1),透明度为50%,下层是红色(RGB=1,0,0)。那么最终的显示效果是紫色(RGB=0.5,0,0.5)。这种颜色的混合(blending)须要消耗必定的GPU资源,由于实际上可能不止只有两层。若是只想显示最上层的蓝色,能够把它的透明度设置为100%,这样GPU会忽略下面全部的layer,从而节约了不少没必要要的运算。网络

第一个调试选项"Color Blended Layers"正是用于检测哪里发生了图层混合,并用红色标记出来。所以咱们须要尽量减小看到的红色区域。一旦发现应该想法设法消除它。开始调试后勾选这个选项,咱们在手机上能够看到以下的场景:app

Color Blended Layers

不少文章里说把控件设置为opaque = true,其原理就是但愿避免图层混合,然而这种调优通常状况下用处不大。由于UIViewopaque属性默认值就是true,也就是说只要不是人为设置成透明,都不会出现图层混合。好比demo中就没有任何透明的控件。iphone

对于UIImageView来讲,不只它自身须要是不透明的,它的图片也不能含有alpha通道,这就是为何图中第三个图片是绿色,而前两个图片是红色的缘由。因为本人对PS和图像几乎一窍不通,恕我不能演示如何消除这些图片的红色。我从网上找了一个美女的头像来讲明,图像自身的性质可能会对结果有影响,所以若是你肯定本身的代码没有问题,并且出现了图层混合,请联系美工或后台解决。

我的认为比opaque属性更重要的是backgroundColor属性,若是不设置这个属性,控件依然被认为是透明的,因此咱们作的第一个优化是在CustomTableCell类的init方法中添加一行代码:

label.backgroundColor = UIColor.whiteColor()
复制代码

虽然在白色背景下,这行代码没法肉眼看到效果,但从新调试后咱们能够发现label的红色消失了。也正是由于对背景颜色的不重视,它成了影响滑动性能的第一个杀手。

PS:若是label文字有中文,依然会出现图层混合,这是由于此时label多了一个sublayer,若是有好的解决办法欢迎告诉我。

光栅化

光栅化是将一个layer预先渲染成位图(bitmap),而后加入缓存中。若是对于阴影效果这样比较消耗资源的静态内容进行缓存,能够获得必定幅度的性能提高。demo中的这一行代码表示将label的layer光栅化:

label.layer.shouldRasterize = true
复制代码

Instrument中,第二个调试选项是“Color Hits Green and Misses Red”,它表示若是命中缓存则显示为绿色,不然显示为红色,显然绿色越多越好,红色越少越好。勾选这个选项后咱们看到以下的场景:

Color Hits Green and Misses Red

光栅化的核心在于缓存的思想。咱们本身动手把玩一下,能够发现如下几个有意思的现象:

  1. 上下微小幅度滑动时,一直是绿色
  2. 上下较大幅度滑动,新出现的label一开始是红色,随后变成绿色
  3. 若是静止一秒钟,刚开始滑动时会变红。

这是由于layer进行光栅化后渲染成位图放在缓存中。当屏幕出现滑动时,咱们直接从缓存中读取而没必要渲染,因此会看到绿色。当新的label出现时,缓存中没有个这个label的位图,因此会变成红色。第三点比较关键,缓存中的对象有效期只有100ms,即若是在0.1s内没有被使用就会自动从缓存中清理出去。这就是为何停留一下子再滑动就会看到红色。

光栅化的缓存机制是一把双刃剑,先写入缓存再读取有可能消耗较多的时间。所以光栅化仅适用于较复杂的、静态的效果。经过Instrument的调试发现,这里使用光栅化常常出现未命中缓存的状况,若是没有特殊须要则能够关闭光栅化,因此咱们作的第二个优化是注释掉下面这行代码:

// label.layer.shouldRasterize = true
复制代码

光栅化会致使离屏渲染,这一点待会儿会讲。

颜色格式

像素在内存中的布局和它在磁盘中的存储方式并不相同。考虑一种简单的状况:每一个像素有R、G、B和alpha四个值,每一个值占用1字节,所以每一个像素占用4字节的内存空间。一张1920*1080的照片(iPhone6 Plus的分辨率)一共有2,073,600个像素,所以占用了超过8Mb的内存。可是一张一样分辨率的PNG格式或JPEG格式的图片通常状况下不会有这么大。这是由于JPEG将像素数据进行了一种很是复杂且可逆的转化。

当咱们打开JPEG格式的图片时,CPU会进行一系列运算,将JPEG图片解压成像素数据。显然这个工做会消耗很多时间,因此不该该在滑动时进行,咱们应该预先处理好图片。借用WWDC上的一页PPT来讲明:

显示流程

Commit Transaction和Decode在同一帧内进行,若是这两个操做的耗时超过16.67ms,Draw Calls就会延迟到下一帧,从而致使fps值的下降。下面是Commit Transaction的详细流程:

解码与转换

在第三步的Prepare中,CPU主要处理两件事:

  1. 把图片从PNG或JPEG等格式中解压出来,获得像素数据
  2. 若是GPU不支持这种颜色各式,CPU须要进行格式转换

好比应用中有一些从网络下载的图片,而GPU刚好不支持这个格式,这就须要CPU预先进行格式转化。第三个选项“Color Copied Images”就用来检测这种实时的格式转化,若是有则会将图片标记为蓝色。

遗憾的是因为我对图片格式不太了解,也不会使用相关工具,并无能模拟出触发这个选项的场景。咱们要记住的是,若是调试时发现有图片被标记为蓝色,说明图片格式出现了一些问题。

图片大小

第四个选项的使用场景很少,咱们直接看一下第五个选项“Color Misaligned Images”。它表示若是图片须要缩放则标记为黄色,若是没有像素对齐则标记为紫色。勾选上这个选项并进行调试,能够看到以下场景:

图片缩放

在demo中,每一个UIImageView的大小都是180x180,而只有第二张图片的像素大小是360x360。所以除了第二张图片,其余的图片都须要被缩放。图片的缩放须要占用时间,所以咱们要尽量保证不管是本地图片仍是从网络或取得图片的大小,都与其frame保持一致。

第三个优化是调整全部图片的像素大小以免没必要要的缩放。

离屏渲染

离屏渲染表示渲染发生在屏幕以外,你可能认为这是一句废话。为了真正解释清楚什么是离屏渲染,咱们先来看一下正常的渲染通道(Render-Pass):

正常渲染通道

首先,OpenGL提交一个命令到Command Buffer,随后GPU开始渲染,渲染结果放到Render Buffer中,这是正常的渲染流程。可是有一些复杂的效果没法直接渲染出结果,它须要分步渲染最后再组合起来,好比添加一个蒙版(mask):

离屏渲染

在前两个渲染通道中,GPU分别获得了纹理(texture,也就是那个相机图标)和layer(蓝色的蒙版)的渲染结果。但这两个渲染结果没有直接放入Render Buffer中,也就表示这是离屏渲染。直到第三个渲染通道,才把二者组合起来放入Render Buffer中。离屏渲染意味着把渲染结果临时保存,等用到时再取出,所以相对于普通渲染更占用资源。

第六个选项“Color Offscreen-Rendered Yellow”会把须要离屏渲染的地方标记为黄色,大部分状况下咱们须要尽量避免黄色的出现。离屏渲染可能会自动触发,也能够手动触发。如下状况可能会致使触发离屏渲染:

  1. 重写drawRect方法
  2. 有mask或者是阴影(layer.masksToBounds, layer.shadow*),模糊效果也是一种mask
  3. layer.shouldRasterize = true

前二者会自动触发离屏渲染,第三种方法是手动开启离屏渲染。

开始调试并勾选“Color Offscreen-Rendered Yellow”,会看到这样的场景:

离屏渲染

若是没有进行第二步优化,你会发现label也是黄色。能够看到tabbar和statusBar也是黄色,这是由于它们使用了模糊效果。图片也是黄色,这说明它也进行了离屏渲染,观察源码后发现主要缘由是它使用了阴影,接下来咱们进行第四个优化,在设置阴影效果的四行代码下面添加一行:

imgView.layer.shadowPath = UIBezierPath(rect: imgView.bounds).CGPath
复制代码

这行代码制定了阴影路径,若是没有手动指定,Core Animation会去自动计算,这就会触发离屏渲染。若是人为指定了阴影路径,就能够免去计算,从而避免产生离屏渲染。

设置cornerRadius自己并不会致使离屏渲染,但不少时候它还须要配合layer.masksToBounds = true使用。根据以前的总结,设置masksToBounds会致使离屏渲染。解决方案是尽量在滑动时避免设置圆角,若是必须设置圆角,可使用光栅化技术将圆角缓存起来:

// 设置圆角
label.layer.masksToBounds = true
label.layer.cornerRadius = 8
label.layer.shouldRasterize = true
label.layer.rasterizationScale = layer.contentsScale
复制代码

快速路径

还记得以前将离屏渲染和渲染路径时的示意图么,离屏渲染的最后一步是把此前的多个路径组合起来。若是这个组合过程能由CPU完成,就会大量减小GPU的工做。这种技术在绘制地图中可能用到。

第七个选项“Color Compositing Fast-Path Blue”用于标记由硬件绘制的路径,蓝色越多越好。

变化区域

刷新视图时,咱们应该把须要重绘的区域尽量缩小。对于未发生变化的内容则不该该重绘,第八个选项“Flash updated Regions”用于标记发生重绘的区域。一个典型的例子是系统的时钟应用,绝大多数时候只有显示秒针的区域须要重绘:

重绘区域

总结

若是你一步一步作到了这里,我想必定会有很多收益。不过,学而不思则罔,思而不学则殆。动手实践后仍是应该总结提炼,优化滑动性能主要涉及三个方面:

避免图层混合

  1. 确保控件的opaque属性设置为true,确保backgroundColor和父视图颜色一致且不透明
  2. 如无特殊须要,不要设置低于1的alpha
  3. 确保UIImage没有alpha通道

避免临时转换

  1. 确保图片大小和frame一致,不要在滑动时缩放图片
  2. 确保图片颜色格式被GPU支持,避免劳烦CPU转换

慎用离屏渲染

  1. 绝大多数时候离屏渲染会影响性能
  2. 重写drawRect方法,设置圆角、阴影、模糊效果,光栅化都会致使离屏渲染
  3. 设置阴影效果是加上阴影路径
  4. 滑动时若须要圆角效果,开启光栅化

实战

本文的demo能够在个人Github上下载,而后一步一步本身体验优化过程。但demo毕竟是刻意搭建的一个环境,我会在我本身的仿写的简书app上不断进行实战优化,欢迎共同窗习交流。

参考资料:

  1. 绘制像素到屏幕上,原文:Getting Pixels onto the Screen
  2. Advanced Graphics and Animations for iOS Apps:这是2014年WWDC Session 419,强烈建议看一遍。
  3. 如何正确地写好一个界面
  4. Mastering UIKit Performance

还有一些高质量的问答:

  1. What triggers “Color Copied Images” and “Color Hits Green and Misses Red” in Instruments?
  2. UILabel is marked as red when Color Blended Layers is selected
  3. What triggers offscreen rendering, blending and layoutSubviews in iOS?
相关文章
相关标签/搜索