Grand Central Dispatch
[TOC]
GCD是什么
Grand Central Dispatch 是蘋果公司發(fā)布的一套多核多線程任務(wù)分發(fā)的解決方案,簡稱GCD搅荞,或者你叫他滾床單也沒有人反對(duì)辈挂,嘿嘿步鉴。
GCD發(fā)布
蘋果公司首次發(fā)布GCD是伴隨Mac OS X 10.6 和 iOS 4系統(tǒng)一起發(fā)布的,也正是伴隨著block塊語法的支持颜懊,GCD技術(shù)將多線程執(zhí)行代碼年堆,通過block封裝成代碼塊,大大提高了多線程開發(fā)的效率呐籽,減少了開發(fā)難度锋勺,也極大增強(qiáng)了代碼的可讀性蚀瘸。
GCD之前的黑暗時(shí)代
如果我將GCD技術(shù)比喻成普羅米修斯帶給人類的火種有一些夸張的話,至少可以將其比作火柴庶橱。而在沒有生火器的石器時(shí)代贮勃,人類只能依靠何鉆木取火。
POSIX線程
POSIX線程(pthread)是一套C語言編寫的線程管理API苏章,面向過程寂嘉,我只在老東家一套C源碼庫中見別人用過,自己從來沒有用過布近,也不會(huì)用垫释,就像我也不會(huì)鉆木取火一樣。
NSThread
Cocoa框架中撑瞧,用OC將pthread對(duì)象化封裝棵譬,就誕生了NSThread
操作類,但很可惜至今NSThread.h
頭文件中一行注釋都木有预伺,只能看出這個(gè)類早在1994年就已經(jīng)存在了订咸。
這里就不列舉具體事例了,因?yàn)槿缃襁@個(gè)類的使用頻率已經(jīng)非常低了酬诀,唯一一種你可能會(huì)遇到的使用情境是判斷當(dāng)前執(zhí)行線程是否為主線程脏嚷,具體代碼如下
if([NSThread isMainThread]){
}
但你在GCD和NSOperation出現(xiàn)之前,會(huì)在各種需要多線程處理的情況下瞒御,使用NSThread
的隱式調(diào)用方法父叙,也就是NSThread頭文件中給NSObject
類作為屬性方法擴(kuò)展的一系列接口:
@interface NSObject (NSThreadPerformAdditions)
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
@end
總計(jì)五個(gè)API,簡易實(shí)現(xiàn)了一般開發(fā)需要使用的基本線程操作肴裙,避免用戶自己動(dòng)手寫NSThread調(diào)度趾唱,引發(fā)的一些列莫名的死鎖問題,在某種程度上減少了當(dāng)時(shí)的多線程開發(fā)難度蜻懦。
但這些API有一些很直觀的問題甜癞,例如由于OC語言限制,這些API的參數(shù)傳遞宛乃、返回值獲取都不易實(shí)現(xiàn)悠咱,并且實(shí)際寫出來的代碼也會(huì)因?yàn)檫壿嬏D(zhuǎn)分布在文件的各個(gè)位置,影響閱讀和糾錯(cuò)征炼,你不相信請(qǐng)看我從教科書上抄下來的例子:
- (void)launchThreadByNSObject_performSelectorInBackground_withObject
{
[self performSelectorInBackground:@selector(doWork) withObject:nil];
}
- (void) doWork
{
/*
*
* 長時(shí)間處理
*
* 例如 圖像處理
* 網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求
* 大型數(shù)據(jù)庫操作
* 磁盤操作
*/
//操作結(jié)束后調(diào)用主線程修改UI
[self performSelectorOnMainThread:@selector(doneWork) withObject:nil waitUntilDone:NO];
}
- (void) doneWork
{
//主線程修改UI
}
這個(gè)例子是一個(gè)解決關(guān)于主線程刷新UI問題的例子析既,我們同學(xué)都知道所有有關(guān)UI刷新的方法,務(wù)必要在主線程調(diào)用谆奥,這是個(gè)硬性要求渡贾,是因?yàn)閁I渲染就是在主線程循環(huán)中完成的,如果在支線程中調(diào)用雄右,會(huì)出現(xiàn)莫名其妙的錯(cuò)誤空骚、UI卡死或者程序崩潰。
所以多線程在我們的日常開發(fā)中擂仍,用得最多的地方囤屹,就是網(wǎng)絡(luò)數(shù)據(jù)的異步請(qǐng)求,然后主線程刷新UI逢渔。將有延遲和計(jì)算量大的操作放在支線程完成肋坚,待完成后使用主線程刷新UI,才能有效地防止主線程UI刷新阻塞肃廓。
iOS 4與block
iOS 4帶來的編譯器對(duì)block塊語法的支持智厌,有點(diǎn)像人類發(fā)現(xiàn)了磷這種易燃物質(zhì)一樣,帶來的是火柴(GCD 和 NSOperation)這個(gè)更簡易的生火工具盲赊。
GCD和NSOperation 可以看作是 pthread(面向過程)和NSThread(面向?qū)ο螅┑腷lock升級(jí)版本铣鹏,帶來的多線程編程體驗(yàn)則是質(zhì)的飛躍。
GCD像是火柴哀蘑,輕便易用诚卸,隨用隨取。NSOperation則像打火機(jī)绘迁,一次開發(fā)合溺,重復(fù)使用。
GCD實(shí)戰(zhàn)
好了缀台,已經(jīng)說了十幾分鐘廢話了棠赛,終要進(jìn)入主題進(jìn)行GCD多線程開發(fā)實(shí)戰(zhàn)。在開始之前膛腐,希望大家要提前學(xué)習(xí)block塊語法的相關(guān)知識(shí)睛约,不要求熟練使用,只要求看得懂依疼。
實(shí)戰(zhàn)一 異步加載
還記得我們?cè)谏厦嬲故镜膹慕炭茣铣聛淼睦用刺等@個(gè)例子如果改成GCD的版本,會(huì)是什么樣子的呢律罢?
//異步請(qǐng)求Dispatch
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//長時(shí)間處理
dispatch_async(dispatch_get_main_queue(), ^{
//主線程更新UI
});
});
這是什么鬼膀值?我來解釋一下。GCD使用的是C語言風(fēng)格的調(diào)用接口误辑,栗子中調(diào)用了兩次dispatch_async
方法沧踏,第一次將長時(shí)間處理操作分撥到支線程處理,在其完成后巾钉,跳轉(zhuǎn)回主線程更新UI翘狱,操作都在方法的block參數(shù)中傳入,簡單明了砰苍,層級(jí)分明潦匈,沒有傳參障礙阱高,沒有閱讀障礙,一氣呵成茬缩,簡直美極了赤惊。
dispatch_async
方法傳入的第二個(gè)參數(shù)是執(zhí)行block,沒啥好說的凰锡,第一個(gè)參數(shù)則是線程未舟。dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
方法獲取的Global線程,是非主線程中的一個(gè)掂为,具體是哪個(gè)不用開發(fā)者操心裕膀,反正是系統(tǒng)認(rèn)為這時(shí)候不是很忙的那一個(gè)。而兩個(gè)傳入?yún)?shù)中的第一個(gè)是線程的優(yōu)先級(jí)(共四個(gè)優(yōu)先級(jí))勇哗,第二個(gè)參數(shù)則約定為0昼扛。dispatch_get_main_queue()
這個(gè)沒有任何參數(shù)的方法,返回的則是主線程智绸。
注意這里返回的參數(shù)類型是dispatch_queue_t
野揪,是一個(gè)普通變量,估計(jì)是線程的索引瞧栗。真是兩三句話就能講明白的方法調(diào)用斯稳,什么你說聽不懂、看不懂迹恐。無所謂呀~ 我們將這段代碼加入代碼片段挣惰,需要使用的時(shí)候拿出來用就行啦。
比如:
圖片異步加載
首先放開權(quán)限NSAppTransportSecurity
,NSAllowsArbitraryLoads
NSURLConnection版本
不加多線程異步操作
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
[cell.imageView setImage:[UIImage new]];
NSURL* url = [NSURL URLWithString:self.static_data[indexPath.row]];
NSData* data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] returningResponse:nil error:nil];
UIImage* image = [UIImage imageWithData:data];
[cell.imageView setImage:image];
[cell setNeedsLayout];
return cell;
}
使用GCD以后
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
[cell.imageView setImage:[UIImage new]];
NSURL* url = [NSURL URLWithString:self.static_data[indexPath.row]];
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData* data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] returningResponse:nil error:nil];
UIImage* image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
[cell.imageView setImage:image];
[cell setNeedsLayout];
});
});
return cell;
}
NSURLSession版本
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
// cell.backgroundColor = [UIColor lightGrayColor];
// Configure the cell...
[cell.imageView setImage:[UIImage new]];
NSURL* url = [NSURL URLWithString:self.static_data[indexPath.row]];
NSURLSessionConfiguration* c = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession* session = [NSURLSession sessionWithConfiguration:c];
NSURLSessionDataTask* task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage* image = [UIImage imageWithData:data];
// NSLog(@"%@",image);
// NSLog(@"%@",cell.imageView);
[cell.imageView setImage:image];
[cell setNeedsLayout];
});
}];
[task resume];
return cell;
}
這次試出來UI刷新的阻塞感受了么殴边?霸髅?你說沒有锤岸,那你用真機(jī)調(diào)試一下竖幔,就會(huì)有更明顯的感受了。
UI阻塞在實(shí)際開發(fā)中是偷,偶爾會(huì)遇到拳氢。并且會(huì)引起一些莫名其妙的bug,希望大家再遇到時(shí)候能及時(shí)往這方面思考蛋铆。比如馋评,我們?nèi)绻?code>UIViewController的初始化等一系列加載函數(shù)中加入能引起阻塞的代碼,整個(gè)VC的加載會(huì)產(chǎn)生卡頓刺啦,還很有可能直接崩潰留特。
所以將阻塞操作放在支線程處理,是十分必要的。我們只要將下面代碼存為代碼片段蜕青,隨用隨取苟蹈。
//支線程調(diào)用
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
<#code#>
});
//主線程調(diào)用
dispatch_async(dispatch_get_main_queue(), ^{
<#code#>
});
實(shí)戰(zhàn)二 同步操作等待
多線程操作的第二個(gè)常用情景就是并行操作等待。
dispatch_group_t group = dispatch_group_create();
// 合并匯總結(jié)果
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
//并行阻塞操作1
[NSThread sleepForTimeInterval:1.0];
NSLog(@"1");
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
//并行阻塞操作2
[NSThread sleepForTimeInterval:0.5];
NSLog(@"2");
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
//并行阻塞操作3
NSLog(@"3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//3項(xiàng)操作都完成后調(diào)用主線程更新UI
NSLog(@"4");
});
在這段演示代碼里面右核,即使你看不懂GCD相關(guān)調(diào)用汉操,也能猜出最后的輸出結(jié)果對(duì)吧,我解釋一下[NSThread sleepForTimeInterval:1.0];
這句調(diào)用是讓線程睡眠1秒中蒙兰,模擬1秒鐘阻塞。
好的告訴我你的答案芒篷。
3
2
1
4
這也是一段可以收藏為代碼片段的實(shí)用工具搜变,可以起名為并行代碼等待
。就像異步等待
一樣针炉,我們現(xiàn)在來舉一個(gè)簡單的實(shí)際案例挠他。
并行操作案例
還是舉一個(gè)不是很簡單的例子,也可能不是很實(shí)用篡帕,但絕對(duì)能體現(xiàn)這套邏輯的精髓殖侵。在講栗子之前,我們先來學(xué)習(xí)一下SDWebImage的另外一段代碼(對(duì)镰烧,又是SDWebImage)拢军。
//SDImageCache.m 608行
- (NSUInteger)getSize {
__block NSUInteger size = 0;
dispatch_sync(self.ioQueue, ^{
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
for (NSString *fileName in fileEnumerator) {
NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
});
return size;
}
這段代碼,具體功能是進(jìn)行文件夾文件大小的統(tǒng)計(jì)怔鳖。對(duì)你沒有聽錯(cuò)茉唉,文件夾是無法直接接獲取其大小的,需要遍歷其中每個(gè)文件然后相加統(tǒng)計(jì)结执。
這段代碼實(shí)用GCD度陆,但使用的方法我們前面并沒有講過,我放在后面再說献幔。目前我們的任務(wù)是把這個(gè)方法改造一下懂傀,讓他可以統(tǒng)計(jì)任意的文件夾大小。
- (NSUInteger)getSize:(NSString*)dicPath {
__block NSUInteger size = 0;
NSDirectoryEnumerator *fileEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:dicPath];
for (NSString *fileName in fileEnumerator) {
NSString *filePath = [dicPath stringByAppendingPathComponent:fileName];
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
return size;
}
接下來我們統(tǒng)計(jì)一下cache目錄和tmp目錄的容量
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDir = [paths objectAtIndex:0];
NSString *tmpDir = NSTemporaryDirectory();
NSUInteger cacheSize = [self getSize:cachesDir];
NSUInteger tmpSize = [self getSize:tmpDir];
NSLog(@"total size : %@ (%@+%@)",@(cacheSize + tmpSize),@(cacheSize),@(tmpSize));
total size : 657060 (657060+0)
tmp文件夾是空的蜡感,我們換成libiary目錄蹬蚁,不過因?yàn)閏ache目錄在libiary目下,所以是有重復(fù)的铸敏,不過無所謂缚忧。
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDir = [paths objectAtIndex:0];
NSArray * paths2 = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString * libraryPath = paths2[0];
NSUInteger cacheSize = [self getSize:cachesDir];
NSUInteger librarySize = [self getSize:libraryPath];
NSLog(@"total size : %@ (%@+%@)",@(cacheSize + librarySize),@(cacheSize),@(librarySize));
我們?cè)趫?zhí)行這段代碼的時(shí)候,一般會(huì)很順暢就執(zhí)行完了杈笔,沒有任何阻塞闪水。原因是統(tǒng)計(jì)的目標(biāo)目錄,文件非常少。如果遇到文件稍多的情況球榆,上面這段代碼就出出現(xiàn)阻塞朽肥,又因?yàn)檎麄€(gè)是在主線程操作的,所以必然會(huì)影響到UI的刷新持钉,界面會(huì)卡頓衡招。好,那讓我們運(yùn)用前面的GCD模版來將這段代碼改造成異步執(zhí)行每强。
NSLog(@"1");
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDir = [paths objectAtIndex:0];
NSArray * paths2 = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString * libraryPath = paths2[0];
NSUInteger cacheSize = [self getSize:cachesDir];
NSUInteger librarySize = [self getSize:libraryPath];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"total size : %@ (%@+%@)",@(cacheSize + librarySize),@(cacheSize),@(librarySize));
});
});
NSLog(@"2");
上面是我修改的結(jié)果始腾,大家來分析一下輸出順序,應(yīng)該是
1
2
total size : 1314324 (657060+657264)
前面坐了這么多鋪墊空执,接下來我們進(jìn)入正題浪箭,講解一下串行和并行。串行很好理解辨绊,我們一般寫的代碼都是一步一步一串一串執(zhí)行的奶栖。并行則是多項(xiàng)任務(wù)同時(shí)進(jìn)行,也不難理解门坷,類似于中學(xué)物理學(xué)的電路的并聯(lián)合串聯(lián)宣鄙。
上面這段代碼,我們前后調(diào)用兩次getSize方法默蚌,按順序分別統(tǒng)計(jì)了兩個(gè)目錄的大小冻晤,我們統(tǒng)計(jì)一下耗時(shí):
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
clock_t begin, duration;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDir = [paths objectAtIndex:0];
NSArray * paths2 = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString * libraryPath = paths2[0];
begin = clock();
NSUInteger cacheSize = [self getSize:cachesDir];
NSUInteger librarySize = [self getSize:libraryPath];
duration = clock() - begin;
NSLog(@"%@",@((double)duration/CLOCKS_PER_SEC));
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"total size : %@ (%@+%@)",@(cacheSize + librarySize),@(cacheSize),@(librarySize));
});
});
0.002481
這里單位是秒,其實(shí)已經(jīng)很快敏簿。 好明也,我們把前面的并行模版套進(jìn)來。
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *path = [[NSBundle mainBundle] bundlePath];
NSArray * paths2 = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString * libraryPath = paths2[0];
__block clock_t begin, duration;
__block NSUInteger cacheSize,librarySize;
dispatch_group_t group = dispatch_group_create();
// 合并匯總結(jié)果
begin = clock();
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
cacheSize = [self getSize:path];
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
librarySize = [self getSize:libraryPath];
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
duration = clock() - begin;
NSLog(@"%@",@((double)duration/CLOCKS_PER_SEC));
NSLog(@"total size : %@ (%@+%@)",@(cacheSize + librarySize),@(cacheSize),@(librarySize));
});
});
0.039834
提問
結(jié)果很讓我欣慰惯裕,整整大了一個(gè)數(shù)量級(jí)温数,請(qǐng)你們分析一下原因。
原因也很簡單蜻势,就是因?yàn)榻y(tǒng)計(jì)這種小目錄是在耗時(shí)太短撑刺,短到比創(chuàng)建GCD Group的CPU占用都要少,所以耗時(shí)不降反增握玛,呵呵够傍。但一旦這個(gè)耗時(shí)任務(wù)CPU占用大于GCD消耗的時(shí)候,并行操作帶來的耗時(shí)收益就是
串行總耗時(shí) - 并行最大耗時(shí)
小結(jié)
這節(jié)課由于篇幅有限挠铲,我們講的內(nèi)容并不多冕屯,但實(shí)用性很高疗我。大家注意到?jīng)]有谋减,從頭到尾我們等于講任何與GCD有關(guān)的接口調(diào)用、類型相關(guān)的內(nèi)容麦向,卻教會(huì)了你進(jìn)行異步請(qǐng)求和同步等待操作的方法,模版拿過來基本不用修改就能嵌套使用浴韭,這就叫知其然丘喻。下一章節(jié)我們?cè)購腁PI方向講解GCD的類型和方法調(diào)用,這叫知其所以然念颈。