YYAsyncLayer 源碼剖析:異步繪制

系列文章:
YYText 源碼剖析:CoreText 與異步繪制
YYAsyncLayer 源碼剖析:異步繪制
YYCache 源碼剖析:一覽亮點
YYModel 源碼剖析:關(guān)注性能
YYImage 源碼剖析:圖片處理技巧
YYWebImage 源碼剖析:線程處理與緩存策略

引言

性能優(yōu)化一直是 iOS 開發(fā)中的一個重頭戲,其中界面流暢度的優(yōu)化是至關(guān)重要的,因為它直接關(guān)系到用戶體驗。從最熟悉和簡單的 UIKit 框架到 CoreAnimation、CoreGraphics、CoreText 甚至是 OpenGL,優(yōu)化似乎是無窮無盡,也非??简為_發(fā)者的水平。

YYAsyncLayer 是 ibireme 寫的一個異步繪制的輪子,雖然代碼加起來才 300 行左右,但質(zhì)量比較高,涉及到很多優(yōu)化思維,值得學(xué)習(xí)。

可能很多人學(xué)習(xí)優(yōu)秀源碼陷入了一個誤區(qū),僅僅是閱讀而不理解。

我們應(yīng)該多思考作者為什么這樣寫,而不是僅僅看懂代碼的表面意思。因為看懂 API 很簡單,這不應(yīng)該是閱讀源碼最關(guān)注的東西,關(guān)注的層次不同自然決定了開發(fā)者的高度。

源碼基于 1.0.0 版本。

一、框架概述

YYAsyncLayer 庫代碼很清晰,就幾個文件:

YYAsyncLayer.h (.m)
YYSentinel.h (.m)
YYTransaction.h (.m)
  • YYAsyncLayer 類繼承自 CALayer ,不同的是作者封裝了異步繪制的邏輯便于使用。
  • YYSentinel 類是一個計數(shù)的類,是為了記錄最新的布局請求標(biāo)識,便于及時的放棄多余的繪制邏輯以減少開銷。
  • YYTransaction 類是事務(wù)類,捕獲主線程 runloop 的某個時機(jī)回調(diào),用于處理異步繪制事件。

可能有些讀者會迷糊,不過沒關(guān)系,后文會詳細(xì)剖析代碼細(xì)節(jié),這里只需要對框架有個大致的認(rèn)識就可以了。

瀏覽一下源碼便可以知道,該框架的用法不過是使用一個 CALayer 的子類 —— YYAsyncLayer。(需要實現(xiàn) YYAsyncLayer 類指定的代理方法,對整個繪制流程做管理,詳細(xì)使用方法可以看看框架的 README

二、為什么需要異步繪制?

1、界面卡頓的實質(zhì)

iOS 設(shè)備顯示器每繪制完一幀畫面,復(fù)位時就會發(fā)送一個 VSync (垂直同步信號) ,并且此時切換幀緩沖區(qū) (iOS 設(shè)備是雙緩存+垂直同步);在讀取經(jīng) GPU 渲染完成的幀緩沖區(qū)數(shù)據(jù)進(jìn)行繪制的同時,還會通過 CADisplayLink 等機(jī)制通知 APP 內(nèi)部可以提交結(jié)果到另一個空閑的幀緩沖區(qū)了;接著 CPU 計算 APP 布局,計算完成交由 GPU 渲染,渲染完成提交到幀緩沖區(qū);當(dāng) VSync 再一次到來的時候,切換幀緩沖區(qū)......
(ps: 上面這段描述是筆者的理解,參考 iOS 保持界面流暢的技巧

當(dāng) VSync 到來準(zhǔn)備切換幀緩沖區(qū)時,若空閑的幀緩存區(qū)并未收到來自 GPU 的提交,此次切換就會作罷,設(shè)備顯示系統(tǒng)會放棄此次繪制,從而引起掉幀。

由此可知,不管是 CPU 還是 GPU 哪一個出現(xiàn)問題導(dǎo)致不能及時的提交渲染結(jié)果到幀緩沖區(qū),都會導(dǎo)致掉幀。優(yōu)化界面流暢程度,實際上就是減少掉幀(iOS設(shè)備上大致是 60 FPS),也就是減小 CPU 和 GPU 的壓力提高性能。

2、UIKit 性能瓶頸

大部分 UIKit 組件的繪制是在主線程進(jìn)行,需要 CPU 來進(jìn)行繪制,當(dāng)同一時刻過多組件需要繪制或者組件元素過于復(fù)雜時,必然會給 CPU 帶來壓力,這個時候就很容易掉幀(主要是文本控件,大量文本內(nèi)容的計算和繪制過程都相當(dāng)繁瑣)。

3、UIKit 替代方案:CoreAnimation 或 CoreGraphics

當(dāng)然,首選優(yōu)化方案是 CoreAnimation 框架。CALayer 的大部分屬性都是由 GPU 繪制的 (硬件層面),不需要 CPU (軟件層面) 做任何繪制。CA 框架下的 CAShapeLayer (多邊形繪制)、CATextLayer(文本繪制)、CAGradientLayer (漸變繪制) 等都有較高的效率,非常實用。

再來看一下 CoreGraphics 框架,實際上它是依托于 CPU 的軟件繪制。在實現(xiàn)CALayerDelegate 協(xié)議的 -drawLayer:inContext: 方法時(等同于UIView 二次封裝的 -drawRect:方法),需要分配一個內(nèi)存占用較高的上下文context,與此同時,CALayer 或者其子類需要創(chuàng)建一個等大的寄宿圖contents。當(dāng)基于 CPU 的軟件繪制完成,還需要通過 IPC (進(jìn)程間通信) 傳遞給設(shè)備顯示系統(tǒng)。值得注意的是:當(dāng)重繪時需要抹除這個上下文重新分配內(nèi)存。

不管是創(chuàng)建上下文、重繪帶來的內(nèi)存重新分配、IPC 都會帶來性能上的較大開銷。所以 CoreGraphics 的性能比較差,日常開發(fā)中要盡量避免直接在主線程使用。通常情況下,直接給 CALayercontents 賦值 CGImage 圖片或者使用 CALayer 的衍生類就能實現(xiàn)大部分需求,還能充分利用硬件支持,圖像處理交給 GPU 當(dāng)然更加放心。

4、多核設(shè)備帶來的可能性

通過以上說明,可以了解 CoreGraphics 較為糟糕的性能。然而可喜的是,市面上的設(shè)備都已經(jīng)不是單核了,這就意味著可以通過后臺線程處理耗時任務(wù),主線程只需要負(fù)責(zé)調(diào)度顯示。

ps:關(guān)于多核設(shè)備的線程性能問題,后面分析源碼會講到

CoreGraphics 框架可以通過圖片上下文將繪制內(nèi)容制作為一張位圖,并且這個操作可以在非主線程執(zhí)行。那么,當(dāng)有 n 個繪制任務(wù)時,可以開辟多個線程在后臺異步繪制,繪制成功拿到位圖回到主線程賦值給 CALayer 的寄宿圖屬性。

這就是 YYAsyncLayer 框架的核心思想,該框架還有其他的亮點后文慢慢闡述。

雖然多個線程異步繪制會消耗大量的內(nèi)存,但是對于性能敏感界面來說,只要工程師控制好內(nèi)存峰值,可以極大的提高交互流暢度。優(yōu)化很多時候就是空間換時間,所謂魚和熊掌不可兼得。這也說明了一個問題,實際開發(fā)中要做有針對性的優(yōu)化,不可盲目跟風(fēng)。

三、YYSentinel

該類非常簡單:

.h
@interface YYSentinel : NSObject
@property (readonly) int32_t value;
- (int32_t)increase;
@end

.m
@implementation YYSentinel { int32_t _value; }
- (int32_t)value { return _value; }
- (int32_t)increase { return OSAtomicIncrement32(&_value); }
@end

一看便知,該類扮演的是計數(shù)的角色,值得注意的是,-increase方法是使用 OSAtomicIncrement32() 方法來對value執(zhí)行自增。

OSAtomicIncrement32()是原子自增方法,線程安全。在日常開發(fā)中,若需要保證整形數(shù)值變量的線程安全,可以使用 OSAtomic 框架下的方法,它往往性能比使用各種“鎖”更為優(yōu)越,并且代碼優(yōu)雅。

至于該類的實際作用后文會解釋。

四、YYTransaction

YYTransaction 貌似和系統(tǒng)的 CATransaction 很像,他們同為“事務(wù)”,但實際上很不一樣。通過 CATransaction 的嵌套用法猜測 CATransaction 對任務(wù)的管理是使用的一個棧結(jié)構(gòu),而 YYTransaction 是使用的集合來管理任務(wù)。

YYTransaction 做的事情就是記錄一系列事件,并且在合適的時機(jī)調(diào)用這些事件。至于為什么這么做,需要先了解 YYTransaction 做了些什么,最終你會恍然大悟??。

1、提交任務(wù)

YYTransaction 有兩個屬性:

@interface YYTransaction()
@property (nonatomic, strong) id target;
@property (nonatomic, assign) SEL selector;
@end
static NSMutableSet *transactionSet = nil;

很簡單,方法接收者 (target) 和方法 (selector),實際上一個 YYTransaction 就是一個任務(wù),而全局區(qū)的 transactionSet 集合就是用來存儲這些任務(wù)。提交方法-commit 不過是初始配置并且將任務(wù)裝入集合。

2、合適的回調(diào)時機(jī)

static void YYTransactionSetup() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        transactionSet = [NSMutableSet new];
        CFRunLoopRef runloop = CFRunLoopGetMain();
        CFRunLoopObserverRef observer;
        observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                           kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                           true,      // repeat
                                           0xFFFFFF,  // after CATransaction(2000000)
                                           YYRunLoopObserverCallBack, NULL);
        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    });
}

這里在主線程的 RunLoop 中添加了一個 oberver 監(jiān)聽,回調(diào)的時機(jī)是 kCFRunLoopBeforeWaitingkCFRunLoopExit ,即是主線程 RunLoop 循環(huán)即將進(jìn)入休眠或者即將退出的時候。而該 oberver 的優(yōu)先級是 0xFFFFFF,優(yōu)先級在 CATransaction 的后面(至于 CATransaction 的優(yōu)先級為什么是 2000000,應(yīng)該在主線程 RunLoop 啟動的源代碼中可以查到,筆者并沒有找到暴露出來的信息)。

從這里可以看出,作者使用一個“低姿態(tài)”侵入主線程 RunLoop,在處理完重要邏輯(即 CATransaction 管理的繪制任務(wù))之后做異步繪制的事情,這也是作者對優(yōu)先級的權(quá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
    }];
}

一目了然,只是將集合中的任務(wù)分別執(zhí)行。

3、自定義 hash 算法

YYTransaction 類重寫了 hash 算法:

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

NSObject 類默認(rèn)的 hash 值為 10 進(jìn)制的內(nèi)存地址,這里作者將_selector_target的內(nèi)存地址進(jìn)行一個位異或處理,意味著只要_selector_target地址都相同時,hash 值就相同。

這么做的意義是什么呢?

上面有提到一個集合:

static NSMutableSet *transactionSet = nil;

和其他編程語言一樣 NSSet 是基于 hash 的集合,它是不能有重復(fù)元素的,而判斷是否重復(fù)毫無疑問是使用 hash。這里將 YYTransaction 的 hash 值依托于_selector_target的內(nèi)存地址,那就意味著兩點:

  1. 同一個 YYTransaction 實例,_selector_target只要有一個內(nèi)存地址不同,就會在集合中體現(xiàn)為兩個值。
  2. 不同的 YYTransaction 實例,_selector_target的內(nèi)存地址都相同,在集合中的體現(xiàn)為一個值。

熟悉 hash 的讀者應(yīng)該一點即通,那么這么做對于業(yè)務(wù)的目的是什么呢?

很簡單,這樣可以避免重復(fù)的方法調(diào)用。加入transactionSet中的事件會在 Runloop 即將進(jìn)入休眠或者即將退出時遍歷執(zhí)行,相同的方法接收者 (_target) 和相同的方法 (_selector) 在一個 Runloop 周期內(nèi)可以視為重復(fù)調(diào)用。

舉個例子:

在 YYText 的YYTextView中,主要是為了將自定義的繪制邏輯裝入transactionSet,然后在 Runloop 要結(jié)束時統(tǒng)一執(zhí)行,Runloop 回調(diào)的優(yōu)先級避免與系統(tǒng)繪制邏輯競爭資源,使用NSSet合并了一次 Runloop 周期多次的繪制請求為一個。

五、YYAsyncLayer

@interface YYAsyncLayer : CALayer
@property BOOL displaysAsynchronously;
@end

YYAsyncLayer 繼承自 CALayer,對外暴露了一個方法可開閉是否異步繪制。

1、初始化配置

- (instancetype)init {
    self = [super init];
    static CGFloat scale; //global
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        scale = [UIScreen mainScreen].scale;
    });
    self.contentsScale = scale;
    _sentinel = [YYSentinel new];
    _displaysAsynchronously = YES;
    return self;
}

這里設(shè)置了YYAsyncLayercontentsScale為屏幕的scale,該屬性是 物理像素 / 邏輯像素,這樣可以充分利用不同設(shè)備的顯示器分辨率,繪制更清晰的圖像。但是若contentsGravity設(shè)置了可拉伸的類型,CoreAnimation 將會優(yōu)先滿足,而忽略掉contentsScale。

同時還創(chuàng)建了一個YYSentinel實例。

@2x和@3x圖

實際上 iPhone4 及其以上的 iPhone 設(shè)備scale都是 2 及以上,也就是說至少都是每個邏輯像素長度對應(yīng)兩個物理像素長度。所以很多美工會只切 @2x 和 @3x 圖給你,而不切一倍圖。

@2x和@3x圖是蘋果一個優(yōu)化顯示效果的機(jī)制,當(dāng) iPhone 設(shè)備scale為 2 時會優(yōu)先讀取 @2x 圖,當(dāng)scale為 3 時會優(yōu)先讀取 @3x 圖,這就意味著,CALayercontentsScale要和設(shè)備的scale對應(yīng)才能達(dá)到預(yù)期的效果(不同設(shè)備顯示相同的邏輯像素大?。?/p>

幸運(yùn)的是,UIViewUIImageView默認(rèn)處理了它們內(nèi)部CALayercontentsScale,所以除非是直接使用CALayer及其衍生類,都不用顯式的配置contentsScale

重寫繪制方法

- (void)setNeedsDisplay {
    [self _cancelAsyncDisplay];
    [super setNeedsDisplay];
}
- (void)display {
    super.contents = super.contents;
    [self _displayAsync:_displaysAsynchronously];
}

可以看到兩個方法,-_cancelAsyncDisplay是取消繪制,稍后解析實現(xiàn)邏輯;-_displayAsync是異步繪制的核心方法。

2、YYAsyncLayerDelegate 代理

@protocol YYAsyncLayerDelegate <NSObject>
@required
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask;
@end
@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

YYAsyncLayerDisplayTask是繪制任務(wù)管理類,可以通過willDisplaydidDisplay回調(diào)將要繪制和結(jié)束繪制時機(jī),最重要的是display,需要實現(xiàn)這個代碼塊,在代碼塊里面寫業(yè)務(wù)繪制邏輯。

這個代理實際上就是框架和業(yè)務(wù)交互的橋梁,不過這個設(shè)計筆者個人認(rèn)為有一些冗余,這里如果直接通過代理方法與業(yè)務(wù)交互而不使用中間類可能看起來更舒服。

3、異步繪制的核心邏輯

刪減了部分代碼:

- (void)_displayAsync:(BOOL)async {
    __strong id<YYAsyncLayerDelegate> delegate = self.delegate;
    YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
    ...
        dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
            if (isCancelled()) return;
            UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
            CGContextRef context = UIGraphicsGetCurrentContext();
            task.display(context, size, isCancelled);
            if (isCancelled()) {
                UIGraphicsEndImageContext();
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self, NO);
                });
                return;
            }
            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            if (isCancelled()) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self, NO);
                });
                return;
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                if (isCancelled()) {
                    if (task.didDisplay) task.didDisplay(self, NO);
                } else {
                    self.contents = (__bridge id)(image.CGImage);
                    if (task.didDisplay) task.didDisplay(self, YES);
                }
            });
        });
    ...
}

先不用管 YYAsyncLayerGetDisplayQueue()方法如何獲取的異步隊列,也先不用管isCancelled()判斷做的一些提前結(jié)束繪制的邏輯,這些后面會講。

那么,實際上核心代碼可以更少:

- (void)_displayAsync:(BOOL)async {
    ...
    dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
        UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
        CGContextRef context = UIGraphicsGetCurrentContext();
        task.display(context, size, isCancelled);
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        dispatch_async(dispatch_get_main_queue(), ^{
            self.contents = (__bridge id)(image.CGImage);
        });
    }];
    ...
}

此時就很清晰了,在異步線程創(chuàng)建一個位圖上下文,調(diào)用taskdisplay代碼塊進(jìn)行繪制(業(yè)務(wù)代碼),然后生成一個位圖,最終進(jìn)入主隊列給YYAsyncLayercontents賦值CGImage由 GPU 渲染過后提交到顯示系統(tǒng)。

4、及時的結(jié)束無用的繪制

針對同一個YYAsyncLayer,很有可能新的繪制請求到來時,當(dāng)前的繪制任務(wù)還未完成,而當(dāng)前的繪制任務(wù)是無用的,會繼續(xù)消耗過多的 CPU (GPU) 資源。當(dāng)然,這種場景主要是出現(xiàn)在列表界面快速滾動時,由于視圖的復(fù)用機(jī)制,導(dǎo)致重新繪制的請求非常頻繁。

為了解決這個問題,作者使用了大量的判斷來及時的結(jié)束無用的繪制,可以看看源碼或者是上文貼出的異步繪制核心邏輯代碼,會發(fā)現(xiàn)一個頻繁的操作:

if (isCancelled()) {...}

看看這個代碼塊的實現(xiàn):

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

這就是YYSentinel計數(shù)類起作用的時候了,這里用一個局部變量value來保持當(dāng)前繪制邏輯的計數(shù)值,保證其他線程改變了全局變量_sentinel的值也不會影響當(dāng)前的value;若當(dāng)前value不等于最新的_sentinel .value時,說明當(dāng)前繪制任務(wù)已經(jīng)被放棄,就需要及時的做返回邏輯。

那么,何時改變這個計數(shù)?

- (void)setNeedsDisplay {
    [self _cancelAsyncDisplay];
    [super setNeedsDisplay];
}
- (void)_cancelAsyncDisplay {
    [_sentinel increase];
}

很明顯,在提交重繪請求時,計數(shù)器加一。

??不得不說,這確實是一個令人興奮的優(yōu)化技巧。

5、異步線程的管理

筆者去除了判斷 YYDispatchQueuePool 庫是否存在的代碼,實際上那就是作者提取的隊列管理封裝,思想和以下代碼一樣。

static dispatch_queue_t YYAsyncLayerGetDisplayQueue() {
//最大隊列數(shù)量
#define MAX_QUEUE_COUNT 16
//隊列數(shù)量
    static int queueCount;
//使用棧區(qū)的數(shù)組存儲隊列
    static dispatch_queue_t queues[MAX_QUEUE_COUNT];
    static dispatch_once_t onceToken;
    static int32_t counter = 0;
    dispatch_once(&onceToken, ^{
//要點 1 :串行隊列數(shù)量和處理器數(shù)量相同
        queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
        queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;
//要點 2 :創(chuàng)建串行隊列,設(shè)置優(yōu)先級
        if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
            for (NSUInteger i = 0; i < queueCount; i++) {
                dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
                queues[i] = dispatch_queue_create("com.ibireme.yykit.render", attr);
            }
        } else {
            for (NSUInteger i = 0; i < queueCount; i++) {
                queues[i] = dispatch_queue_create("com.ibireme.yykit.render", DISPATCH_QUEUE_SERIAL);
                dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
            }
        }
    });
//要點 3 :輪詢返回隊列
    int32_t cur = OSAtomicIncrement32(&counter);
    if (cur < 0) cur = -cur;
    return queues[(cur) % queueCount];
#undef MAX_QUEUE_COUNT
}
要點 1 :串行隊列數(shù)量和處理器數(shù)量相同

首先要明白,并發(fā)并行 的區(qū)別:
并行一定并發(fā),并發(fā)不一定并行。在單核設(shè)備上,CPU通過頻繁的切換上下文來運(yùn)行不同的線程,速度足夠快以至于我們看起來它是‘并行’處理的,然而我們只能說這種情況是并發(fā)而非并行。例如:你和兩個人一起百米賽跑,你一直在不停的切換跑道,而其他兩人就在自己的跑道上,最終,你們?nèi)送瑫r到達(dá)了終點。我們把跑道看做任務(wù),那么,其他兩人就是并行執(zhí)行任務(wù)的,而你只能的說是并發(fā)執(zhí)行任務(wù)。

所以,實際上一個 n 核設(shè)備同一時刻最多能 并行 執(zhí)行 n 個任務(wù),也就是最多有 n 個線程是相互不競爭 CPU 資源的。

當(dāng)你開辟的線程過多,超過了處理器核心數(shù)量,實際上某些并行的線程之間就可能競爭同一個處理器的資源,頻繁的切換上下文也會消耗處理器資源。

所以,筆者認(rèn)為:超過處理器核心數(shù)量的線程沒有處理速度上的優(yōu)勢,只是在業(yè)務(wù)上便于管理,并且能最大化的利用處理器資源。

而串行隊列中只有一個線程,該框架中,作者使用和處理器核心相同數(shù)量的串行隊列來輪詢處理異步任務(wù),有效的減少了線程調(diào)度操作。

要點 2 :創(chuàng)建串行隊列,設(shè)置優(yōu)先級

在 8.0 以上的系統(tǒng),隊列的優(yōu)先級為 QOS_CLASS_USER_INITIATED,低于用戶交互相關(guān)的QOS_CLASS_USER_INTERACTIVE。

在 8.0 以下的系統(tǒng),通過dispatch_set_target_queue()函數(shù)設(shè)置優(yōu)先級為DISPATCH_QUEUE_PRIORITY_DEFAULT(第二個參數(shù)如果使用串行隊列會強(qiáng)行將我們創(chuàng)建的所有線程串行執(zhí)行任務(wù))。

可以猜測主隊列的優(yōu)先級是大于或等于QOS_CLASS_USER_INTERACTIVE的,讓這些串行隊列的優(yōu)先級低于主隊列,避免框架創(chuàng)建的線程和主線程競爭資源。

關(guān)于兩種類型優(yōu)先級的對應(yīng)關(guān)系是這樣的:

 *  - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
 *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
 *  - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
 *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND
要點 3 :輪詢返回隊列

使用原子自增函數(shù)OSAtomicIncrement32()對局部靜態(tài)變量counter進(jìn)行自增,然后通過取模運(yùn)算輪詢返回隊列。

注意這里使用了一個判斷:if (cur < 0) cur = -cur;,當(dāng)cur自增越界時就會變?yōu)樨?fù)數(shù)最大值(在二進(jìn)制層面,是用正整數(shù)的反碼加一來表示其負(fù)數(shù)的)。

為什么要使用 n 個串行隊列實現(xiàn)并發(fā)

可能有人會有疑惑,為什么這里需要使用 n 個串行隊列來調(diào)度,而不用一個并行隊列。

主要是因為并行隊列無法精確的控制線程數(shù)量,很有可能創(chuàng)建過多的線程,導(dǎo)致 CPU 線程調(diào)度過于頻繁,影響交互性能。

可能會想到用信號量 (dispatch_semaphore_t) 來控制并發(fā),然而這樣只能控制并發(fā)的任務(wù)數(shù)量,而不能控制線程數(shù)量,并且使用起來不是很優(yōu)雅。而使用串行隊列就很簡單了,我們可以很明確的知道自己創(chuàng)建的線程數(shù)量,一切皆在掌控之中。

以上就是 YYKit 對線程處理的核心思想。

結(jié)語

不知道讀者朋友有沒有感受到 YYAsyncLayer 的 300 行左右代碼所涵蓋的東西。實際上學(xué)習(xí)一份優(yōu)秀源碼需要在過程中去了解和學(xué)習(xí)源碼之外的其它很多知識,這也是優(yōu)秀源碼的價值所在。

沉下心來感受代碼的藝術(shù)。

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

相關(guān)閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 30,282評論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,681評論 1 32
  • 從哪說起呢? 單純講多線程編程真的不知道從哪下嘴。。 不如我直接引用一個最簡單的問題,以這個作為切入點好了 在ma...
    Mr_Baymax閱讀 2,917評論 1 17
  • 沈文彬馬上腦補(bǔ)當(dāng)日的情景,好像是那天要到了她的手機(jī)號碼。 傻笑了片刻,便決心一生只愛她一個人了。 再翻開一頁...
    俗底閱讀 317評論 0 4
  • 理想的生活, 有一個自己愛好并且當(dāng)作事業(yè)一樣的工作, 一個溫暖且舒服的小窩, 一只貓, 一只狗, 幾條小金魚, 一...
    LeylaYY閱讀 222評論 0 0

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