iOS-UIView異步繪制

1.異步繪制原理

UIView 中有一個 CALayer 的屬性,負責 UIView 具體內容的顯示。具體過程是系統會把 UIView 顯示的內容(包括 UILabel 的文字,UIImageView 的圖片等)繪制在一張畫布上,完成后倒出圖片賦值給 CALayercontents 屬性,完成顯示。

這其中的工作都是在主線程中完成的,這就導致了主線程頻繁的處理 UI 繪制的工作,如果要繪制的元素過多,過于頻繁,就會造成卡頓。

那么是否可以將復雜的繪制過程放到后臺線程中執行,從而減輕主線程負擔,來提升 UI 流暢度呢?

答案是可以的,系統給我們留下的異步繪制的口子,請看下面的流程圖,它是我們進行基本繪制的基礎。

UIView繪制流程
  1. UIView調用setNeedsDisplay并沒有立刻進行繪制;
  2. UIView調用setNeedsDisplay方法其實是調用其layer屬性的同名方法;
  3. 在當前runloop即將結束時調用display進入繪制流程;
  4. UIViewlayer.delegate就是UIView本身,UIView 并沒有實現displayLayer:方法,所以進入系統的繪制流程,我們可以通過實現displayLayer:方法來進行異步繪制。

有了上面的異步繪制原理流程圖,我們可以得到一個實現異步繪制的初步思路:在“異步繪制入口”去開辟子線程,然后在子線程中實現和系統類似的繪制流程。

2.系統繪制流程

要實現異步繪制,首先要了解系統的繪制流程,看下面一張流程圖:

系統繪制流程
  1. CALayer在內部創建一個上下文環境(CGContextRef);
  2. 判斷layer是否有代理:
    沒有代理:調用layerdrawInContext:方法,
    有代理:調用delegatedrawLayer:inContext:方法,然后在合適的時機回調代理,在[UIView drawRect]中進行UI的繪制工作,
  3. 最后layer上傳backingStorebitmap位圖到GPU,也就是將生成的bitmap位圖賦值給layer.content屬性,結束系統繪制流程。

3.異步繪制流程

關于異步繪制,參考如下圖:

異步繪制流程
  1. 某個時機調用setNeedsDisplay;
  2. runloop將要結束時調用[CALayer display];
  3. 若代理實現了displayLayer將會調用此方法,在子線程中做異步繪制的工作;
  4. 在子線程中創建上下文、繪制控件并生成圖片;
  5. 在主線程中設置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
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。