iOS性能優化(中級+): 異步繪製

山雨欲來

「砰砰砰、砰砰砰、砰砰砰」git

「大師,大師,江湖救急啊」github

「不知少俠,着急讓老夫出關所爲什麼事?」性能優化

「大師以前授與個人iOS性能優化(初級)iOS性能優化(中級),我已熟悉研讀多日,且勤學苦練,至今已能解決大部分滑動卡頓問題。」bash

「少俠,果真聰慧過人」異步

「可是,最近依然遇到了問題,小師妹想作一個相似於微博主頁的頁面,有不少feed,每一個feed裏面,有話題,連接、圖片、表情、圓角頭像等,這麼多元素雜在一塊兒,縱然我使出畢生所學,卻依然會有卡頓,達不到小師妹對流暢性的要求,因此非常苦惱,懇求大師指點。」async

「原來是這樣,老夫這就來助你突破瓶頸,更上一層樓。」oop

異步繪製

iOS性能優化(初級)iOS性能優化(中級)中,爲了屏幕流暢咱們作了不少,也取得了不錯的成果。但不管怎麼作,最後的繪製是提交給系統的,系統默認是在主線程作這一切,當須要繪製的元素過多,過於頻繁,那麼依然會形成卡頓。post

那麼咱們可不能夠像處理複雜數據同樣,把繪製過程放在後臺線程執行呢?性能

很高興,答案是能夠的。學習

iOS裏面的視圖UIView中有一個CALayer *layer的屬性,UIView的內容,實際上是layer顯示的,layer中有一個屬性id contentscontents的內容就是要顯示的具體內容,大多數狀況下,contents的值是一張圖片。咱們經常使用的不管是 UILabel仍是 UIImageView裏面顯示的內容,其實都是繪製在一張畫布上,繪製完成從畫布中導出圖片,再把圖片賦值給layer.contents就完成了顯示。

異步繪製,就是異步在畫布上繪製內容。

異步繪製

小試鋒芒

Talk is cheap. Show me the code

首先來新建一個AsyncLabel類,而後重寫- (void)displayLayer:(CALayer *)layer方法,在其中進行異步繪製。

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface AsyncLabel : UIView

//設置文字內容
@property(nonatomic, copy) NSString *text;
//設置字體
@property(nonatomic, strong) UIFont *font;

@end

NS_ASSUME_NONNULL_END
複製代碼
#import "AsyncLabel.h"
#import <CoreText/CoreText.h>

@implementation AsyncLabel

- (void)displayLayer:(CALayer *)layer
{
    NSLog(@"是否是主線程 %d", [[NSThread currentThread] isMainThread]);
    //輸出 1 表明是主線程
    //異步繪製,因此咱們在使用了全局子隊列,實際使用中,最好自創隊列
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        __block CGSize size = CGSizeZero;
        __block CGFloat scale = 1.0;
        dispatch_sync(dispatch_get_main_queue(), ^{
            size = self.bounds.size;
            scale = [UIScreen mainScreen].scale;
        });
    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);
       });
    });
}

@end
複製代碼
- (void)draw:(CGContextRef)context size:(CGSize)size
{
    //將座標系上下翻轉。由於底層座標系和UIKit的座標系原點位置不一樣。
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, size.height);
    CGContextScaleCTM(context, 1.0,-1.0);
    
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
    
    //設置內容
    NSMutableAttributedString * attString = [[NSMutableAttributedString alloc] initWithString:self.text];
    //設置字體
    [attString addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
    
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attString.length), path, NULL);
    
    //把frame繪製到context裏
    CTFrameDraw(frame, context);
}
複製代碼

這樣就完成了一個簡單的繪製。在- (void)displayLayer:(CALayer *)layer方法中,在異步線程裏,建立一個畫布並把繪製的結果在主線程中傳給layer.contents

繪製過程使用了CoreText,這裏只簡單的把文字繪製上去,實際使用過程當中,根據須要可能會有不少的地方須要設置,還請少俠自行學習CoreText

調用一下看一下結果:

AsyncLabel *label = [[AsyncLabel alloc] initWithFrame:CGRectMake(50, 200, [UIScreen mainScreen].bounds.size.width - 2 * 50, 100)];
label.backgroundColor = [UIColor lightGrayColor];
label.text = @"今天是個好日子啊,心想的事兒都能成,今天是個好日子啊,啊,安心,太平";
label.font = [UIFont systemFontOfSize:20];
[self.view addSubview:label];
[label.layer setNeedsDisplay];
複製代碼

繪製結果

顯示效果達到。

「多謝大師指點,大師一番操做,讓我茅塞頓開。」

耳目一新

上面的操做是很是常規的操做,在實際使用中還有幾個問題須要解決:

  1. 當AsyncLabel使用在cell中,數量較多,不斷重繪時,要處理好子線程問題,不能放在全局隊列(由於全局隊列中可能有系統提交的任務)。
  2. 對不一樣類型如文字、圖片的封裝性問題。

下面老夫來給少俠介紹一種,全新的解決方式,刷新常規想法,且封裝優秀。

YYAsyncLayer

它的主要處理流程以下:

  1. 在主線程的runLoop中註冊一個observer,它的優先級要比系統的CATransaction要低,保證系統先作完必須的工做。
  2. 把須要異步繪製的操做集中起來。好比設置字體、顏色、背景這些,不是設置一個就繪製一個,把他們都收集起來,runloop會在observer須要的時機通知統一處理。
  3. 處理時機到時,執行異步繪製,並在主線程中把繪製結果傳遞給layer.contents

YYAsyncLayer主要流程

大概瞭解了原理,咱們來使用一下YYAsyncLayer

刪除以前在AsyncLabel.m中使用原始方式異步繪製的代碼加入下列代碼

- (void)setText:(NSString *)text {
    _text = text.copy;
    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)setFont:(UIFont *)font {
    _font = font;
    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)layoutSubviews {
    [super layoutSubviews];
    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)contentsNeedUpdated {
    // do update
    [self.layer setNeedsDisplay];
}
複製代碼

這一些代碼,執行了處理流程中的12,註冊了observer,並收集了要統一處理的操做。

+ (Class)layerClass
{
    return [YYAsyncLayer class];
}

- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
    
    YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
    task.willDisplay = ^(CALayer *layer) {
        //...
    };
    
    task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) {
        if (isCancelled()) {
            return;
        }
        if (!self.text.length) {
            return;
        }
        [self draw:context size:size];
    };
    
    task.didDisplay = ^(CALayer *layer, BOOL finished) {
        if (finished) {
            // finished
        } else {
            // cancelled
        }
    };
    
    return task;
}
複製代碼

這些代碼實現了流程中的3,異步繪製,並提供給使用者willDisplaydisplaydidDisplay幾個block。

有一點須要注意,必須重寫+ (Class)layerClass,纔會進入自定義的subLayer執行方法。至關於打UIView的layer,從默認layer指到subLayer。

繪製結果

得心應手

上述招式,老夫只是簡單演示,但少俠遇到的事要比老夫複雜的多。少俠天資聰慧,切不可傲嬌,還需好生練習並配合runloopCoreText使用,方能得心應手。快去答覆小師妹去罷。

相關文章
相關標籤/搜索