一、異步繪制原理
在 UIView 中有一個(gè) CALayer 的屬性,負(fù)責(zé) UIView 具體內(nèi)容的顯示。具體過程是系統(tǒng)會(huì)把 UIView 顯示的內(nèi)容(包括 UILabel 的文字,UIImageView 的圖片等)繪制在一張畫布上,完成后倒出圖片賦值給 CALayer 的 contents 屬性,完成顯示。
這其中的工作都是在主線程中完成的,這就導(dǎo)致了主線程頻繁的處理 UI 繪制的工作,如果要繪制的元素過多,過于頻繁,就會(huì)造成卡頓。
那么是否可以將復(fù)雜的繪制過程放到后臺(tái)線程中執(zhí)行,從而減輕主線程負(fù)擔(dān),來提升 UI 流暢度呢?
答案是可以的,系統(tǒng)給我們留下的異步繪制的口子,請看下面的流程圖,它是我們進(jìn)行基本繪制的基礎(chǔ)。
UIView 繪制原理

UIView 調(diào)用 setNeedsDisplay 方法其實(shí)是調(diào)用其 layer 屬性的同名方法,這時(shí) layer 并不會(huì)立刻調(diào)用 display 方法,而是要等到當(dāng)前 runloop 即將結(jié)束的時(shí)候調(diào)用 display,進(jìn)入到繪制流程。在 UIView 中 layer.delegate 就是 UIView 本身,UIView 并沒有實(shí)現(xiàn) displayLayer: 方法,所以進(jìn)入系統(tǒng)的繪制流程,我們可以通過實(shí)現(xiàn) displayLayer: 方法來進(jìn)行異步繪制。
有了上面的異步繪制原理流程圖,我們可以得到一個(gè)實(shí)現(xiàn)異步繪制的初步思路:在“異步繪制入口”去開辟子線程,然后在子線程中實(shí)現(xiàn)和系統(tǒng)類似的繪制流程。
二、系統(tǒng)繪制流程
要實(shí)現(xiàn)異步繪制,我們首先要了解系統(tǒng)的繪制流程,看下面一張流程圖:
UIView 系統(tǒng)繪制流程

首先 CALayer 會(huì)在內(nèi)部創(chuàng)建 一個(gè)上下文環(huán)境(CGContextRef),然后判斷 layer 是否有代理,沒有就調(diào)用 layer 的 drawInContext: 方法,有則調(diào)用 delegate 的 drawLayer:inContext 方法,系統(tǒng)的 UIView 中該方法確實(shí)實(shí)現(xiàn)了,但是它在里面又調(diào)用了 layer 的 drawInContext: 方法,所以 UIView 走的還是 CALayer 的繪制。在 UIView 中完成以上方法后還會(huì)調(diào)用 drawRect: 方法,讓我們進(jìn)行 UI 的重繪工作。
最后無論是哪個(gè)分支都把 backing store 的 bitmap 位圖提交到 GPU,也就是將生成的 bitmap 位圖賦值給 layer.content 屬性。
三、異步繪制流程
我們看一幅時(shí)序圖
異步繪制的機(jī)制和流程

請看下面的代碼
#import "AsyncDrawLabel.h"
#import <CoreText/CoreText.h>
@implementation AsyncDrawLabel
- (void)setText:(NSString *)text {
_text = text;
[self.layer setNeedsDisplay];
}
- (void)setFont:(UIFont *)font {
_font = font;
[self.layer setNeedsDisplay];
}
- (void)displayLayer:(CALayer *)layer {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__block CGSize size;
dispatch_sync(dispatch_get_main_queue(), ^{
size = self.bounds.size;
});
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[self draw:context size:size];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (__bridge id)image.CGImage;
});
});
}
- (void)draw:(CGContextRef)context size:(CGSize)size {
// 將坐標(biāo)系上下翻轉(zhuǎn),因?yàn)榈讓幼鴺?biāo)系和 UIKit 坐標(biāo)系原點(diǎn)位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原點(diǎn)為左下角
CGContextScaleCTM(context, 1, -1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
}
@end
AsyncDrawLabel 是一個(gè)繼承 UIView 的類,其 Label 的文本繪制功能需要我們自己實(shí)現(xiàn)。
我們在 - (void)displayLayer:(CALayer *)layer 方法中異步在全局隊(duì)列中創(chuàng)建上下文環(huán)境然后使用 - (void)draw:(CGContextRef)context size:(CGSize)size 方法進(jìn)行文本的簡單繪制,再回到主線程為 self.layer.contents 賦值。從而完成了一個(gè)簡單的異步繪制。
當(dāng)然這樣的繪制的問題是,如果繪制數(shù)量較多,繪制頻繁,會(huì)阻塞全局隊(duì)列,因?yàn)槿株?duì)列中還有一些系統(tǒng)提交的任務(wù)需要執(zhí)行,可能會(huì)對其造成影響。
YYAsyncLayer
我們需要更加優(yōu)化的方式去管理異步繪制的線程和執(zhí)行流程,使用 YYAsyncLayer 可以讓我們把注意力放在具體的繪制(需要我們做的是上面代碼中 - draw: size: 做的事情),而不需要考慮線程的管理,繪制的時(shí)機(jī)等,大大提高繪制的效率以及我們編程的速度。
YYAsyncLayer 的主要流程如下
在主線程的 RunLoop 中注冊一個(gè) observer,它的優(yōu)先級要比系統(tǒng)的 CATransaction 低,保證系統(tǒng)先做完必須的工作。
把需要異步繪制的操作集中起來。比如設(shè)置字體、顏色、背景色等,不是設(shè)置一個(gè)就繪制一個(gè),而是把它們集中起來,RunLoop 會(huì)在 observer 需要的時(shí)機(jī)通知統(tǒng)一處理。
處理時(shí)機(jī)到時(shí),執(zhí)行異步繪制,并在主線程中把繪制結(jié)果傳遞給 layer.contents。
流程圖如下:
YYAsyncLayer主要流程

使用 YYAsyncLayer 的代碼:
#import "AsyncDrawLabel.h"
#import <YYAsyncLayer.h>
#import <CoreText/CoreText.h>
@interface AsyncDrawLabel ()<YYAsyncLayerDelegate>
@end
@implementation AsyncDrawLabel
+ (Class)layerClass {
return YYAsyncLayer.class;
}
- (void)setText:(NSString *)text {
_text = text.copy;
[self commitTransaction];
}
- (void)setFont:(UIFont *)font {
_font = font;
[self commitTransaction];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self commitTransaction];
}
- (void)contentsNeedUpdated {
[self.layer setNeedsDisplay];
}
- (void)commitTransaction {
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
// 在這里創(chuàng)建異步繪制的任務(wù)
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer * _Nonnull layer) {
};
task.display = ^(CGContextRef _Nonnull context, CGSize size, BOOL (^ _Nonnull isCancelled)(void)) {
if (isCancelled() || self.text.length == 0) {
return;
}
// 在這里進(jìn)行異步繪制
[self draw:context size:size];
};
task.didDisplay = ^(CALayer * _Nonnull layer, BOOL finished) {
if (finished) {
} else {
}
};
return task;
}
- (void)draw:(CGContextRef)context size:(CGSize)size {
// 將坐標(biāo)系上下翻轉(zhuǎn),因?yàn)榈讓幼鴺?biāo)系和 UIKit 坐標(biāo)系原點(diǎn)位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原點(diǎn)為左下角
CGContextScaleCTM(context, 1, -1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
}
@end