最近研究了一下微博,发现微博列表中可以优化的点相当多,先将比较容易优化的点罗列如下:
预排版
当获取到 API JSON 数据后,将每条 cell 需要的数据都在后台线程计算并封装成一个布局对象 cellLayout。cellLayout 包含有所有文本的 CoreText 排版结果、cell 内部每个控件的高度、cell 整体的高度。每个 cellLayout 占用的内存并不多,所以当生成后,可以全部缓存到内存,以供稍后使用。这样,tableView 在计算各个 cell 高度时,将不会消耗任何多余的计算量;当把 CellLayout 设置到 cell 内部时,cell 内部也就不用再计算布局了。
对于通常的 tableView 来说,提前在后台计算好布局结果是一个非常重要的性能优化点。为了达到最高性能,可能会牺牲一些开发速度,不要使用 AutoLayout 等技术,少用 UILabel 等文本控件。如果你对性能的要求并不那么高,可以尝试使用 TableView 的预估行高功能,并把每个 cell 高度缓存下来。这里有个百度知道团队的开源项目可以很方便的帮你实现这一点:FDTemplateLayoutCell.预渲染
微博的头像在某次改版中换成了圆形,当头像下载下来后,在后台线程将头像预先渲染为圆形并单独保存到一个图片缓存队列中去。
对于 tableView 来说,cell 内容的离屏渲染会带来较大的 GPU 消耗。使用 layer 的圆角属性在低性能的设备上快速滑动的时候,尽管没有感觉到较大的卡顿,但整体的平均帧数降了下来。用 Instument 查看的时候可以看到 GPU 已经满负荷运转,而 CPU 却比较清闲。为了避免离屏渲染,应当尽量避免使用 Layer 的 border、corner、shadow、mask 等技术,而尽量在后台线程预先绘制好对应的内容。异步绘制
参考 ASDK 的实现原理,实现了一部分简单异步绘制的代码:YYAsyncLayer。YYAsyncLayer 是 CALayer 的子类,当它需要显示内容(比如调用了[self setNeedDisplay])的时候它会向代理,也就是 UIView 请求一个异步绘制的任务。在异步绘制的时候,Layer 会传递一个 block 回调,绘制代码可以随时调用该 block 判断绘制任务是否被取消。
当 tableView 快速滑动的时候,会有大量的异步绘制任务提交到后台线程去执行,但是滑动速度过快的时候绘制任务还没有完成就已经被取消了,这时候如果继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法实现,这样我们就需要尽量快速、提前判断当前绘制任务是否被取消,保证被取消的任务能及时退出,不至于影响后续操作。
目前有些第三方微博客户端,使用了这样一种方法来避免高速滑动时候 cell 的绘制过程,相关实现见这个项目VVeboTableViewDemo.它的原理是,当滑动时,松开手指后,立刻计算出滑动停止的时候 cell 的位置,并需要绘制那个位置附近的几个 cell,而忽略当前滑动中的 cel。这个方法比较有技巧性,并且对于滑动性来说是很大的提升,唯一的缺点是快速滑动过程中会出现大量空白的内容。如果你不想实现比较麻烦的异步绘制而又想保证滑动的流程性,这个技巧是不错的选择。-
全局并发控制
当使用 concurrent queue 来执行大量绘制任务的时候,偶尔会遇到这种问题:
大量的任务提交到后台队列的时候,某些任务会因为某些原因(此处是 CGFont锁)被锁住导致线程休眠,或者被阻塞。concurrent queue 随后会创建新的线程来执行其他任务。当这种情况多变时,或者 App 中使用了大量 concurrent queue 来执行较多任务时,App 在同一时刻就会存在几十个线程同时运行、创建、销毁。CPU 是用时间片轮转来实现线程并发的,尽管 concurrent queue 能控制线程的优先级,但大量线程同时操作的时候,这个操作仍然会挤占掉主线程的 CPU 资源。ASDK 有个 Feed 列表的 Demo:SocialAppLayout,当列表内 cell 过多,并且非常快速的滑动的时候,界面仍然会出现少量卡顿,就是这个原因造成的。
使用 concurrent queue 时不可避免的会遇到这种问题,但当使用 serial queue 又不能充分发挥多核 CPU 资源,有一个简单的工具: YYDispatchQueuePool 为不同优先级创建和 CPU 数量相同的 serial queue ,每次从 pool 中取 queue 的时候,会轮询返回其中的一个 queue。可以把 App 内所有的异步操作,包括图像解码、对象释放、异步绘制等,都按照优先级的不同放入了全局的 serial queue 中执行,这样尽量避免了过多线程导致的性能问题
更高效的异步图片加载
SDWebImage 有时候仍然会产生少量的性能问题,并且有地方不能满足需求,所以这时候需要实现一个性能更高德图片加载库。在显示单张图片的时候,利用 UIView.layer.contents 就足够了,没有必要使用 UIImageView 带来额外的资源消耗,为此可以在 CALayer 上添加 setImageWithURL: 等方法。除此之外,还可以把图片解码等操作通过 YYDispatchQueuePool 进行管理,控制 App 总线程数量。其它可以改进的地方
列表中不需要响应触摸事件的控件可以事先用 ASDK 的图层合成技术预先合为一张图,
进一步减少每个 cell 内部图层的数量,用 CALayer 替换掉 UIView。
将 cell 按类型划分,进一步减少 cell 内部不必要的视图对象和操作
把需要放到主线程的任务划分为足够小的块,并通过 Runloop 来进行调度,在每个 loop 中判断下一次 Vsync 时间,并在下次 Vsync 到来之前,把当前未执行完的任务延迟到下一个循环中去。
如何评测界面的流畅度
过早的优化是万恶之源,在需求未定、性能问题不明显时,没有必要尝试进行优化,而是要尽量正确的实现功能。在做性能优化的时候,也最好走 修改代码->profile->修改代码这样一个流程,优化最值得优化的地方。
如果你需要一个明确的 FPS 指示器,可以尝试一下 KMCGeigerCounter,对于 CPU 的卡顿,它可以通过内置的 CADisplayLink 检测出来;对于 CPU 带来的卡顿,它用了一个1X1 的 SKView 来进行监视。这个项目有两个小问题:SKView 虽然能监视到 GPU 的卡顿,但是引入 SKView 本身就会对 CPU/GPU 带来一些额外的资源消耗;而且这个项目在 iOS 下会有一些额外的兼容问题。
这个 FPS 指示器 YYFPSLabel 虽然只有几十行代码,仅用到了 CADisplayLink 来监视 CPU 的卡顿问题,但是可以满足日常需要。
最后使用 Instument 的 GPU Driver 预设,能够实时查看到 CPU 和 GPU 的资源消耗。早这个预设内,你可以看到所以与现实有关的数据,比如 Texture 数量、CA 提交的频率、GPU 消耗等,在定位界面卡顿的问题的时候,这是最好的工具。