iOS的異步渲染
最近看了YYAsyncLayer在這里總結一下。YYAsyncLayer是整個YYKit異步渲染的基礎。整個項目的Github地址在這里。你可以先下載了一睹為快,也可以跟著我一步一步的了解它是怎么實現(xiàn)異步繪制的。
如何實現(xiàn)異步
兩種方式可以實現(xiàn)異步。一種是使用另外的一個線程,一種是使用RunLoop。另外開一個線程的方法有很多,但是現(xiàn)在最方便的就是GCD了。
GCD
這里介紹一些GCD里常用的方法,為了后面閱讀的需要。還有YYAsyncLayer中用到的更加高級的用法會在下文中深入介紹。
創(chuàng)建一個queue
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和以上版本的話,創(chuàng)建queue的方法和之前的版本的不太太一樣。在iOS 8和以上的版本中創(chuàng)建queue需要先創(chuàng)建一個dispatch_queue_attr_t類型的實例。并作為參數(shù)傳入到queue的生成方法里。
DISPATCH_QUEUE_SERIAL說明在這個queue內部的task是串行執(zhí)行的。
dispatch_set_target_queue
dispatch_set_target_queue 有兩個作用:
- 設定創(chuàng)建的queue的優(yōu)先級。
- 讓多個serial的queue的任務由并行的變?yōu)樵趖arget queue內是串行的。
這里主要的作用是第一個。也就是把dispatch_queue_create創(chuàng)建的queue的優(yōu)先級設置為和dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)為同一優(yōu)先級。
蘋果的文檔在這里。
dispatch_once
使用dispatch_once和dispatch_once_t的組合可以實現(xiàn)其中的task只被執(zhí)行一次。但是有一個前提條件,看代碼:
static dispatch_once_t onceToken; // 1
// 2
dispatch_once(&onceToken, ^{
// 這里的task只被執(zhí)行一次
});
- 這里的
dispatch_once_t必須是靜態(tài)的。也就是要有APP一樣長的生存期來保證這段時間內task只被執(zhí)行一次。如果不是static的,那么只被執(zhí)行一次是保證不了的。 -
dispatch_once方法在這里執(zhí)行,onceToken在這里有一個取地址的操作。也就是onceToken把地址傳入方法內部被初始化和賦值。
RunLoop
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);
我們來分析一下這段代碼
-
CFRunLoopGetMain方法返回主線程的RunLoop引用。后面用這個引用來添加回調。 - 使用系統(tǒng)內置的c方法創(chuàng)建一個
RunLoop的觀察者,在創(chuàng)建這個觀察者的時候回同時指定回調方法。 - 給
RunLoop實例添加觀察者,之后減少一個觀察者的引用。
在第二步創(chuàng)建觀察者的時候,還指定了觀察者觀察的事件:kCFRunLoopBeforeWaiting | kCFRunLoopExit,在
RunLoop進入等待或者即將要退出的時候開始執(zhí)行觀察者。指定了觀察者是否重復(true)。指定了觀察者的優(yōu)先級:0xFFFFFF,這個優(yōu)先級比CATransaction優(yōu)先級為2000000的優(yōu)先級更低。這是為了確保系統(tǒng)的動畫優(yōu)先執(zhí)行,之后再執(zhí)行異步渲染。
YYRunLoopObserverCallBack就是觀察者收到通知的時候要執(zhí)行的回調方法。這個方法的聲明是這樣的:
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
渲染是怎么回事
渲染就是把我們代碼里設置的代碼的視圖和數(shù)據(jù)結合,最后繪制成一張圖呈現(xiàn)在用戶的面前。每秒繪制60張圖,用戶看著就是流暢的界面呈現(xiàn),如果不到60幀,那么用戶看到的幀數(shù)越少就會越卡。
CALayer
在iOS中,最終我們看到的視圖都是在CALayer里呈現(xiàn)的,在CALayer有一個屬性叫做contents,這里不放別的,放的就是顯示用的一張圖。
我們來看看YYAsyncLayer類的代碼:
// 類聲明
@interface YYAsyncLayer : CALayer // 1
/// Whether the render code is executed in background. Default is YES.
@property BOOL displaysAsynchronously;
@end
//類實現(xiàn)的一部分代碼
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext(); // 2
// ...
dispatch_async(dispatch_get_main_queue(), ^{
self.contents = (__bridge id)(image.CGImage); // 3
});
-
YYAsyncLayer繼承自CALayer。 -
UIGraphicsGetImageFromCurrentImageContext這是一個CoreGraphics的調用,是在一些繪制之后返回組成的圖片。 - 在2>中生成的圖片,最終被賦值給了
CALahyer#contents屬性。
CoreGraphics
如果說CALayer是一個繪制結果的展示,那么繪制的過程就要用到CoreGraphics了。
在正式開始以前,首先需要了解一個方法的實現(xiàn)。這個方法會用來繪制具體的界面上的內容:
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參數(shù)來繪制文字。
目前了解這么多就足夠了,后面會有詳細的介紹。
在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變量的值。 - 在CoreGraphics一節(jié)開始的時候講到的繪制具體內容的block。
- 繪制完畢,獲取到
UIImage實例。 - 返回主線程,并給
contents屬性設置繪制的成果圖片。至此異步繪制全部結束。
為了讓讀者更加關注異步繪制這個主題,所以省略了部分代碼。生路的代碼中很多事檢查是否取消的。異步的繪制,尤其是在一個滾動的UITableView或者UICollectionView中隨時都可能會取消,所以即使的檢查是否取消并終止正在進行的繪制很有必要。這些,你會在完整的代碼中看到。
不能無限的開辟線程
我們都知道,把阻塞主線程執(zhí)行的代碼放入另外的線程里保證APP可以及時的響應用戶的操作。但是線程的切換也是需要額外的開銷的。也就是說,線程不能無限度的開辟下去。
那么,dispatch_queue_t的實例也不能一直增加下去。有人會說可以用dispatch_get_global_queue()來獲取系統(tǒng)的隊列。沒錯,但是這個情況只適用于少量的任務分配。因為,系統(tǒng)本身也會往這個queue里添加任務的。
所以,我們需要用自己的queue,但是是有限個的。在YY里給這個數(shù)量指定的最大值是16。它會首先判斷CPU的核數(shù)(int)[NSProcessInfo processInfo].activeProcessorCount。如果核數(shù)大于給定的最大值則使用最大值。
開辟線程的時候使用的是YYKit里自己的一套“線程池”工具來控制開辟的線程數(shù)量的。
設計,把點連成線
YYAsyncLayer異步繪制的過程就是一個觀察者執(zhí)行的過程。所謂的觀察者就是你設置了一個機關,當它被觸發(fā)的時候可以執(zhí)行你預設的東西。比如你走到一扇門前,它感應到了你的紅外輻射就會打開。
async layer也是一樣,它會把“感應器”放在run loop里。當run loop要閑下來的時候“感應器”的回調開始執(zhí)行,告訴async layer可以開始異步渲染了。
但是異步渲染要干什么呢?我們現(xiàn)在就來說說異步渲染的內容從哪里來?一個需要異步渲染的view會在定義的時候就把需要異步渲染的內容通過layer保存在view的代理發(fā)送給layer。
CALayer和UIView的關系
UIView是顯示層,而顯示在屏幕上的內容是由CALayer來管理的。CALayer的一個代理方法可以在UIView宿主里實現(xiàn)。
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
在實現(xiàn)的時候是這樣的:
#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;
}
- 創(chuàng)建了
YYAsyncLayerDisplayTask對象 - 設置task的
willDisplayblock回調。 3. 4.分別設置了其他的display回調block。
可見YYAsyncLayer的代理的實現(xiàn)會創(chuàng)建一個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:方法,開始異步繪制的過程。
總結
最后,我們把整個異步渲染的過程來串聯(lián)起來。
對一個包含了YYAsyncLayer的view,比如YYLable就像文檔里的一樣。重寫layoutSubviews方法添加對layer的setNeedsDisplay方法的調用。
這樣一個調用鏈就形成了:用戶操作->RunLoop(Waiting | Exiting)->調用observer的回調->[view layoutSubviews]->[view.layer setNeedsDisplay]->[layer display]->[layer _displayAsync]異步繪制開始(準確的說是_displayAsync方法的參數(shù)為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]開始的。