[TOC]數據庫
Grand Central Dispatch 是蘋果公司發佈的一套多核多線程任務分發的解決方案,簡稱GCD,或者你叫他滾牀單也沒有人反對,嘿嘿。編程
蘋果公司首次發佈GCD是伴隨Mac OS X 10.6 和 iOS 4系統一塊兒發佈的,也正是伴隨着block塊語法的支持,GCD技術將多線程執行代碼,經過block封裝成代碼塊,大大提升了多線程開發的效率,減小了開發難度,也極大加強了代碼的可讀性。網絡
若是我將GCD技術比喻成普羅米修斯帶給人類的火種有一些誇張的話,至少能夠將其比做火柴。而在沒有生火器的石器時代,人類只能依靠何鑽木取火。session
POSIX線程(pthread)是一套C語言編寫的線程管理API,面向過程,我只在老東家一套C源碼庫中見別人用過,本身歷來沒有用過,也不會用,就像我也不會鑽木取火同樣。多線程
Cocoa框架中,用OC將pthread對象化封裝,就誕生了NSThread
操做類,但很惋惜至今NSThread.h
頭文件中一行註釋都木有,只能看出這個類早在1994年就已經存在了。框架
這裏就不列舉具體事例了,由於現在這個類的使用頻率已經很是低了,惟一一種你可能會遇到的使用情境是判斷當前執行線程是否爲主線程,具體代碼以下異步
if([NSThread isMainThread]){ }
但你在GCD和NSOperation出現以前,會在各類須要多線程處理的狀況下,使用NSThread
的隱式調用方法,也就是NSThread頭文件中給NSObject
類做爲屬性方法擴展的一系列接口:async
@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
總計五個API,簡易實現了通常開發須要使用的基本線程操做,避免用戶本身動手寫NSThread調度,引起的一些列莫名的死鎖問題,在某種程度上減小了當時的多線程開發難度。函數
但這些API有一些很直觀的問題,例如因爲OC語言限制,這些API的參數傳遞、返回值獲取都不易實現,而且實際寫出來的代碼也會由於邏輯跳轉分佈在文件的各個位置,影響閱讀和糾錯,你不相信請看我從教科書上抄下來的例子:工具
- (void)launchThreadByNSObject_performSelectorInBackground_withObject { [self performSelectorInBackground:@selector(doWork) withObject:nil]; } - (void) doWork { /* * * 長時間處理 * * 例如 圖像處理 * 網絡數據請求 * 大型數據庫操做 * 磁盤操做 */ //操做結束後調用主線程修改UI [self performSelectorOnMainThread:@selector(doneWork) withObject:nil waitUntilDone:NO]; } - (void) doneWork { //主線程修改UI }
這個例子是一個解決關於主線程刷新UI問題的例子,咱們同窗都知道全部有關UI刷新的方法,務必要在主線程調用,這是個硬性要求,是由於UI渲染就是在主線程循環中完成的,若是在支線程中調用,會出現莫名其妙的錯誤、UI卡死或者程序崩潰。
因此多線程在咱們的平常開發中,用得最多的地方,就是網絡數據的異步請求,而後主線程刷新UI。將有延遲和計算量大的操做放在支線程完成,待完成後使用主線程刷新UI,纔能有效地防止主線程UI刷新阻塞。
iOS 4帶來的編譯器對block塊語法的支持,有點像人類發現了磷這種易燃物質同樣,帶來的是火柴(GCD 和 NSOperation)這個更簡易的生火工具。
GCD和NSOperation 能夠看做是 pthread(面向過程)和NSThread(面向對象)的block升級版本,帶來的多線程編程體驗則是質的飛躍。
GCD像是火柴,輕便易用,隨用隨取。NSOperation則像打火機,一次開發,重複使用。
好了,已經說了十幾分鍾廢話了,終要進入主題進行GCD多線程開發實戰。在開始以前,但願你們要提早學習block塊語法的相關知識,不要求熟練使用,只要求看得懂。
還記得咱們在上面展現的從教科書上抄下來的例子麼,這個例子若是改爲GCD的版本,會是什麼樣子的呢?
//異步請求Dispatch dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //長時間處理 dispatch_async(dispatch_get_main_queue(), ^{ //主線程更新UI }); });
這是什麼鬼?我來解釋一下。GCD使用的是C語言風格的調用接口,栗子中調用了兩次dispatch_async
方法,第一次將長時間處理操做分撥到支線程處理,在其完成後,跳轉回主線程更新UI,操做都在方法的block參數中傳入,簡單明瞭,層級分明,沒有傳參障礙,沒有閱讀障礙,一鼓作氣,簡直美極了。
dispatch_async
方法傳入的第二個參數是執行block,沒啥好說的,第一個參數則是線程。dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
方法獲取的Global線程,是非主線程中的一個,具體是哪一個不用開發者操心,反正是系統認爲這時候不是很忙的那一個。而兩個傳入參數中的第一個是線程的優先級(共四個優先級),第二個參數則約定爲0。dispatch_get_main_queue()
這個沒有任何參數的方法,返回的則是主線程。
注意這裏返回的參數類型是dispatch_queue_t
,是一個普通變量,估計是線程的索引。真是兩三句話就能講明白的方法調用,什麼你說聽不懂、看不懂。無所謂呀~ 咱們將這段代碼加入代碼片斷,須要使用的時候拿出來用就行啦。
好比:
首先放開權限NSAppTransportSecurity
,NSAllowsArbitraryLoads
不加多線程異步操做
- (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; }
- (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刷新的阻塞感覺了麼?啊?你說沒有,那你用真機調試一下,就會有更明顯的感覺了。
UI阻塞在實際開發中,偶爾會遇到。而且會引發一些莫名其妙的bug,但願你們再遇到時候能及時往這方面思考。好比,咱們若是在UIViewController
的初始化等一系列加載函數中加入能引發阻塞的代碼,整個VC的加載會產生卡頓,還頗有可能直接崩潰。
因此將阻塞操做放在支線程處理,是十分必要的。咱們只要將下面代碼存爲代碼片斷,隨用隨取。
//支線程調用 dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ <#code#> }); //主線程調用 dispatch_async(dispatch_get_main_queue(), ^{ <#code#> });
多線程操做的第二個經常使用情景就是並行操做等待。
dispatch_group_t group = dispatch_group_create(); // 合併彙總結果 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項操做都完成後調用主線程更新UI NSLog(@"4"); });
在這段演示代碼裏面,即便你看不懂GCD相關調用,也能猜出最後的輸出結果對吧,我解釋一下[NSThread sleepForTimeInterval:1.0];
這句調用是讓線程睡眠1秒中,模擬1秒鐘阻塞。
好的告訴我你的答案。
3 2 1 4
這也是一段能夠收藏爲代碼片斷的實用工具,能夠起名爲並行代碼等待
。就像異步等待
同樣,咱們如今來舉一個簡單的實際案例。
仍是舉一個不是很簡單的例子,也可能不是很實用,但絕對能體現這套邏輯的精髓。在講栗子以前,咱們先來學習一下SDWebImage的另一段代碼(對,又是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; }
這段代碼,具體功能是進行文件夾文件大小的統計。對你沒有聽錯,文件夾是沒法直接接獲取其大小的,須要遍歷其中每一個文件而後相加統計。
這段代碼實用GCD,但使用的方法咱們前面並無講過,我放在後面再說。目前咱們的任務是把這個方法改造一下,讓他能夠統計任意的文件夾大小。
- (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; }
接下來咱們統計一下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目錄,不過由於cache目錄在libiary目下,因此是有重複的,不過無所謂。
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));
咱們在執行這段代碼的時候,通常會很順暢就執行完了,沒有任何阻塞。緣由是統計的目標目錄,文件很是少。若是遇到文件稍多的狀況,上面這段代碼就出出現阻塞,又由於整個是在主線程操做的,因此必然會影響到UI的刷新,界面會卡頓。好,那讓咱們運用前面的GCD模版來將這段代碼改形成異步執行。
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");
上面是我修改的結果,你們來分析一下輸出順序,應該是
1 2 total size : 1314324 (657060+657264)
前面坐了這麼多鋪墊,接下來咱們進入正題,講解一下串行和並行。串行很好理解,咱們通常寫的代碼都是一步一步一串一串執行的。並行則是多項任務同時進行,也不難理解,相似於中學物理學的電路的並聯合串聯。
上面這段代碼,咱們先後調用兩次getSize方法,按順序分別統計了兩個目錄的大小,咱們統計一下耗時:
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
這裏單位是秒,其實已經很快。 好,咱們把前面的並行模版套進來。
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(); // 合併彙總結果 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
結果很讓我欣慰,整整大了一個數量級,請大家分析一下緣由。
緣由也很簡單,就是由於統計這種小目錄是在耗時過短,短到比建立GCD Group的CPU佔用都要少,因此耗時不降反增,呵呵。但一旦這個耗時任務CPU佔用大於GCD消耗的時候,並行操做帶來的耗時收益就是
串行總耗時 - 並行最大耗時
這節課因爲篇幅有限,咱們講的內容並很少,但實用性很高。你們注意到沒有,從頭至尾咱們等於講任何與GCD有關的接口調用、類型相關的內容,卻教會了你進行異步請求和同步等待操做的方法,模版拿過來基本不用修改就能嵌套使用,這就叫知其然。下一章節咱們再從API方向講解GCD的類型和方法調用,這叫知其因此然。