微博 Demo 性能优化技巧
最后更新于:2022-04-01 04:58:26
## **微博 Demo 性能优化技巧**
我为了演示 YYKit 的功能,实现了微博和 Twitter 的 Demo,并为它们做了不少性能优化,下面就是优化时用到的一些技巧。
[TOC=3,3]
### **预排版**
当获取到 API JSON 数据后,我会把每条 Cell 需要的数据都在后台线程计算并封装为一个布局对象 CellLayout。CellLayout 包含所有文本的 CoreText 排版结果、Cell 内部每个控件的高度、Cell 的整体高度。每个 CellLayout 的内存占用并不多,所以当生成后,可以全部缓存到内存,以供稍后使用。这样,TableView 在请求各个高度函数时,不会消耗任何多余计算量;当把 CellLayout 设置到 Cell 内部时,Cell 内部也不用再计算布局了。
对于通常的 TableView 来说,提前在后台计算好布局结果是非常重要的一个性能优化点。为了达到最高性能,你可能需要牺牲一些开发速度,不要用 Autolayout 等技术,少用 UILabel 等文本控件。但如果你对性能的要求并不那么高,可以尝试用 TableView 的预估高度的功能,并把每个 Cell 高度缓存下来。这里有个来自百度知道团队的开源项目可以很方便的帮你实现这一点:[FDTemplateLayoutCell](https://github.com/forkingdog/UITableView-FDTemplateLayoutCell/)。
### **预渲染**
微博的头像在某次改版中换成了圆形,所以我也跟进了一下。当头像下载下来后,我会在后台线程将头像预先渲染为圆形并单独保存到一个 ImageCache 中去。
对于 TableView 来说,Cell 内容的离屏渲染会带来较大的 GPU 消耗。在 Twitter Demo 中,我为了图省事儿用到了不少 layer 的圆角属性,你可以在低性能的设备(比如 iPad 3)上快速滑动一下这个列表,能感受到虽然列表并没有较大的卡顿,但是整体的平均帧数降了下来。用 Instument 查看时能够看到 GPU 已经满负荷运转,而 CPU 却比较清闲。为了避免离屏渲染,你应当尽量避免使用 layer 的 border、corner、shadow、mask 等技术,而尽量在后台线程预先绘制好对应内容。
### **异步绘制**
我只在显示文本的控件上用到了异步绘制的功能,但效果很不错。我参考 ASDK 的原理,实现了一个简单的异步绘制控件。这块代码我单独提取出来,放到了这里:[YYAsyncLayer](https://github.com/ibireme/YYAsyncLayer)。YYAsyncLayer 是 CALayer 的子类,当它需要显示内容(比如调用了 [layer setNeedDisplay])时,它会向 delegate,也就是 UIView 请求一个异步绘制的任务。在异步绘制时,Layer 会传递一个 BOOL(^isCancelled)()这样的 block,绘制代码可以随时调用该 block 判断绘制任务是否已经被取消。
当 TableView 快速滑动时,会有大量异步绘制任务提交到后台线程去执行。但是有时滑动速度过快时,绘制任务还没有完成就可能已经被取消了。如果这时仍然继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法完成。我的做法是尽量快速、提前判断当前绘制任务是否已经被取消;在绘制每一行文本前,我都会调用 isCancelled() 来进行判断,保证被取消的任务能及时退出,不至于影响后续操作。
目前有些第三方微博客户端(比如 VVebo、墨客等),使用了一种方式来避免高速滑动时 Cell 的绘制过程,相关实现见这个项目:[VVeboTableViewDemo](https://github.com/johnil/VVeboTableViewDemo)。它的原理是,当滑动时,松开手指后,立刻计算出滑动停止时 Cell 的位置,并预先绘制那个位置附近的几个 Cell,而忽略当前滑动中的 Cell。这个方法比较有技巧性,并且对于滑动性能来说提升也很大,唯一的缺点就是快速滑动中会出现大量空白内容。如果你不想实现比较麻烦的异步绘制但又想保证滑动的流畅性,这个技巧是个不错的选择。
### **全局并发控制**
当我用 concurrent queue 来执行大量绘制任务时,偶尔会遇到这种问题:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-11-19_564d5004ac339.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-11-19_564d5004da1af.png)
大量的任务提交到后台队列时,某些任务会因为某些原因(此处是 CGFont 锁)被锁住导致线程休眠,或者被阻塞,concurrent queue 随后会创建新的线程来执行其他任务。当这种情况变多时,或者 App 中使用了大量 concurrent queue 来执行较多任务时,App 在同一时刻就会存在几十个线程同时运行、创建、销毁。CPU 是用时间片轮转来实现线程并发的,尽管 concurrent queue 能控制线程的优先级,但当大量线程同时创建运行销毁时,这些操作仍然会挤占掉主线程的 CPU 资源。ASDK 有个 Feed 列表的 Demo:[SocialAppLayout](https://github.com/facebook/AsyncDisplayKit/tree/master/examples/SocialAppLayout),当列表内 Cell 过多,并且非常快速的滑动时,界面仍然会出现少量卡顿,我谨慎的猜测可能与这个问题有关。
使用 concurrent queue 时不可避免会遇到这种问题,但使用 serial queue 又不能充分利用多核 CPU 的资源。我写了一个简单的工具 [YYDispatchQueuePool](https://github.com/ibireme/YYDispatchQueuePool),为不同优先级创建和 CPU 数量相同的 serial queue,每次从 pool 中获取 queue 时,会轮询返回其中一个 queue。我把 App 内所有异步操作,包括图像解码、对象释放、异步绘制等,都按优先级不同放入了全局的 serial queue 中执行,这样尽量避免了过多线程导致的性能问题。
### **更高效的异步图片加载**
SDWebImage 在这个 Demo 里仍然会产生少量性能问题,并且有些地方不能满足我的需求,所以我自己实现了一个性能更高的图片加载库。在显示简单的单张图片时,利用 UIView.layer.contents 就足够了,没必要使用 UIImageView 带来额外的资源消耗,为此我在 CALayer 上添加了 setImageWithURL 等方法。除此之外,我还把图片解码等操作通过 YYDispatchQueuePool 进行管理,控制了 App 总线程数量。
### **其他可以改进的地方**
上面这些优化做完后,微博 Demo 已经非常流畅了,但在我的设想中,仍然有一些进一步优化的技巧,但限于时间和精力我并没有实现,下面简单列一下:
列表中有不少视觉元素并不需要触摸事件,这些元素可以用 ASDK 的图层合成技术预先绘制为一张图。
再进一步减少每个 Cell 内图层的数量,用 CALayer 替换掉 UIView。
目前每个 Cell 的类型都是相同的,但显示的内容却各部一样,比如有的 Cell 有图片,有的 Cell 里是卡片。把 Cell 按类型划分,进一步减少 Cell 内不必要的视图对象和操作,应该能有一些效果。
把需要放到主线程执行的任务划分为足够小的块,并通过 Runloop 来进行调度,在每个 Loop 里判断下一次 VSync 的时间,并在下次 VSync 到来前,把当前未执行完的任务延迟到下一个机会去。这个只是我的一个设想,并不一定能实现或起作用。