最近看了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]
開始的。