开源学习之YYAsyncLayer

YYAsyncLayer是YYKit中提供的一个可以在异步绘制内容的CALayer子类,通过将layer绘制部分放入异步线程中进行达到提升性能的目的。

display

由于本身是一个异步绘制的Layer,有可能在异步绘制的过程中,Layer执行了dealloc(UITableView中使用)外部强制调用setNeedsDisplay方法等操作,导致上下文有所改变,可以认为是之前的绘制已经无效,需要取消掉,作者这里使用一个计数器来完成线程之间的差异性判断。

系统建议子类需要重写CALayer的display方法来进行绘制内容。

绘制操作中通过使用YYAsyncLayerDisplayTask来将不同的时机传递出去,在这里作者使用一个强制delegate的转换,而不是通过让YYAsyncLayerDelegate继承至CALayerDelegate,不明白这样做法的原因。

1
2
3
4
5
6
7
8
9
10
- (void)display {
super.contents = super.contents;
[self _displayAsync:_displaysAsynchronously];
}

- (void)_displayAsync:(BOOL)async {
__strong id<YYTextAsyncLayerDelegate> delegate = (id)self.delegate;
YYTextAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
// 其他逻辑省略
}

在display方法中通过代理可以获得一个task对象:YYTextAsyncLayerDisplayTask,接下来就是根据逻辑调用task对应的回调block将状态传递出去。

如果在不考虑多线程可能发生的问题的话,常规的做法中会这样写这段逻辑(如果去掉加粗的部分就是一个同步加载的过程):

  1. 对task调用willDisplay事件回调
  2. 进入异步线程
  3. 调用UIGraphicsBeginImageContext..函数进入CoreGraphics,以及使用UIGraphicsGetCurrentContext生成CGContextRef类型的当前context
  4. save当前context,对context进行一些设置:颜色、rect等,然后restore当前的context
  5. 将处理好的context通过task的display回调传递给外部
  6. 使用UIGraphicsGetImageFromCurrentImageContext函数获取当前context下的UIImage对象
  7. 调用UIGraphicsEndImageContext结束对context的使用
  8. 回到异步主线程,将获取的UIImage对象设置为layer的contents
  9. 调用task的didDisplay回调

上面说了作者使用一个计数器类YYSentinel来确保线程安全,为YYAsyncLayer添加一个计数器属性,判断是否是当前线程的做法如下,由于局部变量sentinel会被block捕获,所以其value的值在当前线程是不会改变的。

1
2
3
4
5
_YYTextSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)() = ^BOOL() {
return value != sentinel.value;
};

作者在上面的流程中的以下位置处添加了判断:

  • 在第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
2
3
4
5
6
7
+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{
if (!target || !selector) return nil;
YYTransaction *t = [YYTransaction new];
t.target = target;
t.selector = selector;
return t;
}

然后transaction通过调用commit将一个transaction实例添加到全局的集合中,同时确保为main runloop添加对应的observer:

1
2
3
4
5
- (void)commit {
if (!_target || !_selector) return;
YYTransactionSetup();
[transactionSet addObject:self];
}

另外,重写hash、isEqual方法来确保添加到set中的对象不会重复。

在执行runloop注册观察者的回调方法中,会对已经添加到全局transactionSet集合中的YYTransaction执行target-action,然后将他们全部移除,核心代码如下:

在整个YYKit中,作者只在YYTextView中使用了YYTransaction,使用方式和YYAsyncLayer的readme中类似,都是在重写一些属性的setter方法中提供update方法,更新textLayout、contentSize、_selectionView等。

参考文章