这篇是继 YYCache 后的 YYKit 源码解析。YYAsyncLayer 又是一个短小却质量奇高的轮子。非常适合学习和借鉴。
使用
YYAsyncLayer 可以提供一个绘制自定义 CALayer 的自定义 UIView 组件。使用方式在其 Github上有完整的体现:
1 | @interface YYLabel : UIView |
总的来说需要以下几点:
- 自定义的 UIView 实现
YYAsyncLayerDelegate
协议。 - 重写自定义 UIView 会引起视图更新的方法,在其中手动调用
YYTransaction
的方法,手动设置 layersetNeedsDisplay
。 - 重写 UIView 的 layerClass 方法,返回
YYAsyncLayer
类型。 - 实现
YYAsyncLayerDelegate
协议 require 的方法newAsyncDisplayTask
,在其中创建一个YYAsyncLayerDisplayTask
实例。提供三个 block:willDisplay
,display
,didDisplay
。在display
中通过 Core Graphic 绘图。
如何实现界面流畅
为何产生卡顿
计算机系统中 CPU、GPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
因此,要解决卡顿问题,就要从 CPU 和 GPU 两个角度完成。
CPU 资源消耗原因
对象的创建与销毁
对象的创建于销毁会消耗 CPU 资源。我们可以:
- 推迟创建,以及异步创建和销毁对象
- 复用对象
- 使用 CALayer 代替 UIView
关于对象的异步销毁,ibireme 提供了一种方式,把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁:
1 | NSArray *tmp = self.array; |
布局计算与 AutoLayout
频繁的手动布局计算也会占用 CPU 的资源。因此,比如 tableview,就鼓励缓存 cell 的高度。
AutoLayout 在 iOS12 前效率比较低,不过在 iOS 12后已经进行了算法的优化,和直接设置 frame 差距不大。
文本渲染
常用控件的文本渲染都是通过 CoreText 在主线程绘制 Bitmap 进行的。因此, 可以通过自定义控件,使用 TextKit 或者 CoreText 异步渲染
图片解码
图片在提交到 GPU 之前才会被解码,并且是主线程中。因此,可以后台线程把图片绘制到 CGBitmapContext 中,然后通过 Bitmap 创建图片。
图像的绘制
由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。
1 | - (void)display { |
有一篇 《内存恶鬼drawRect》 的文章,讲述了 drawRect 会在重复创建 Bitmap 并且没有及时释放的时候会消耗大量的内存。
不过这是针对大范围的绘图的情况,通过 CAShapLayer 矢量图的方式而不是 Bitmap 有助于缓解内存压力
GPU 资源消耗原因
视图混合
多个视图重叠在一起的时候需要 GPU 把他们混合在一起。可以适当减少视图数量以及层级。并且避免使用存在透明度的视图。同时可以预先将多个视图渲染为一个图片显示。
图像的合成
避免离屏渲染
实现
结构
YYAsyncLayer 的结构非常简单。只有三个类文件:
YYAsyncLayer
YYSentinel
YYTransaction
YYAsyncLayer
继承于 CALayer,在内部重写了 display
方法,用来异步绘制。
YYSentinel
是一个自增的计数类。每次即将渲染的时候都会递增,并记录下来。当开始异步渲染的时候判断是否变化,来决定是否取消上次的渲染。
YYTransaction
给主线程 runloop 添加 observer 回调。用于在 runloop 空闲的时候进行渲染。
YYTransaction
创建
我们来看它的创建方法:
1 | /// 创建 YYTransaction 实例,需要传入 target 和 selector |
创建方法很普通,在生成的 YYTransaction
实例中存入 target
和 selector
,也就是之后要执行的对象和方法。
提交
提交方法中添加了主线程 runloop 的 kCFRunLoopBeforeWaiting | kCFRunLoopExit
时刻的监听:
1 | - (void)commit { |
创建一个 Set,保存所有要执行的方法,然后将任务添加到主线程的 runloop 空闲时候执行。这是一个非常好的优化思想。非常值得学习。
YYSentinel
这是一个自增的计数器。通过 OSAtomicIncrement32
方法实现递增:
1 | @implementation YYSentinel { |
OSAtomicIncrement32()
是原子自增方法,线程安全。在日常开发中,若需要保证整形数值变量的线程安全,可以使用 OSAtomic 框架下的方法,它往往性能比使用各种“锁”更为优越,并且代码优雅。
YYAsyncLayer
初始化
YYAsyncLayer
继承于 CALayer。前面在设置 UIView 子类的时候需要设置 layerClass
方法返回 YYAsyncLayer.class
就是为了在创建 UIView 的时候,使用 YYAsyncLayer
作为其 CALayer。创建的时候会调用 YYAsyncLayer
的初始化方法:
1 | - (instancetype)init { |
它会根据屏幕的像素比设置 contentsScale
。
渲染方法
UIView 是持有 CALayer,并且作为 CALayer 的 delegate 的存在。当 UIView 即将渲染的时候,会调用 CALayer 的 display
方法。代码比较长,不过已经做了详尽的注释:
1 | /// CALayer 的显示方法 |
主要做了以下几点:
- 取出自定义 UIView 中实现的
newAsyncDisplayTask
方法返回的实例。从中取出 display 以及前后的回调。 - 判断要渲染的宽或者高为 0,那么直接清空 contents
- 如果是异步,那么创建异步队列,并通过 CoreGraphic 渲染。
- 如果是同步,直接同步渲染。
可以看到这里作者使用了大量的 if (isCancelled()) {...}
判断,只要失败,立刻返回。这样能够尽可能多的节省 CPU 资源。
来看一下 isCancelled
这个 block 的实现方式:
1 | BOOL (^isCancelled)(void) = ^BOOL() { |
通过 block 捕获当前渲染周期内的变量值。其他线程改变了全局变量_sentinel
的值也不会影响当前的value
。若当前value
不等于最新的_sentinel .value
时,说明绘制任务已经更新,当前绘制任务已经被放弃,就需要及时的做返回逻辑。
那么什么时候这个计数会改变呢?在提交重绘的时候。
1 | - (void)setNeedsDisplay { |
异步线程的管理
每次异步渲染获取的队列并不是一个并行队列。而是多个串行队列:
1 | static dispatch_queue_t YYAsyncLayerGetDisplayQueue() { |
为什么不用并行队列呢?因为并行和并发是有区别的。在单核设备上,CPU通过频繁的切换上下文来运行不同的线程,速度足够快以至于我们看起来它是‘并行’处理的,然而我们只能说这种情况是并发而非并行。
并行队列并不能完全体现出多核处理器的优势。实际上一个 n 核设备同一时刻最多能 并行 执行 n 个任务,也就是最多有 n 个线程是相互不竞争 CPU 资源的。
而串行队列中只有一个线程,该框架中,作者使用和处理器核心相同数量的串行队列来轮询处理异步任务,有效的减少了线程调度操作。
ASDK
ASDK 的渲染部分也是通过异步的方式进行渲染。不过 ASDK 还在其他方面进行了优化,包括,Layout,Rendering 和 UIKit Objects。
总结
YYAsyncLayer 的代码也就几百行,非常的精简,但是蕴含着精华:
- 通过 Core Graphic 异步预渲染视图,在主线程中设置 layer 的 contents 属性。
- 给 runloop 增加 observer,在 runloop 即将结束的时候运行耗时逻辑,以防止阻塞主线程
- 创建处理器数量个串行队列作为 GCD 的执行队列,最大程度上利用多核 CPU 的优势。
思考题
- 为什么会产生卡顿?
- CPU 和 GPU 的职责有哪些?
- CPU 方面如何优化?
- GPU 如何优化?
- YYAsyncLayer 为什么可以异步渲染?
- YYAsyncLayer 在何时执行渲染?
- YYAsyncLayer 如何通过串行队列实现异步的?
参考
iOS 保持界面流畅的技巧啥也不说了,经典中的经典
不得不说,indulge_in 和 Draveness 是我见过的唯二文章写的深刻又易懂的人了。佩服