YYAsyncLayer 源碼剖析:異步繪制

系列文章:
YYText 源碼剖析:CoreText 與異步繪制
YYAsyncLayer 源碼剖析:異步繪制
YYCache 源碼剖析:一覽亮點(diǎn)
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吴汪。(需要實(shí)現(xiàn) YYAsyncLayer 類指定的代理方法惠窄,對整個繪制流程做管理,詳細(xì)使用方法可以看看框架的 README

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

1杆融、界面卡頓的實(shí)質(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)化界面流暢程度,實(shí)際上就是減少掉幀(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 (漸變繪制) 等都有較高的效率顶别,非常實(shí)用谷徙。

再來看一下 CoreGraphics 框架,實(shí)際上它是依托于 CPU 的軟件繪制驯绎。在實(shí)現(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 的衍生類就能實(shí)現(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 框架的核心思想晕城,該框架還有其他的亮點(diǎn)后文慢慢闡述。

雖然多個線程異步繪制會消耗大量的內(nèi)存窖贤,但是對于性能敏感界面來說砖顷,只要工程師控制好內(nèi)存峰值,可以極大的提高交互流暢度赃梧。優(yōu)化很多時候就是空間換時間滤蝠,所謂魚和熊掌不可兼得。這也說明了一個問題授嘀,實(shí)際開發(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)雅。

至于該類的實(shí)際作用后文會解釋芯丧。

四芍阎、YYTransaction

YYTransaction 貌似和系統(tǒng)的 CATransaction 很像,他們同為“事務(wù)”缨恒,但實(shí)際上很不一樣谴咸。通過 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)鲫凶,實(shí)際上一個 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)存地址山卦,那就意味著兩點(diǎn):

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

熟悉 hash 的讀者應(yīng)該一點(diǎn)即通,那么這么做對于業(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實(shí)例个唧。

@2x和@3x圖

實(shí)際上 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è)備顯示相同的邏輯像素大心鸾铡)灭翔。

幸運(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是取消繪制狭园,稍后解析實(shí)現(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绎谦,需要實(shí)現(xiàn)這個代碼塊管闷,在代碼塊里面寫業(yè)務(wù)繪制邏輯。

這個代理實(shí)際上就是框架和業(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é)束繪制的邏輯,這些后面會講纤怒。

那么糯而,實(shí)際上核心代碼可以更少:

- (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()) {...}

看看這個代碼塊的實(shí)現(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ù)器加一。

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

5、異步線程的管理

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

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, ^{
//要點(diǎn) 1 :串行隊列數(shù)量和處理器數(shù)量相同
        queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
        queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;
//要點(diǎn) 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));
            }
        }
    });
//要點(diǎn) 3 :輪詢返回隊列
    int32_t cur = OSAtomicIncrement32(&counter);
    if (cur < 0) cur = -cur;
    return queues[(cur) % queueCount];
#undef MAX_QUEUE_COUNT
}
要點(diǎn) 1 :串行隊列數(shù)量和處理器數(shù)量相同

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

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

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

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

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

要點(diǎn) 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
要點(diǎn) 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 個串行隊列實(shí)現(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 行左右代碼所涵蓋的東西旷偿。實(shí)際上學(xué)習(xí)一份優(yōu)秀源碼需要在過程中去了解和學(xué)習(xí)源碼之外的其它很多知識,這也是優(yōu)秀源碼的價值所在爆侣。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市兔仰,隨后出現(xiàn)的幾起案子茫负,更是在濱河造成了極大的恐慌,老刑警劉巖乎赴,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忍法,死亡現(xiàn)場離奇詭異潮尝,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)饿序,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門勉失,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人原探,你說我怎么就攤上這事乱凿。” “怎么了咽弦?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵徒蟆,是天一觀的道長。 經(jīng)常有香客問我型型,道長段审,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任输莺,我火速辦了婚禮戚哎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嫂用。我一直安慰自己,他們只是感情好丈冬,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布嘱函。 她就那樣靜靜地躺著,像睡著了一般埂蕊。 火紅的嫁衣襯著肌膚如雪往弓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天蓄氧,我揣著相機(jī)與錄音函似,去河邊找鬼。 笑死喉童,一個胖子當(dāng)著我的面吹牛撇寞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播堂氯,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼蔑担,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了咽白?” 一聲冷哼從身側(cè)響起啤握,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎晶框,沒想到半個月后排抬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體懂从,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年蹲蒲,在試婚紗的時候發(fā)現(xiàn)自己被綠了莫绣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡悠鞍,死狀恐怖对室,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咖祭,我是刑警寧澤掩宜,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站么翰,受9級特大地震影響牺汤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜浩嫌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一檐迟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧码耐,春花似錦器腋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至束铭,卻和暖如春廓块,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背契沫。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工带猴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人懈万。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓拴清,卻偏偏與公主長得像,于是被迫代替她去往敵國和親钞速。 傳聞我的和親對象是個殘疾皇子贷掖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

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

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