最近看了YYAsyncLayer
在这里总结一下。YYAsyncLayer
是整个YYKit
异步渲染的基础。整个项目的Github地址在这里。你能够先下载了一睹为快,也能够跟着我一步一步的了解它是怎么实现异步绘制的。git
两种方式能够实现异步。一种是使用另外的一个线程,一种是使用RunLoop。另外开一个线程的方法有不少,可是如今最方便的就是GCD了。github
这里介绍一些GCD里经常使用的方法,为了后面阅读的须要。还有YYAsyncLayer
中用到的更加高级的用法会在下文中深刻介绍。app
dispatch_queue_t queue; if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) { dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); queue = dispatch_queue_create("com.ibireme.yykit.render", attr); } else { queue = dispatch_queue_create("com.ibireme.yykit.render", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); }
若是iOS 8和以上版本的话,建立queue的方法和以前的版本的不太太同样。在iOS 8和以上的版本中建立queue须要先建立一个dispatch_queue_attr_t
类型的实例。并做为参数传入到queue的生成方法里。异步
DISPATCH_QUEUE_SERIAL
说明在这个queue内部的task是串行执行的。async
dispatch_set_target_queue
有两个做用:工具
这里主要的做用是第一个。也就是把dispatch_queue_create
建立的queue的优先级设置为和dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
为同一优先级。oop
苹果的文档在这里。动画
使用dispatch_once
和dispatch_once_t
的组合能够实现其中的task只被执行一次。可是有一个前提条件,看代码:ui
static dispatch_once_t onceToken; // 1 // 2 dispatch_once(&onceToken, ^{ // 这里的task只被执行一次 });
dispatch_once_t
必须是静态的。也就是要有APP同样长的生存期来保证这段时间内task只被执行一次。若是不是static的,那么只被执行一次是保证不了的。dispatch_once
方法在这里执行,onceToken
在这里有一个取地址的操做。也就是onceToken
把地址传入方法内部被初始化和赋值。CFRunLoopRef runloop = CFRunLoopGetMain(); // 1 CFRunLoopObserverRef observer; // 2 observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit, true, // repeat 0xFFFFFF, // after CATransaction(2000000) YYRunLoopObserverCallBack, NULL); // 3 CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes); CFRelease(observer);
咱们来分析一下这段代码spa
CFRunLoopGetMain
方法返回主线程的RunLoop
引用。后面用这个引用来添加回调。RunLoop
的观察者,在建立这个观察者的时候回同时指定回调方法。RunLoop
实例添加观察者,以后减小一个观察者的引用。在第二步建立观察者的时候,还指定了观察者观察的事件:kCFRunLoopBeforeWaiting | kCFRunLoopExit
,在RunLoop
进入等待或者即将要退出的时候开始执行观察者。指定了观察者是否重复(true)。指定了观察者的优先级:0xFFFFFF
,这个优先级比CATransaction
优先级为2000000的优先级更低。这是为了确保系统的动画优先执行,以后再执行异步渲染。
YYRunLoopObserverCallBack
就是观察者收到通知的时候要执行的回调方法。这个方法的声明是这样的:
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
渲染就是把咱们代码里设置的代码的视图和数据结合,最后绘制成一张图呈如今用户的面前。每秒绘制60张图,用户看着就是流畅的界面呈现,若是不到60帧,那么用户看到的帧数越少就会越卡。
在iOS中,最终咱们看到的视图都是在CALayer里呈现的,在CALayer
有一个属性叫作contents
,这里不放别的,放的就是显示用的一张图。
咱们来看看YYAsyncLayer
类的代码:
// 类声明 @interface YYAsyncLayer : CALayer // 1 /// Whether the render code is executed in background. Default is YES. @property BOOL displaysAsynchronously; @end //类实现的一部分代码 UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); // 2 // ... dispatch_async(dispatch_get_main_queue(), ^{ self.contents = (__bridge id)(image.CGImage); // 3 });
YYAsyncLayer
继承自CALayer
。UIGraphicsGetImageFromCurrentImageContext
这是一个CoreGraphics
的调用,是在一些绘制以后返回组成的图片。CALahyer#contents
属性。若是说CALayer
是一个绘制结果的展现,那么绘制的过程就要用到CoreGraphics
了。
在正式开始之前,首先须要了解一个方法的实现。这个方法会用来绘制具体的界面上的内容:
task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) { if (isCancelled()) return; NSArray *lines = CreateCTLines(text, font, size.width); if (isCancelled()) return; for (int i = 0; i < lines.count; i++) { CTLineRef line = line[i]; CGContextSetTextPosition(context, 0, i * font.pointSize * 1.5); CTLineDraw(line, context); if (isCancelled()) return; } };
你也看到了,这其实不是一个方法而是一个block。这个block会使用传入的CGContextRef context
参数来绘制文字。
目前了解这么多就足够了,后面会有详细的介绍。
在YYAsyncLayer#_displayAsync
方法是如何绘制的,_displayAsync
是一个“私有方法”。
//这里咱们只讨论异步的状况 // 1 CGSize size = self.bounds.size; BOOL opaque = self.opaque; CGFloat scale = self.contentsScale; CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL; dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{ // 2 UIGraphicsBeginImageContextWithOptions(size, opaque, scale); CGContextRef context = UIGraphicsGetCurrentContext(); // 3 if (opaque) { CGContextSaveGState(context); { if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) { CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale)); CGContextFillPath(context); } if (backgroundColor) { CGContextSetFillColorWithColor(context, backgroundColor); CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale)); CGContextFillPath(context); } } CGContextRestoreGState(context); CGColorRelease(backgroundColor); } task.display(context, size, isCancelled); // 4 // 5 UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); // 6 dispatch_async(dispatch_get_main_queue(), ^{ self.contents = (__bridge id)(image.CGImage); }); });
解释以下:
size
, opaque
, scale
和backgroundColor
这个四个值。这些在获取绘制的取悦的时候用到。背景色另外有处理。YYAsyncLayerGetDisplayQueue()
方法返回一个dispatch_queue_t
实例,并在其中开始异步操做。opaque
的值,若是是非透明的话处理背景色。这个时候就会用到第一步里获取到的backgroundColor
变量的值。UIImage
实例。contents
属性设置绘制的成果图片。至此异步绘制所有结束。为了让读者更加关注异步绘制这个主题,因此省略了部分代码。生路的代码中不少事检查是否取消的。异步的绘制,尤为是在一个滚动的UITableView
或者UICollectionView
中随时均可能会取消,因此即便的检查是否取消并终止正在进行的绘制颇有必要。这些,你会在完整的代码中看到。
咱们都知道,把阻塞主线程执行的代码放入另外的线程里保证APP能够及时的响应用户的操做。可是线程的切换也是须要额外的开销的。也就是说,线程不能无限度的开辟下去。
那么,dispatch_queue_t
的实例也不能一直增长下去。有人会说能够用dispatch_get_global_queue()
来获取系统的队列。没错,可是这个状况只适用于少许的任务分配。由于,系统自己也会往这个queue里添加任务的。
因此,咱们须要用本身的queue,可是是有限个的。在YY里给这个数量指定的最大值是16
。它会首先判断CPU的核数(int)[NSProcessInfo processInfo].activeProcessorCount
。若是核数大于给定的最大值则使用最大值。
开辟线程的时候使用的是YYKit里本身的一套“线程池”工具来控制开辟的线程数量的。
YYAsyncLayer
异步绘制的过程就是一个观察者执行的过程。所谓的观察者就是你设置了一个机关,当它被触发的时候能够执行你预设的东西。好比你走到一扇门前,它感应到了你的红外辐射就会打开。
async layer也是同样,它会把“感应器”放在run loop里。当run loop要闲下来的时候“感应器”的回调开始执行,告诉async layer能够开始异步渲染了。
可是异步渲染要干什么呢?咱们如今就来讲说异步渲染的内容从哪里来?一个须要异步渲染的view会在定义的时候就把须要异步渲染的内容经过layer保存在view的代理发送给layer。
UIView是显示层,而显示在屏幕上的内容是由CALayer来管理的。CALayer
的一个代理方法能够在UIView
宿主里实现。
YYAsyncLayer
用的就是这个方式。代理为:
@protocol YYAsyncLayerDelegate <NSObject> @required /// This method is called to return a new display task when the layer's contents need update. - (YYAsyncLayerDisplayTask *)newAsyncDisplayTask; @end
在实现的时候是这样的:
#pragma mark - YYTextAsyncLayerDelegate - (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask { // 1 YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new]; // 2 task.willDisplay = ^(CALayer *layer) { // ... } // 3 task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) { // ... } // 4 task.didDisplay = ^(CALayer *layer, BOOL finished) { // ... } return task; }
YYAsyncLayerDisplayTask
对象willDisplay
block回调。 3. 4.分别设置了其余的display回调block。可见YYAsyncLayer
的代理的实现会建立一个YYAsyncLayerDisplayTask
的实例并返回。在这个实例中包含了layer显示顺序的回调:willDisplay
、display
和didDisplay
。
setNeedsDisplay
对CALayer
实例调用setNeedsDisplay
方法以后CALayer
的display
方法就会被调用。YYAsyncLayer
重写了display
方法:
- (void)display { super.contents = super.contents; [self _displayAsync:_displaysAsynchronously]; }
最终会调用YYAsyncLayer
实例的display
方法。display
方法又会调用到_displayAsync:
方法,开始异步绘制的过程。
最后,咱们把整个异步渲染的过程来串联起来。
对一个包含了YYAsyncLayer
的view,好比YYLable
就像文档里的同样。重写layoutSubviews
方法添加对layer的setNeedsDisplay
方法的调用。
这样一个调用链就造成了:用户操做->RunLoop(Waiting | Exiting)->调用observer的回调->[view layoutSubviews]->[view.layer setNeedsDisplay]->[layer display]->[layer _displayAsync]异步绘制开始(准确的说是_displayAsync
方法的参数为true**的时候开始异步绘制)。
可是这并无用到RunLoop。因此代码会修改成每次调用layoutSubviews
的时候给RunLoop提交一个异步绘制的任务:
- (void)layoutSubviews { [super layoutSubviews]; [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit]; } - (void)contentsNeedUpdated { // do update [self.layer setNeedsDisplay]; }
这样每次RunLoop要进入休眠或者即将退出的时候会开始异步的绘制。这个任务是从[layer setNeedsDisplay]
开始的。