iOS的異步渲染
最近看了YYAsyncLayer在這里總結一下。YYAsyncLayer是整個YYKit異步渲染的基礎。整個項目的Github地址在這里。你可以先下載了一睹為快,也可以跟着我一步一步的了解它是怎么實現異步繪制的。
如何實現異步
兩種方式可以實現異步。一種是使用另外的一個線程,一種是使用RunLoop。另外開一個線程的方法有很多,但是現在最方便的就死GCD了。
GCD
這里介紹一些GCD里常用的方法,為了后面閱讀的需要。還有YYAsyncLayer中用到的更加高級的用法會在下文中深入介紹。
創建一個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和以上版本的話,創建queue的方法和之前的版本的不太太一樣。在iOS 8和以上的版本中創建queue需要先創建一個dispatch_queue_attr_t類型的實例。並作為參數傳入到queue的生成方法里。
DISPATCH_QUEUE_SERIAL說明在這個queue內部的task是串行執行的。
dispatch_once
使用dispatch_once和dispatch_once_t的組合可以實現其中的task只被執行一次。但是有一個前提條件,看代碼:
static dispatch_once_t onceToken; // 1
// 2
dispatch_once(&onceToken, ^{
// 這里的task只被執行一次
});
- 這里的
dispatch_once_t必須是靜態的。也就是要有APP一樣長的生存期來保證這段時間內task只被執行一次。如果不是static的,那么只被執行一次是保證不了的。 dispatch_once方法在這里執行,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引用。后面用這個引用來添加回調。- 使用系統內置的c方法創建一個
RunLoop的觀察者,在創建這個觀察者的時候回同時指定回調方法。 - 給
RunLoop實例添加觀察者,之后減少一個觀察者的引用。
在第二步創建觀察者的時候,還指定了觀察者觀察的事件:kCFRunLoopBeforeWaiting | kCFRunLoopExit,在
RunLoop進入等待或者即將要退出的時候開始執行觀察者。指定了觀察者是否重復(true)。指定了觀察者的優先級:0xFFFFFF,這個優先級比CATransaction優先級為2000000的優先級更低。這是為了確保系統的動畫優先執行,之后再執行異步渲染。
YYRunLoopObserverCallBack就是觀察者收到通知的時候要執行的回調方法。這個方法的聲明是這樣的:
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
渲染是怎么回事
渲染就是把我們代碼里設置的代碼的視圖和數據結合,最后繪制成一張圖呈現在用戶的面前。每秒繪制60張圖,用戶看着就是流暢的揭秘男呈現,如果不到60幀,那么越少用戶看着就會越卡。
CALayer
在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的調用,是在一些繪制之后返回組成的圖片。- 在2>中生成的圖片,最終被賦值給了
CALahyer#contents屬性。
CoreGraphics
如果說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變量的值。 - 在CoreGraphics一節開始的時候講到的繪制具體內容的block。
- 繪制完畢,獲取到
UIImage實例。 - 返回主線程,並給
contents屬性設置繪制的成果圖片。至此異步繪制全部結束。
為了讓讀者更加關注異步繪制這個主題,所以省略了部分代碼。生路的代碼中很多事檢查是否取消的。異步的繪制,尤其是在一個滾動的UITableView或者UICollectionView中隨時都可能會取消,所以即使的檢查是否取消並終止正在進行的繪制很有必要。這些,你會在完整的代碼中看到。
不能無限的開辟線程
我們都知道,把阻塞主線程執行的代碼放入另外的線程里保證APP可以及時的響應用戶的操作。但是線程的切換也是需要額外的開銷的。也就是說,線程不能無限度的開辟下去。
那么,dispatch_queue_t的實例也不能一直增加下去。有人會說可以用dispatch_get_global_queue()來獲取系統的隊列。沒錯,但是這個情況只適用於少量的任務分配。因為,系統本身也會往這個queue里添加任務的。
所以,我們需要用自己的queue,但是是有限個的。在YY里給這個數量指定的值是16。
指定為16,我也是有些疑惑的。在Android里指定線程池的大小的時候通常的值是CPU的內核個數的兩倍。
設計,把點連成線
YYAsyncLayer異步繪制的過程就是一個觀察者執行的過程。所謂的觀察者就是你設置了一個機關,當它被觸發的時候可以執行你預設的東西。比如你走到一扇門前,它感應到了你的紅外輻射就會打開。
async layer也是一樣,它會把“感應器”放在run loop里。當run loop要閑下來的時候“感應器”的回調開始執行,告訴async layer可以開始異步渲染了。
但是異步渲染要干什么呢?我們現在就來說說異步渲染的內容從哪里來?一個需要異步渲染的view會在定義的時候就把需要異步渲染的內容通過layer保存在view的代理發送給layer。
CALayer和UIView的關系
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
``
在實現的時候是這樣的:
```objc
#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對象 - 設置task的
willDisplayblock回調。 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方法的調用。
這樣一個調用鏈就形成了:用戶操作->[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]開始的。
