iOS 異步繪制與顯示的工具-YYAsyncLayer源碼和原理解析

眾所周知,在iOS系統(tǒng)中买乃,UI相關(guān)操作和用戶的操作都是在主線程中進(jìn)行姻氨,大量的UI操作,會造成主線程阻塞为牍,影響應(yīng)用的流暢度和用戶體驗(yàn)哼绑,為了保證APP可以及時的響應(yīng)用戶的操作,所以一些UI的繪制工作碉咆,最好放到子線程抖韩,進(jìn)行異步繪制,減輕主線程的工作疫铜,因此YY作者寫出了一個異步繪制的工具YYAsyncLayer茂浮。

YYAsyncLayer 是 CALayer 的子類,當(dāng)它需要顯示內(nèi)容(比如調(diào)用了 [layer setNeedDisplay])時壳咕,它會向 delegate席揽,也就是 UIView 請求一個異步繪制的任務(wù)。在異步繪制時谓厘,Layer 會傳遞一個 BOOL(^isCancelled)() 這樣的 block幌羞,繪制代碼可以隨時調(diào)用該 block 判斷繪制任務(wù)是否已經(jīng)被取消。

當(dāng) TableView 快速滑動時竟稳,會有大量異步繪制任務(wù)提交到后臺線程去執(zhí)行属桦。但是有時滑動速度過快時,繪制任務(wù)還沒有完成就可能已經(jīng)被取消了他爸。如果這時仍然繼續(xù)繪制聂宾,就會造成大量的 CPU 資源浪費(fèi),甚至阻塞線程并造成后續(xù)的繪制任務(wù)遲遲無法完成诊笤。我的做法是盡量快速系谐、提前判斷當(dāng)前繪制任務(wù)是否已經(jīng)被取消;在繪制每一行文本前讨跟,我都會調(diào)用 isCancelled() 來進(jìn)行判斷纪他,保證被取消的任務(wù)能及時退出鄙煤,不至于影響后續(xù)操作。iOS 保持界面流暢的技巧

一止喷、YYAsyncLayer 文件類組成

YYAsyncLayer中主要有三個類

YYTransaction:注冊一個通知馆类,在監(jiān)控runLoop睡眠和退出,來執(zhí)行任務(wù)回調(diào)弹谁,利用runloop空閑,執(zhí)行任務(wù)句喜。(如果你想知道為什么要在睡眠和退出的時候预愤,執(zhí)行任務(wù),你可以看下YY的這篇博客了解下深入理解RunLoop)

YYSentine:線程安全的計(jì)數(shù)器咳胃,通過判斷計(jì)數(shù)器的值是否相等植康,來判斷異步繪制任務(wù)是否被取消。

YYAsyncLayer:異步渲染的核心類展懈,是CALayer子類销睁,用來異步渲染layer內(nèi)容。

二存崖、YYAsyncLayer 源碼分析

1.YYTransaction源碼分析

YYTransaction繪制任務(wù)的機(jī)制是仿照CoreAnimation的繪制機(jī)制冻记,監(jiān)聽主線程RunLoop,在空閑階段插入繪制任務(wù)来惧,并將任務(wù)優(yōu)先級設(shè)置在CoreAnimation繪制完成之后冗栗,然后遍歷繪制任務(wù)集合進(jìn)行繪制工作并且清空集合,具體可以看源碼供搀。

/**
 YYTransaction let you perform a selector once before current runloop sleep.
 */
@interface YYTransaction : NSObject

/**
 Creates and returns a transaction with a specified target and selector.
 
 @param target    A specified target, the target is retained until runloop end.
 @param selector  A selector for target.
 
 @return A new transaction, or nil if an error occurs.
 */
+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector;

/**
 Commit the trancaction to main runloop.
 
 @discussion It will perform the selector on the target once before main runloop's
 current loop sleep. If the same transaction (same target and same selector) has 
 already commit to runloop in this loop, this method do nothing.
 */
- (void)commit;

@end

上面的YYTransaction.h文件中有兩個方法:
第一個方法是+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector隅居,根據(jù)傳入的target和selector來創(chuàng)建一個任務(wù)。
第二個方法是- (void)commit;葛虐,它用來在runloop睡眠的時候胎源,執(zhí)行傳入任務(wù),并且對于相同的任務(wù)屿脐,在runloop中只執(zhí)行一次涕蚤。

看到這里你應(yīng)該有兩個疑問,
第一是如何來實(shí)現(xiàn)在runLoop將要休眠的時候摄悯,來執(zhí)行傳進(jìn)來的任務(wù)赞季??奢驯?
第二是如何保證相同的任務(wù)只執(zhí)行一次申钩??瘪阁?

what撒遣?why邮偎?.jpg

那么下面我們來看下源碼,分析上面的兩個問題??

// 注冊 Runloop Observer
static void YYTransactionSetup() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        transactionSet = [NSMutableSet new];
        CFRunLoopRef runloop = CFRunLoopGetMain();
        CFRunLoopObserverRef observer;
/**
         創(chuàng)建一個RunLoop的觀察者
         allocator:該參數(shù)為對象內(nèi)存分配器义黎,一般使用默認(rèn)的分配器kCFAllocatorDefault禾进。或者nil
         activities:該參數(shù)配置觀察者監(jiān)聽Run Loop的哪種運(yùn)行狀態(tài)廉涕,這里我們監(jiān)聽beforeWaiting和exit狀態(tài)
         repeats:CFRunLoopObserver是否循環(huán)調(diào)用泻云。
         order:CFRunLoopObserver的優(yōu)先級,當(dāng)在Runloop同一運(yùn)行階段中有多個CFRunLoopObserver時狐蜕,根據(jù)這個來先后調(diào)用CFRunLoopObserver宠纯,0為最高優(yōu)先級別。正常情況下使用0层释。
         callout:觀察者的回調(diào)函數(shù)婆瓜,在Core Foundation框架中用CFRunLoopObserverCallBack重定義了回調(diào)函數(shù)的閉包。
         context:觀察者的上下文贡羔。 (類似與KVO傳遞的context廉白,可以傳遞信息,)因?yàn)檫@個函數(shù)創(chuàng)建ovserver的時候需要傳遞進(jìn)一個函數(shù)指針乖寒,而這個函數(shù)指針可能用在n多個oberver 可以當(dāng)做區(qū)分是哪個observer的狀機(jī)態(tài)猴蹂。(下面的通過block創(chuàng)建的observer一般是一對一的,一般也不需要Context宵统,)晕讲,還有一個例子類似與NSNOtificationCenter的 SEL和 Block方式
         */
        observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                           kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                           true,      // repeat
                                           0xFFFFFF,  // after CATransaction(2000000)
                                           YYRunLoopObserverCallBack, NULL);
        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    });
}

//監(jiān)聽回調(diào)的方法
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (transactionSet.count == 0) return;
    NSSet *currentSet = transactionSet;
    transactionSet = [NSMutableSet new];
    [currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
    }];
}

上面的代碼就是runLoop將要休眠的時候,執(zhí)行任務(wù)的核心代碼马澈,
首先我們來看一下YYTransactionSetup方法中CFRunLoopObserverCreate函數(shù)的參數(shù)選绕笆 :
1. 任務(wù)執(zhí)行的時機(jī)
從上面源碼可以看出是在 kCFRunLoopBeforeWaiting | kCFRunLoopExit,也就是在將要睡眠和推出的時候來執(zhí)行痊班。
2.執(zhí)行的優(yōu)先級
從上面源碼可以看出是0xFFFFFF, // after CATransaction(2000000)勤婚,這是在CoreAnimation繪制完成之后之后執(zhí)行。
3.執(zhí)行的runLoop
從上面源碼可以看出是 CFRunLoopGetMain();也就是主線程的runLoop中執(zhí)行這個回調(diào)涤伐,這是因?yàn)閳?zhí)行的任務(wù)跟UI相關(guān)馒胆,必須要在主線程執(zhí)行。

然后我們看下回調(diào)方法凝果,是通過執(zhí)行 transactionSet 中的 transaction來執(zhí)行具體的方法祝迂,看到這個第一個問題“如何來實(shí)現(xiàn)在runLoop將要休眠的時候,執(zhí)行任務(wù)”已經(jīng)有答案了吧器净。

下面我們來看下“如何保證相同的任務(wù)只執(zhí)行一次型雳???”

- (void)commit {
    if (!_target || !_selector) return;
    YYTransactionSetup();
    [transactionSet addObject:self];
}

- (NSUInteger)hash {
    long v1 = (long)((void *)_selector);
    long v2 = (long)_target;
    return v1 ^ v2;
}

- (BOOL)isEqual:(id)object {
    if (self == object) return YES;
    if (![object isMemberOfClass:self.class]) return NO;
    YYTransaction *other = object;
    return other.selector == _selector && other.target == _target;
}

就是在commit的時候?qū)ransaction添加到transactionSet中纠俭,而我們知道NSMutableSet中不會出現(xiàn)相同的對象沿量,所以這就實(shí)現(xiàn)了相同的任務(wù)只執(zhí)行一次,同事由于NSMutableSet中是通過isEqual和hash來判斷對象是否相同的冤荆,所以將這兩個方法重寫,保證transactionSet中任務(wù)的唯一性钓简。

2.** YYAsyncLayer源碼分析**

YYAsyncLayer為了異步繪制而繼承CALayer的子類。通過使用CoreGraphic相關(guān)方法涌庭,在子線程中繪制內(nèi)容Context芥被,繪制完成后坐榆,回到主線程對layer.contents進(jìn)行直接顯示冗茸。

@interface YYAsyncLayer : CALayer
/// Whether the render code is executed in background. Default is YES.
@property BOOL displaysAsynchronously;
@end

@protocol YYAsyncLayerDelegate <NSObject>
@required
/// This method is called to return a new display task when the layer's contents need update.
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;
@end

/**
 A display task used by YYAsyncLayer to render the contents in background queue.
 */
@interface YYAsyncLayerDisplayTask : NSObject

@property (nullable, nonatomic, copy) void (^willDisplay)(CALayer *layer);

@property (nullable, nonatomic, copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void));

@property (nullable, nonatomic, copy) void (^didDisplay)(CALayer *layer, BOOL finished);

@end

YYAsyncLayer中主要有三部分:
1. YYAsyncLayerDelegate
YYAsyncLayerDelegate 的 newAsyncDisplayTask 是提供了 YYAsyncLayer 需要在后臺隊(duì)列繪制的內(nèi)容

2. YYAsyncLayerDisplayTask
display 在mainthread或者background thread調(diào)用 這要求 display 應(yīng)該是線程安全的
willdisplay 和 didDisplay 在 mainthread 調(diào)用。

3.YYAsyncLayer
YYAsyncLayer是通過創(chuàng)建異步創(chuàng)建圖像Context在其繪制夏漱,最后再主線程異步添加圖像從而實(shí)現(xiàn)的異步繪制。同時挂绰,在繪制過程中進(jìn)行了多次進(jìn)行取消判斷屎篱,以避免額外繪制.

YYAsyncLayer如何實(shí)現(xiàn)異步繪制和取消繪制功能:
1)異步繪制
通過 重寫display 方法,調(diào)用- (void)_displayAsync:(BOOL)async葵蒂,在后臺線程中調(diào)用task.display 進(jìn)行繪制交播,最終在主線程中將繪制圖片賦值給self.contents。

2)是否取消繪制
通過isCancelled來判斷是否取消繪制践付,主要是利用了局部變量被block捕獲后秦士,在block中value就不會改變,通過判斷block中的value和外部的value值是否相等永高,來判斷任務(wù)是否已經(jīng)取消隧土。

        YYSentinel *sentinel = _sentinel;
        int32_t value = sentinel.value;
        BOOL (^isCancelled)() = ^BOOL() {
            return value != sentinel.value;
        };

3)如何創(chuàng)建隊(duì)列
通過[NSProcessInfo processInfo].activeProcessorCount控制隊(duì)列的最大數(shù)量和cpu的數(shù)量保持一致,因?yàn)榫€程的切換也是需要額外的開銷的命爬。所以線程不是越多曹傀,執(zhí)行效率越高。

三饲宛、YYAsyncLayer 問題總結(jié)

1. YYTransaction中皆愉,如何在runLoop將要休眠的時候,來執(zhí)行傳進(jìn)來的任務(wù)?
通過YYTransactionSetup方法中執(zhí)行CFRunLoopObserverCreate函數(shù)實(shí)現(xiàn)亥啦,具體的函數(shù)參數(shù)選取如下:
1) 任務(wù)執(zhí)行的時機(jī)
從上面源碼可以看出是在 kCFRunLoopBeforeWaiting | kCFRunLoopExit炭剪,也就是在將要睡眠和推出的時候來執(zhí)行。
2)執(zhí)行的優(yōu)先級
從上面源碼可以看出是0xFFFFFF, // after CATransaction(2000000)翔脱,這是在CoreAnimation繪制完成之后之后執(zhí)行奴拦。
3)執(zhí)行的runLoop
從上面源碼可以看出是 CFRunLoopGetMain();也就是主線程的runLoop中執(zhí)行這個回調(diào),這是因?yàn)閳?zhí)行的任務(wù)跟UI相關(guān)届吁,必須要在主線程執(zhí)行错妖。

2. YYTransaction如何保證相同的任務(wù)(transaction)只執(zhí)行一次?
通過在commit的時候?qū)ransaction添加到transactionSet中疚沐,而我們知道NSMutableSet中不會出現(xiàn)相同的對象暂氯,所以這就實(shí)現(xiàn)了相同的任務(wù)只執(zhí)行一次,同事由于NSMutableSet中是通過isEqual和hash來判斷對象是否相同的亮蛔,所以將這兩個方法重寫痴施,保證transactionSet中任務(wù)的唯一性。

3.YYAsyncLayer如何實(shí)現(xiàn)異步繪制和取消繪制功能
1)異步繪制
通過 重寫display 方法究流,調(diào)用- (void)_displayAsync:(BOOL)async辣吃,在后臺線程中調(diào)用task.display 進(jìn)行繪制,最終在主線程中將繪制圖片賦值給self.contents芬探。

2)是否取消繪制
通過isCancelled來判斷是否取消繪制神得,主要是利用了局部變量被block捕獲后,在block中value就不會改變偷仿,通過判斷block中的value和外部的value值是否相等哩簿,來判斷任務(wù)是否已經(jīng)取消。

        YYSentinel *sentinel = _sentinel;
        int32_t value = sentinel.value;
        BOOL (^isCancelled)() = ^BOOL() {
            return value != sentinel.value;
        };

3)如何創(chuàng)建隊(duì)列
通過[NSProcessInfo processInfo].activeProcessorCount控制隊(duì)列的最大數(shù)量和cpu的數(shù)量保持一致酝静,因?yàn)榫€程的切換也是需要額外的開銷的节榜。所以線程不是越多,執(zhí)行效率越高全跨。

四亿遂、YYAsyncLayer 相關(guān)知識點(diǎn)總結(jié)

1.CFRunLoopObserverCreate函數(shù)使用,創(chuàng)建runLoop監(jiān)聽蛇数。
2.NSMutableSet使用,一直相同對象判斷條件hash和isEqual碌上。
3.OSAtomicIncrement32線程安全的自增計(jì)數(shù),每調(diào)用一次+1倚评。
4.GCD相關(guān)隊(duì)列天梧,dispatch_once等使用霞丧。
5.block捕獲局部變量,值不會改變特性蛹尝。
6.CoreGraphics相關(guān)繪制API的使用。

參考資料:
https://github.com/ibireme/YYAsyncLayer
https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/
https://blog.ibireme.com/2015/05/18/runloop/
https://juejin.im/post/5a0a52b5f265da43247ff4ad
http://www.reibang.com/p/58e7571d7806
iOS的異步繪制--YYAsyncLayer源碼分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挫酿,一起剝皮案震驚了整個濱河市愕难,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌猫缭,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異茫打,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)老赤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門抬旺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人开财,你說我怎么就攤上這事≡瘅ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵正塌,是天一觀的道長。 經(jīng)常有香客問我帜羊,道長,這世上最難降的妖魔是什么讼育? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任粮宛,我火速辦了婚禮,結(jié)果婚禮上巍杈,老公的妹妹穿的比我還像新娘。我一直安慰自己词裤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布吼砂。 她就那樣靜靜地躺著鼎文,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拇惋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天蓉坎,我揣著相機(jī)與錄音胡嘿,去河邊找鬼。 笑死衷敌,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的逢享。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼弓柱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了矢空?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤屁药,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后复亏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缔御,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年耕突,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眷茁。...
    茶點(diǎn)故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡纵诞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出浙芙,到底是詐尸還是另有隱情,我是刑警寧澤茁裙,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布晤锥,位于F島的核電站廊宪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏箭启。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一放妈、第九天 我趴在偏房一處隱蔽的房頂上張望北救。 院中可真熱鬧芜抒,春花似錦、人聲如沸宅倒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽线召。三九已至,卻和暖如春灶搜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背割卖。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鹏溯,地道東北人。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓肺孵,卻偏偏與公主長得像颜阐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子凳怨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評論 2 348