1.異步繪制原理
在 UIView 中有一個(gè) CALayer 的屬性裸卫,負(fù)責(zé) UIView 具體內(nèi)容的顯示仿贬。具體過(guò)程是系統(tǒng)會(huì)把 UIView 顯示的內(nèi)容(包括 UILabel 的文字,UIImageView 的圖片等)繪制在一張畫布上墓贿,完成后倒出圖片賦值給 CALayer 的 contents 屬性茧泪,完成顯示蜓氨。
這其中的工作都是在主線程中完成的,這就導(dǎo)致了主線程頻繁的處理 UI 繪制的工作队伟,如果要繪制的元素過(guò)多穴吹,過(guò)于頻繁,就會(huì)造成卡頓嗜侮。
那么是否可以將復(fù)雜的繪制過(guò)程放到后臺(tái)線程中執(zhí)行刀荒,從而減輕主線程負(fù)擔(dān),來(lái)提升 UI 流暢度呢棘钞?
答案是可以的缠借,系統(tǒng)給我們留下的異步繪制的口子,請(qǐng)看下面的流程圖宜猜,它是我們進(jìn)行基本繪制的基礎(chǔ)泼返。
UIView繪制流程
-
UIView
調(diào)用setNeedsDisplay
并沒(méi)有立刻進(jìn)行繪制; -
UIView
調(diào)用setNeedsDisplay
方法其實(shí)是調(diào)用其layer
屬性的同名方法; - 在當(dāng)前
runloop
即將結(jié)束時(shí)調(diào)用display
進(jìn)入繪制流程姨拥; - 在
UIView
中layer.delegate
就是UIView
本身绅喉,UIView
并沒(méi)有實(shí)現(xiàn)displayLayer:
方法,所以進(jìn)入系統(tǒng)的繪制流程叫乌,我們可以通過(guò)實(shí)現(xiàn)displayLayer:
方法來(lái)進(jìn)行異步繪制柴罐。
有了上面的異步繪制原理流程圖,我們可以得到一個(gè)實(shí)現(xiàn)異步繪制的初步思路:在“異步繪制入口”去開辟子線程憨奸,然后在子線程中實(shí)現(xiàn)和系統(tǒng)類似的繪制流程革屠。
2.系統(tǒng)繪制流程
要實(shí)現(xiàn)異步繪制,首先要了解系統(tǒng)的繪制流程排宰,看下面一張流程圖:
系統(tǒng)繪制流程
-
CALayer
在內(nèi)部創(chuàng)建一個(gè)上下文環(huán)境(CGContextRef)
; - 判斷
layer
是否有代理:
沒(méi)有代理:調(diào)用layer
的drawInContext:
方法似芝,
有代理:調(diào)用delegate
的drawLayer:inContext:
方法,然后在合適的時(shí)機(jī)回調(diào)代理板甘,在[UIView drawRect]
中進(jìn)行UI的繪制工作党瓮, - 最后
layer
上傳backingStore
的bitmap
位圖到GPU
,也就是將生成的bitmap
位圖賦值給layer.content
屬性盐类,結(jié)束系統(tǒng)繪制流程寞奸。
3.異步繪制流程
關(guān)于異步繪制,參考如下圖:
異步繪制流程
- 某個(gè)時(shí)機(jī)調(diào)用
setNeedsDisplay
; -
runloop
將要結(jié)束時(shí)調(diào)用[CALayer display]
; - 若代理實(shí)現(xiàn)了
displayLayer
將會(huì)調(diào)用此方法在跳,在子線程中做異步繪制的工作枪萄; - 在子線程中創(chuàng)建上下文、繪制控件并生成圖片硬毕;
- 在主線程中設(shè)置
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需要自己創(chuàng)建[http://www.reibang.com/p/86f025f06d62] coreText用法簡(jiǎn)介:[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);
// 獲取當(dāng)前上下文
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);
// 文本沿著Y軸移動(dòng)
CGContextTranslateCTM(context, 0, size.height); // 原點(diǎn)為左下角
// 文本反轉(zhuǎn)成context坐標(biāo)系
CGContextScaleCTM(context, 1, -1);
// 創(chuàng)建繪制區(qū)域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
// 創(chuàng)建需要繪制的文字
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
// 根據(jù)attStr生成CTFramesetterRef
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
// 將frame的內(nèi)容繪制到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]; // 不調(diào)用的話不會(huì)觸發(fā)displayLayer方法
}
@end