开源学习之YYAsyncLayer
YYAsyncLayer是YYKit中提供的一个可以在异步绘制内容的CALayer子类,通过将layer绘制部分放入异步线程中进行达到提升性能的目的。
display
由于本身是一个异步绘制的Layer,有可能在异步绘制的过程中,Layer执行了dealloc(UITableView中使用)
、外部强制调用setNeedsDisplay方法
等操作,导致上下文有所改变,可以认为是之前的绘制已经无效,需要取消掉,作者这里使用一个计数器来完成线程之间的差异性判断。
系统建议子类需要重写CALayer的display方法来进行绘制内容。
绘制操作中通过使用YYAsyncLayerDisplayTask
来将不同的时机传递出去,在这里作者使用一个强制delegate的转换,而不是通过让YYAsyncLayerDelegate
继承至CALayerDelegate,不明白这样做法的原因。
1 | - (void)display { |
在display方法中通过代理可以获得一个task对象:YYTextAsyncLayerDisplayTask
,接下来就是根据逻辑调用task对应的回调block将状态传递出去。
如果在不考虑多线程可能发生的问题的话,常规的做法中会这样写这段逻辑(如果去掉加粗的部分就是一个同步加载的过程):
- 对task调用willDisplay事件回调
- 进入异步线程
- 调用UIGraphicsBeginImageContext..函数进入CoreGraphics,以及使用UIGraphicsGetCurrentContext生成CGContextRef类型的当前context
- save当前context,对context进行一些设置:颜色、rect等,然后restore当前的context
- 将处理好的context通过task的display回调传递给外部
- 使用UIGraphicsGetImageFromCurrentImageContext函数获取当前context下的UIImage对象
- 调用UIGraphicsEndImageContext结束对context的使用
- 回到异步主线程,将获取的UIImage对象设置为layer的contents
- 调用task的didDisplay回调
上面说了作者使用一个计数器类YYSentinel来确保线程安全,为YYAsyncLayer添加一个计数器属性,判断是否是当前线程的做法如下,由于局部变量sentinel会被block捕获,所以其value的值在当前线程是不会改变的。
1 | _YYTextSentinel *sentinel = _sentinel; |
作者在上面的流程中的以下位置处添加了判断:
- 在第2步进入异步线程之后,也就是将要开始使用CoreGraphics之前,进行判断isCancelled
- 在第6步之前,也就是生成UIImage对象之前
- 在第7步之后,也就是即将要异步回到主线程之前
- 在第8步之中,即将要为YYAsyncLayer设置contents之前
这样提供了cancel功能,就可以避免一些不必要的绘制操作
,减少性能上的损耗
,同时还可以避免多线程之间切换照成的消耗甚至阻塞。
YYDispatchQueuePool
在常规的多线程使用中,都是使用就创建一个新的异步线程,为其提供label、type等,大部分场景都是创建concurrent queue
,也就是并发队列。
由于YYAsyncLayer也是使用异步并发
来达到效果的,可能在同一时刻,特别是在tableView的滑动过程中,可能造成队列的创建、运行、销毁等操作,会挤占主线程CPU资源,直观的影响就是界面卡顿。
基于这样的情况,作者在这里自己实现了一个线程池,为不同优先级创建一些串行队列,从头文件可以看出来有三种方法来获取串行队列:
- 使用
alloc-init初始化
YYDispatchQueuePool,然后从pool中获取对应的queue - 使用系统提供的
defaultPool
,然后从pool中获取queue - 直接使用
YYDispatchQueueGetForQOS
方法从全局pool中获取queue
NSQualityOfService
在上一小节中初始化queuePool的时候有使用到QoS
,全称为Queue quality of service,线程服务质量。
在我们利用GCD使用多线程的时候,定义好要执行的任务,然后将任务放到对应的queue中就行了,GCD会根据你的设置(串行、并行、同步、异步等)来创建线程执行任务。系统内部会根据这些配置合理的运用资源高效的执行代码,其中主要涉及到CPU调度的优先级
、IO优先级
、任务运行在哪个线程
以及运行的顺序
等等,这些东西可以使用QoS来抽象表示。
使用QoS可以为系统提供我们任务的执行场景,目前有:
- user interactive,用户交互的任务,通常和UI有关
- user initiated,由用户发起的并且需要立即得到结果的任务,比如滑动scroll view时去加载数据用于后续cell的显示,这些任务通常跟后续的用户交互相关,在几秒或者更短的时间内完成
- utility,一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载
- background,这些任务对用户不可见,比如后台进行备份的操作
- default,默认的值,优先级介于user-initiated 和 utility之间
YYLabel
在YYLabel内通过重写layerClass,将YYAsyncLayer设置为其内部layer,由于在UIView中CALayer的delegate就为UIView本身,所以这里YYLable也实现了上面提到的YYAsyncLayerDelegate
代理方法。
方法内部根据task提供的三个事件回调,结合作者自己实现的YYText相关的逻辑进行界面的渲染,主要使用到的有_innerText、_innerContainer、_innerLayout、_attachment等内部变量。
在task的willDisplay中移除所有已添加过的attachmentView、attachmentLayer以及自己本身的动画。
在display回调中首先根据属性字符串text和container创建YYTextLayout,使用YYTextLayout结合传入的context在对应的point以及size下进行设置,文字、阴影以及边框等等这些都会在这个时候设置到当前context中,这些信息都会在下一步生成UIImage进而设置为contents中起到作用。
上一小节中已经知道,在task的didDisplay回调之前,已经为layer设置过了contents,这里在didDisplay回调中会将对应的attachment进行添加到当前控件中,最后使用CATransition动画将内容展示到屏幕上。
YYTransaction
在YYAsyncLayer的readme中,作者使用一个简单的例子演示了异步渲染,其中使用到了YYTransaction
这个类,他提供一个对当前runloop的观察者,在当前runloop休眠前、结束的时候会执行observer的方法.
YYTransaction通过接受外部的target、selector来初始化:
1 | + (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{ |
然后transaction通过调用commit
将一个transaction实例添加到全局的集合中,同时确保为main runloop添加对应的observer:
1 | - (void)commit { |
另外,重写hash、isEqual方法来确保添加到set中的对象不会重复。
在执行runloop注册观察者的回调方法中,会对已经添加到全局transactionSet
集合中的YYTransaction执行target-action,然后将他们全部移除,核心代码如下:
在整个YYKit中,作者只在YYTextView中使用了YYTransaction,使用方式和YYAsyncLayer的readme中类似,都是在重写一些属性的setter方法中提供update方法,更新textLayout、contentSize、_selectionView等。