iOS UIView 異步繪制

一、異步繪制原理

在 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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容