1.異步繪制原理
在 UIView 中有一個 CALayer 的屬性,負責 UIView 具體內容的顯示。具體過程是系統會把 UIView 顯示的內容(包括 UILabel 的文字,UIImageView 的圖片等)繪制在一張畫布上,完成后倒出圖片賦值給 CALayer 的 contents 屬性,完成顯示。
這其中的工作都是在主線程中完成的,這就導致了主線程頻繁的處理 UI 繪制的工作,如果要繪制的元素過多,過于頻繁,就會造成卡頓。
那么是否可以將復雜的繪制過程放到后臺線程中執行,從而減輕主線程負擔,來提升 UI 流暢度呢?
答案是可以的,系統給我們留下的異步繪制的口子,請看下面的流程圖,它是我們進行基本繪制的基礎。
UIView繪制流程
-
UIView
調用setNeedsDisplay
并沒有立刻進行繪制; -
UIView
調用setNeedsDisplay
方法其實是調用其layer
屬性的同名方法; - 在當前
runloop
即將結束時調用display
進入繪制流程; - 在
UIView
中layer.delegate
就是UIView
本身,UIView
并沒有實現displayLayer:
方法,所以進入系統的繪制流程,我們可以通過實現displayLayer:
方法來進行異步繪制。
有了上面的異步繪制原理流程圖,我們可以得到一個實現異步繪制的初步思路:在“異步繪制入口”去開辟子線程,然后在子線程中實現和系統類似的繪制流程。
2.系統繪制流程
要實現異步繪制,首先要了解系統的繪制流程,看下面一張流程圖:
系統繪制流程
-
CALayer
在內部創建一個上下文環境(CGContextRef)
; - 判斷
layer
是否有代理:
沒有代理:調用layer
的drawInContext:
方法,
有代理:調用delegate
的drawLayer:inContext:
方法,然后在合適的時機回調代理,在[UIView drawRect]
中進行UI的繪制工作, - 最后
layer
上傳backingStore
的bitmap
位圖到GPU
,也就是將生成的bitmap
位圖賦值給layer.content
屬性,結束系統繪制流程。
3.異步繪制流程
關于異步繪制,參考如下圖:
異步繪制流程
- 某個時機調用
setNeedsDisplay
; -
runloop
將要結束時調用[CALayer display]
; - 若代理實現了
displayLayer
將會調用此方法,在子線程中做異步繪制的工作; - 在子線程中創建上下文、繪制控件并生成圖片;
- 在主線程中設置
layer.contents
,將生成的視圖展示在layer
上。
異步繪制示例代碼:
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AsyncDrawLabel : UIView
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UIFont *font;
@end
NS_ASSUME_NONNULL_END
#import "AsyncDrawLabel.h"
#import <CoreText/CoreText.h>
@implementation AsyncDrawLabel
- (void)setText:(NSString *)text {
_text = text;
}
- (void)setFont:(UIFont *)font {
_font = font;
}
// 除了在drawRect方法中, 其他地方獲取context需要自己創建[http://www.lxweimin.com/p/86f025f06d62] coreText用法簡介:[https://www.cnblogs.com/purple-sweet-pottoes/p/5109413.html]
- (void)displayLayer:(CALayer *)layer {
CGSize size = self.bounds.size;
CGFloat scale = [UIScreen mainScreen].scale;
// 異步繪制,切換至子線程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIGraphicsBeginImageContextWithOptions(size, NO, 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 {
// 將坐標系上下翻轉,因為底層坐標系和 UIKit 坐標系原點位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
// 文本沿著Y軸移動
CGContextTranslateCTM(context, 0, size.height); // 原點為左下角
// 文本反轉成context坐標系
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)];
// 根據attStr生成CTFramesetterRef
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
// 將frame的內容繪制到content中
CTFrameDraw(frame, context);
}
@end
#import "ViewController.h"
#import "AsyncDrawLabel.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
AsyncDrawLabel *label = [[AsyncDrawLabel alloc] initWithFrame:CGRectMake(100, 100, 200, 100)];
label.backgroundColor = [UIColor yellowColor];
label.text = @"異步繪制text";
label.font = [UIFont systemFontOfSize:16];
[self.view addSubview:label];
[label.layer setNeedsDisplay]; // 不調用的話不會觸發displayLayer方法
}
@end