iOS精选

iOS性能优化系列篇之“列表流畅度优化” (转)

2018-09-27  本文已影响44人  林伟彦笔记

看到一篇关于讲列表优化,讲的很好,特转摘过来。原文链接

这一篇文章是iOS性能优化系列文章的的第二篇,主要内容是关于列表流畅度的优化。在具体内容的阐述过程中会结合性能优化的总体原则进行分析,所以建议大家在阅读这篇文章前先阅读一下上一篇文章:iOS性能优化系列篇之“优化总体原则”。 希望后面有时间把这个系列更新下去,包括内存等其他方面的专项优化内容。希望这篇文章能够给大家在列表流畅度优化方面带来一点点启示。

和上一篇综述性质的文章不同,这一篇文章工程实用性更强一些,更多的是一些优化技术细节。文中讨论了许多可能影响列表流畅度的因素,由于2018 WWDC里面讲述了大量的关于性能优化相关的内容,因此本文也在相关的内容里面加入2018 WWDC的性能优化部分。

读者可将本体提及的优化手段或者原理应用到自己的项目中去。但是希望大家在优化过程中,要结合自己的项目具体问题具体分析,因为本文讨论的影响流畅度的因素,可能并不是你的应用流畅性不佳的瓶颈,根据我的经验,大部分流畅的问题都是业务逻辑导致的,反倒什么离屏渲染啊之类大家耳熟能详的流畅度的影响因素在实际项目中并没有想象的那么大。如果不经实地测量就盲目应用一些优化手段,可能会导致过度优化,事倍功半。

卡顿产生的原因

在总体原则篇中提到,五大原则中的其中一个就是要理解优化任务的底层运行机制,因为只有深入了解底层机制才能更好的有针对性的提出更优的解决方案,所以在进行列表流畅度优化前,我们一定要弄清楚一个view从创建到显示到屏幕上都经历了那些过程,在这些过程中那些方面可能会导致性能瓶颈,以及造成卡顿的底层原因是什么。

我们知道iOS设备大部分情况下,屏幕刷新频率是60hz(ProMotion下是120hz),也就是每隔16.67ms会进行一次屏幕刷新。每次刷新时,需要CPU和GPU配合完成一次图像显示。其主要流程如下:

应用内:

应用外(render server):

image

从上面的图中可以看到,在view显示的过程中,CPU和GPU都各自承担了不同的任务,CPU和GPU不论哪个阻碍了显示流程,都会造成掉帧现象。所以优化方法也需要分别对CPU和GPU压力进行评估和优化,在CPU和GPU压力之间找到性能最优的平衡点, 无论过度优化哪一方导致另一方压力过大都会造成整体FPS性能的下降。而寻找平衡点的过程则因项目特点不同而不同,并没有一套通用的方法,需要我们用instrument等性能评测工具,根据实际app的性能度量结果去做优化,不能凭空乱猜。

CPU优化

我们先看table view在滑动过程中CPU占用的情况。

image

从上图可以看出,在滑动过程中CPU占用特点是:

根据上述特点我们可以做如下优化:

预加载,空间换时间

为什么要预加载:

通过预加载我们希望达到的CPU理想占用效果如下:

image

预加载内容:

静态资源预加载

动态资源预加载

如何预加载:

在iOS10以后,UITableView和UICollectionView提供了预加载机制,iOS12开始prefeatching做了优化,不再与cell的加载同时并发进行,而是cell加载完成之后串行开始prefeatch,从而优化了流畅度

iOS10以前,也可以自己实现类似机制,主要利用的机制有:

加载内容:

注意事项:

WWDC 2018中讲到了一个iOS12的底层优化点,苹果工程师在性能调优的时候发现一个导致丢帧的奇怪case,在没有其他后台线程运行、只有滑动的情况下,会比有少量的后台线程的情况更容易掉帧。通过调研CPU的调度算法发现,在仅有滑动的情况下,为了省电,CPU占用会保持比较底,但是这样CPU会花更多的时间来计算,就会导致可能错过这一帧。所以iOS12中,会把UIKit框架上所有的信息(滑动信息以及滑动frame的关键时间点)传递给底层CPU性能控制器,这样CPU可以更智能调度以在frame截止的时机内完成CPU计算。这部分属于系统底层的优化,对于应用开发者只要应用运行在iOS12就可以获得这部分优化。

多线程

为什么要多线程:

最终通过多线程,我们希望CPU占用达到如下效果:

image

使用多线程注意事项:

可在子线程中进行的任务

缓存

缓存的内容可以是

更优的实现方式

这里说的更优的实现方式,主要是指为了实现同一功能或者效果,CPU占用更小的实现方式。这部分包括的内容其实非常多,也很杂。受限于篇幅和水平有限,这里笔者仅罗列一些比较常见的点,并针对其中比较重要的drawRect优化和图片优化内容做进一步的讲解。

下面详细讲下drawRect优化和图片优化

drawRect优化

首选使用CAShapeLayer替代drawRect,在大多数场景下,都可以使用CAShapeLayer替代drawRect。二者对比:

异步绘制。可以使用异步绘制的方式,在子线程绘制好获得image,然后交给主线程。

Dirty Rectangles: 可以使用setNeedsDisplayInRect标记Dirty Rectangles,仅重绘指定区域,也会极大提升性能。

图片优化

在大多数app中,图片绝对是使用最频繁的资源之一,我们知道磁盘和网络的加载速度和内存比要慢很多,而一般图片都比较大,I/O十分耗时。而且图片还涉及解码,也是一项十分消耗CPU的工作,因此图片的优化对app的性能有着十分关键的作用。

在之前将的优化总体原则的时候,我们说过需要理解优化对象的运行机制,我们先了解下图片显示原理:

如果图片数据没对齐,Core Animation会拷贝一份数据,进行字节对齐

GPU处理位图数据,进行渲染

针对上面的过程,我们的优化手段主要有:

GPU优化

CPU和GPU之所以大不相同,是由于其设计目标的不同,它们分别针对了两种不同的应用场景。CPU需要很强的通用性来处理各种不同的数据类型,同时又要逻辑判断又会引入大量的分支跳转和中断的处理。这些都使得CPU的内部结构异常复杂。而GPU面对的则是类型高度统一的、相互无依赖的大规模数据和不需要被打断的纯净的计算环境。所以CPU擅长逻辑控制,串行的运算。和通用类型数据运算不同,GPU擅长的是大规模并发计算,这也正是密码破解等所需要的。所以GPU除了图像处理,也越来越多的参与到计算当中来。

iOS中GPU在显示方面的工作主要是:接收提交的纹理(Texture)和顶点描述(三角形),进行变换(transform)、混合并渲染,然后输出到屏幕上。屏幕上的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。一般来说,CALayer的大多数属性都是使用GPU来绘制的。虽然GPU在处理图像等渲染是速度很快,但如果开发过程中使用不当,仍会导致GPU占用过高,渲染速度跟不上屏幕刷新导致卡顿。

对GPU消耗比较高的操作有:

纹理的渲染

所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是 GPU 调整和渲染 Texture 的过程,都要消耗不少 GPU 资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。

当图片过大,超过 GPU 的最大纹理尺寸时,图片需要先由 CPU 进行预处理,这对 CPU 和 GPU 都会带来额外的资源消耗。目前来说,iPhone 4S 以上机型,纹理尺寸上限都是 4096x4096,更详细的资料可以看这里:iosres.com。所以,尽量不要让图片和视图的大小超过这个值。

视图的混合 (Composing)

当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。

图形的生成

CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。

常用优化手段

On-Screen Rendering 意为当前屏幕渲染,指的是 GPU 的渲染操作是在当前用于显示的屏幕缓冲区中进行。

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

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

创建新缓冲区 要想进行离屏渲染,首先要创建一个新的缓冲区。

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

所以在图形生成的步骤我们要尽可能的避免离屏渲染

优化工具

iOS开发中,在GPU优化上,我们一般使用instruments中的Core Animation工具来进行滑动流畅度优化,在Core Animation中我们可也看到列表滑动过程中的FPS,其中有一些很有用的debug选项,帮助我们找到代码中有性能问题的代码。下面是一些常用的选项:

Color Blended Layers

Color Blended Layers是用来检测个半透明图层的混合区,渲染程度对屏幕中的混合区域进行绿到红的高亮。因为计算混合区的颜色时,导致overdraw,消耗一定的GPU资源,是导致滑动性能的一个因素。所以尽量要尽量避免

在开发过程中,避免Blended Layers的手段有

Color Hits Green and Misses Red Color Hits Green and Misses Red用来检测是否正确使用shouldRasterize,当缓存需要重新生成时,红色高亮rasterized layers,当设置shouldRasterize=YES,会将layer预先渲染成位图,并缓存。以提高性能。但是如果cache频繁重复地生成,表示shouldRasterize可能带来的是负面的性能影响。因此shouldRasterize适用于渲染耗时、图像内容不变的情况,在列表中由于内容要频繁变化,因此不推荐使用此属性

Color Copied Images

大多数时,Core Animation只需要提交原始图片的指针到render server,不涉及内存copy。但是一些情况下,Core Animation不得不copy一份图片发送到render server。苹果的GPU只解析32bit的颜色格式,如果图片颜色格式不对,CPU会预先格式转换。copy images是非常耗CPU的操作,一定要避免。

Color Misaligned Images 被拉伸缩放的图片、无法正确对齐到像素的图片(可能有不是整数的的坐标)。是耗CPU的操作

Color Offscreen-Rendered Yellow

GPU在当前屏幕缓冲区外开辟新的缓冲区进行渲染, 屏幕外缓冲区和当前屏幕缓冲区上下文切换是十分耗时的操作

引起Offscreen-Rendered的操作有:

避免Offscreen-Rendered的方式可以其他方式实现圆角、shadow + shadowPath等。

总结

本文的讲了一些造成卡顿的原因,以及CPU和GPU优化的常用技巧和工具,大家在优化的时候可以作为参考。但不要把优化手段局限在这些方面,不同的应用有各自不同的特点,一定要具体问题具体分析。甚至可以跳出技术范畴,在交互方面做一些文章,比如在减少列表每次从服务器获取的数据数量、采用用户手动点击触发获取更多数据而不是滑动过程中自动获取、使用交互动画等都可以极大改善用户的滑动体验。

最后还是要强调一下我上一篇文章讲的优化时候需要注意的几大原则,这样才能在优化过程中有更好的全局观,尽量少走弯路,希望大家能够在优化过程中时刻牢记。

其他耗时的资源

注意事项:

在预加载带来的滑动性能提升和内存占用增加之间权衡

注意数据过期的问题

WWDC 2018中讲到了一个iOS12的底层优化点,苹果工程师在性能调优的时候发现一个导致丢帧的奇怪case,在没有其他后台线程运行、只有滑动的情况下,会比有少量的后台线程的情况更容易掉帧。通过调研CPU的调度算法发现,在仅有滑动的情况下,为了省电,CPU占用会保持比较底,但是这样CPU会花更多的时间来计算,就会导致可能错过这一帧。所以iOS12中,会把UIKit框架上所有的信息(滑动信息以及滑动frame的关键时间点)传递给底层CPU性能控制器,这样CPU可以更智能调度以在frame截止的时机内完成CPU计算。这部分属于系统底层的优化,对于应用开发者只要应用运行在iOS12就可以获得这部分优化。

上一篇 下一篇

猜你喜欢

热点阅读