系列文章:
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ā)中要盡量避免直接在主線程使用芒粹。通常情況下兄纺,直接給 CALayer
的 contents
賦值 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ī)是 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)存地址山卦,那就意味著兩點(diǎn):
- 同一個 YYTransaction 實(shí)例,
_selector
和_target
只要有一個內(nèi)存地址不同诵次,就會在集合中體現(xiàn)為兩個值账蓉。 - 不同的 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è)置了YYAsyncLayer
的contentsScale
為屏幕的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 圖桨螺,這就意味著,CALayer
的contentsScale
要和設(shè)備的scale
對應(yīng)才能達(dá)到預(yù)期的效果(不同設(shè)備顯示相同的邏輯像素大心鸾铡)灭翔。
幸運(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
是取消繪制狭园,稍后解析實(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ù)管理類,可以通過willDisplay
和didDisplay
回調(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)用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()) {...}
看看這個代碼塊的實(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ù)萍程。