iOS 之 異步繪制原理

一、異步繪制產(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ǔ):

異步繪制流程圖.png
  1. 首先 UIView 調(diào)用 setNeedsDisplay 方法

  2. 其實是調(diào)用其 layer 屬性的同名方法(view.layer setNeedsDisplay)

  3. 這時 layer 并不會立刻調(diào)用 display 方法,而是要等到當前 runloop 即將結(jié)束的時候調(diào)用 display滤祖,進入到繪制流程。

  4. 在 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)繪制流程:


系統(tǒng)繪制流程.png
  • 首先 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 屬性诫硕。

下面看一下異步繪制的時序圖能更好的理解異步繪制流程:

異步繪制時序圖.png
  • 首先在主線程調(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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饮醇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子秕豫,更是在濱河造成了極大的恐慌朴艰,老刑警劉巖观蓄,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異祠墅,居然都是意外死亡侮穿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門毁嗦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亲茅,“玉大人,你說我怎么就攤上這事狗准】寺啵” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵驶俊,是天一觀的道長娶耍。 經(jīng)常有香客問我,道長饼酿,這世上最難降的妖魔是什么榕酒? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮故俐,結(jié)果婚禮上想鹰,老公的妹妹穿的比我還像新娘。我一直安慰自己药版,他們只是感情好辑舷,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著槽片,像睡著了一般何缓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上还栓,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天碌廓,我揣著相機與錄音,去河邊找鬼剩盒。 笑死谷婆,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的辽聊。 我是一名探鬼主播纪挎,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼跟匆!你這毒婦竟也來了异袄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤玛臂,失蹤者是張志新(化名)和其女友劉穎烤蜕,沒想到半個月后埠帕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡玖绿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年敛瓷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斑匪。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡呐籽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蚀瘸,到底是詐尸還是另有隱情狡蝶,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布贮勃,位于F島的核電站贪惹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一衷模、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧硼端,春花似錦、人聲如沸寓搬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽句喷。三九已至镣典,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間唾琼,已是汗流浹背兄春。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留父叙,地道東北人神郊。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓肴裙,卻偏偏與公主長得像趾唱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蜻懦,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 類 分類的作用甜癞? 聲明私有方法,分解體積宛乃。分類的特點悠咱? 運行時決議蒸辆,可以為系統(tǒng)類添加分類分類可以添加哪些內(nèi)容?分類...
    Alex1989閱讀 2,099評論 0 26
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,321評論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,089評論 1 32
  • CALayer通過四個屬性來確定大小和位置, 分別為:frame析既、bounds躬贡、position、anchorPo...
    一川煙草i蓑衣閱讀 395評論 0 1
  • UIView和CALayer之間的關(guān)系 UIView繪制原理 圖像顯示原理UI卡頓眼坏、掉幀的原因解決方案 離屏渲染何...
    Mark_Guan閱讀 1,458評論 0 9