一、異步繪制產(chǎn)生背景
UIView 中有一個 CALayer 的屬性接癌,負責 UIView 具體內(nèi)容的顯示。
具體過程是系統(tǒng)會把 UIView 顯示的內(nèi)容(包括 UILabel 的文字扣讼,UIImageView 的圖片等)繪制在一張畫布上缺猛,完成后倒出圖片賦值給 CALayer 的 contents 屬性,完成顯示椭符。
這其中的工作都是在主線程中完成的荔燎,這就導致了主線程頻繁的處理 UI 繪制的工作,如果要繪制的元素過多销钝,過于頻繁有咨,就會造成卡頓。
解決方案使用異步繪制就是:
把UIView 顯示的內(nèi)容(包括 UILabel 的文字蒸健,UIImageView 的圖片等)繪制生成的bitmap在子線程完成座享。
然后在回到主線程把bitmap賦值給view.layer.content屬性。
二似忧、異步繪制流程
那么是否可以將復雜的繪制過程放到后臺線程中執(zhí)行渣叛,從而減輕主線程負擔,來提升UI流暢度呢盯捌?
可以的淳衙,系統(tǒng)給我們留下的異步繪制的口子,請看下面的流程圖饺著,它是我們進行基本繪制的基礎(chǔ):
首先 UIView 調(diào)用 setNeedsDisplay 方法
其實是調(diào)用其 layer 屬性的同名方法(view.layer setNeedsDisplay)
這時 layer 并不會立刻調(diào)用 display 方法,而是要等到當前 runloop 即將結(jié)束的時候調(diào)用 display滤祖,進入到繪制流程。
在 UIView 中 layer.delegate 就是 UIView 本身瓶籽,UIView 并沒有實現(xiàn) displayLayer: 方法匠童,所以進入系統(tǒng)的繪制流程,我們可以通過實現(xiàn) displayLayer: 方法來進行異步繪制塑顺。
所以去實現(xiàn)displayLayer方式汤求,實現(xiàn)開啟異步繪制入口俏险,
在“異步繪制入口”去開辟子線程,然后在子線程中實現(xiàn)和系統(tǒng)類似的繪制流程扬绪。
三竖独、系統(tǒng)繪制流程
先從下面的系統(tǒng)繪制流程圖來了解一下系統(tǒng)繪制流程:
首先 CALayer 會在內(nèi)部創(chuàng)建 一個上下文環(huán)境(CGContextRef)
-
然后判斷 layer 是否有代理:
沒有代理的話,就調(diào)用 layer 的 drawInContext: 方法
有代理的話挤牛,調(diào)用 delegate 的drawLayer : inContext 方法莹痢,這個方法實現(xiàn)是系統(tǒng)完成。
然后在合適的時機回調(diào)代理墓赴,調(diào)用drawRect默認操作是什么都不做(而之所以有這個接口,就是為了讓我們在系統(tǒng)繪制之后,還可以做些自定義的繪制工作)竞膳。
最后無論是哪個分支都把 backing store(上下文環(huán)境) 的 bitmap 位圖提交到 GPU,
也就是將生成的 bitmap 位圖賦值給 layer.content 屬性诫硕。
下面看一下異步繪制的時序圖能更好的理解異步繪制流程:
首先在主線程調(diào)用setNeedsdispay方法
系統(tǒng)會在runloop將要結(jié)束的時候調(diào)用[CAlayer display]方法
-
如果我們的代理實現(xiàn)了dispayLayer這個方法坦辟,會調(diào)用dispayLayer這個方法。我們可以去子線程里面進行異步繪制章办。子線程主要做的工作:
- 創(chuàng)建上下文
- UI控件的繪制工作
- 生成對應的圖片(bitmap)
主線程可以做其他工作
異步繪制完事之后锉走,回到主線程,把繪制的bitmap賦值view.layer.contents屬性中
四藕届、面試考點
一挪蹭、我們調(diào)用[UIView setNeedsDisplay]方法的時候,不會立馬發(fā)送對應視圖的繪制工作休偶,為什么梁厉?
調(diào)用[UIView setNeedsDisplay]后,
然后會調(diào)用系統(tǒng)的同名方法[view.layer setNeedsDisplay]方法并在當前view上面打上一個臟標記
當前Runloop將要結(jié)束的時候才會調(diào)用[CALyer display]方法椅贱,然后進入到視圖真正的繪制工作當中懂算。
二只冻、是否知道異步繪制庇麦?如何進行異步繪制?
- 基于系統(tǒng)開的口子[layer.delegate dispayLayer:]方法喜德。
- 并且實現(xiàn)/遵從了dispayLayer這個方法山橄,我們就可以進行異步繪制:
1)代理負責生產(chǎn)對應的bitmap
2)設(shè)置bitmap作為layer.contents屬性的值
五、異步繪代碼:
#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用法簡介:[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 {
// 將坐標系上下翻轉(zhuǎn),因為底層坐標系和 UIKit 坐標系原點位置不同萌衬。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
// 文本沿著Y軸移動
CGContextTranslateCTM(context, 0, size.height); // 原點為左下角
// 文本反轉(zhuǎn)成context坐標系
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);
}
簡單的調(diào)用:
#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)用的話不會觸發(fā)displayLayer方法
}
@end