系列文章:
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ā)中要盡量避免直接在主線程使用。通常情況下,直接給 CALayer 的 contents 賦值 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ī)是 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit ,即是主線程 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)存地址,那就意味著兩點:
- 同一個 YYTransaction 實例,
_selector和_target只要有一個內(nèi)存地址不同,就會在集合中體現(xiàn)為兩個值。 - 不同的 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è)置了YYAsyncLayer的contentsScale為屏幕的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 圖,這就意味著,CALayer的contentsScale要和設(shè)備的scale對應(yīng)才能達(dá)到預(yù)期的效果(不同設(shè)備顯示相同的邏輯像素大?。?/p>
幸運(yùn)的是,UIView和UIImageView默認(rèn)處理了它們內(nèi)部CALayer的contentsScale,所以除非是直接使用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ù)管理類,可以通過willDisplay和didDisplay回調(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)用task的display代碼塊進(jìn)行繪制(業(yè)務(wù)代碼),然后生成一個位圖,最終進(jìn)入主隊列給YYAsyncLayer的contents賦值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ù)。