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

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

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

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

一、YYAsyncLayer 文件類組成

YYAsyncLayer中主要有三個(gè)類

YYTransaction:注冊一個(gè)通知,在監(jiān)控runLoop睡眠和退出,來執(zhí)行任務(wù)回調(diào),利用runloop空閑,執(zhí)行任務(wù)。(如果你想知道為什么要在睡眠和退出的時(shí)候,執(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)先級(jí)設(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文件中有兩個(gè)方法:
第一個(gè)方法是+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector,根據(jù)傳入的target和selector來創(chuàng)建一個(gè)任務(wù)。
第二個(gè)方法是- (void)commit;,它用來在runloop睡眠的時(shí)候,執(zhí)行傳入任務(wù),并且對于相同的任務(wù),在runloop中只執(zhí)行一次。

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

what?why?.jpg

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

// 注冊 Runloop Observer
static void YYTransactionSetup() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        transactionSet = [NSMutableSet new];
        CFRunLoopRef runloop = CFRunLoopGetMain();
        CFRunLoopObserverRef observer;
/**
         創(chuàng)建一個(gè)RunLoop的觀察者
         allocator:該參數(shù)為對象內(nèi)存分配器,一般使用默認(rèn)的分配器kCFAllocatorDefault?;蛘遪il
         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)先級(jí),當(dāng)在Runloop同一運(yùn)行階段中有多個(gè)CFRunLoopObserver時(shí),根據(jù)這個(gè)來先后調(diào)用CFRunLoopObserver,0為最高優(yōu)先級(jí)別。正常情況下使用0。
         callout:觀察者的回調(diào)函數(shù),在Core Foundation框架中用CFRunLoopObserverCallBack重定義了回調(diào)函數(shù)的閉包。
         context:觀察者的上下文。 (類似與KVO傳遞的context,可以傳遞信息,)因?yàn)檫@個(gè)函數(shù)創(chuàng)建ovserver的時(shí)候需要傳遞進(jìn)一個(gè)函數(shù)指針,而這個(gè)函數(shù)指針可能用在n多個(gè)oberver 可以當(dāng)做區(qū)分是哪個(gè)observer的狀機(jī)態(tài)。(下面的通過block創(chuàng)建的observer一般是一對一的,一般也不需要Context,),還有一個(gè)例子類似與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將要休眠的時(shí)候,執(zhí)行任務(wù)的核心代碼
首先我們來看一下YYTransactionSetup方法中CFRunLoopObserverCreate函數(shù)的參數(shù)選?。?br> 1. 任務(wù)執(zhí)行的時(shí)機(jī)
從上面源碼可以看出是在 kCFRunLoopBeforeWaiting | kCFRunLoopExit,也就是在將要睡眠和推出的時(shí)候來執(zhí)行。
2.執(zhí)行的優(yōu)先級(jí)
從上面源碼可以看出是0xFFFFFF, // after CATransaction(2000000),這是在CoreAnimation繪制完成之后之后執(zhí)行。
3.執(zhí)行的runLoop
從上面源碼可以看出是 CFRunLoopGetMain();也就是主線程的runLoop中執(zhí)行這個(gè)回調(diào),這是因?yàn)閳?zhí)行的任務(wù)跟UI相關(guān),必須要在主線程執(zhí)行。

然后我們看下回調(diào)方法,是通過執(zhí)行 transactionSet 中的 transaction來執(zhí)行具體的方法,看到這個(gè)第一個(gè)問題“如何來實(shí)現(xiàn)在runLoop將要休眠的時(shí)候,執(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的時(shí)候?qū)ransaction添加到transactionSet中,而我們知道NSMutableSet中不會(huì)出現(xiàn)相同的對象,所以這就實(shí)現(xiàn)了相同的任務(wù)只執(zhí)行一次,同事由于NSMutableSet中是通過isEqual和hash來判斷對象是否相同的,所以將這兩個(gè)方法重寫,保證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 需要在后臺(tái)隊(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)的異步繪制。同時(shí),在繪制過程中進(jìn)行了多次進(jìn)行取消判斷,以避免額外繪制.

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

2)是否取消繪制
通過isCancelled來判斷是否取消繪制,主要是利用了局部變量被block捕獲后,在block中value就不會(huì)改變,通過判斷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將要休眠的時(shí)候,來執(zhí)行傳進(jìn)來的任務(wù)?
通過YYTransactionSetup方法中執(zhí)行CFRunLoopObserverCreate函數(shù)實(shí)現(xiàn),具體的函數(shù)參數(shù)選取如下:
1) 任務(wù)執(zhí)行的時(shí)機(jī)
從上面源碼可以看出是在 kCFRunLoopBeforeWaiting | kCFRunLoopExit,也就是在將要睡眠和推出的時(shí)候來執(zhí)行。
2)執(zhí)行的優(yōu)先級(jí)
從上面源碼可以看出是0xFFFFFF, // after CATransaction(2000000),這是在CoreAnimation繪制完成之后之后執(zhí)行。
3)執(zhí)行的runLoop
從上面源碼可以看出是 CFRunLoopGetMain();也就是主線程的runLoop中執(zhí)行這個(gè)回調(diào),這是因?yàn)閳?zhí)行的任務(wù)跟UI相關(guān),必須要在主線程執(zhí)行。

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

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

2)是否取消繪制
通過isCancelled來判斷是否取消繪制,主要是利用了局部變量被block捕獲后,在block中value就不會(huì)改變,通過判斷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)知識(shí)點(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捕獲局部變量,值不會(huì)改變特性。
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.itdecent.cn/p/58e7571d7806
iOS的異步繪制--YYAsyncLayer源碼分析

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

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