你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> 深入學習中央調度(GCD)--第一部分

深入學習中央調度(GCD)--第一部分

編輯:IOS開發綜合
盡管中央調用(簡稱GCD)已經存在一段時間了,但並不是每個人都知道如何有效地使用它。這是可以理解的,並發本身就是棘手的,然而基於C語言的GCD API看起來像一套深入OC世界的彎角(轉換器)。這個系列教程分兩部分,深入地介紹中央調度(GCD)。 在這兩部分中,第一部分解釋了什麼是GCD以及GCD常用的幾個基本函數,在第二部分中,將會介紹幾個GCD提供的更高級的功能。 **

什麼是GCD?

**
libdispatch俗稱GCD,蘋果提供的庫,用以支持在iOS和OS X的多核硬件上執行並行代碼。它有以下幾個有點:
1、GCD可以通過延緩耗時的計算任務放在後台運行來提高App的響應能力
2、GCD提供了比加鎖和線程更加簡單的並發模型來避免並發bugs
3、GCD可以使用高性能的執行單元優化代碼,比如常用的模式:單例
本教程假設你已經對blocks和GCD有一個基本的了解,如果是全新接觸GCD,可以查閱供初學者;了解學習要點的基於iOS的多線程處理和中央調度。

 **

GCD術語

**
要理解GCD,需要能應付自如幾個跟線程和並發相關的概念。這些可能既模糊又微妙,所以在GCD的上下文中花點時間去簡要回顧一下它們。
1、串行與並行
這倆術語描述了任務被執行時彼此的關系。串行執行任務每次執行一個任務,並發執行任務可能在同一時間執行多個任務。
盡管這些術語有廣泛的應用,但對於該教程來說,你可以把一個任務當做是一個OC代碼塊。不知道什麼是塊(block)?請查閱在iOS5下如何使用blocks。實際上,你也可以以函數指針的方式使用GCD,但在大多數情況下這樣使用起來更加棘手。Blocks是更加簡單的。
2、同步與異步
在GCD下,這倆術語描述了當一個功能完成之後與之關聯的另一個任務功能如何請求GCD調用執行。同步意味著僅當任務按序執行完畢之後才會返回。
異步,換句話說,就是立即返回預定的任務要執行但是不會等待。因此,異步不會阻塞當前線程的執行繼續向下執行。
注意,當你看到一個阻塞當前線程、函數或操作的的同步操作時,不要弄混了。這個動作塊描述了一個功能是如何影響它的線程,並且沒有連接到名詞塊(描述了一個在OC中的字面匿名函數且定義了一個提交到GCD的任務)。
3、臨界區
這是一段不能被並發執行的代碼,那就是,同時只能有一個線程執行。這就是一般並行進程訪問共享資源(比如變量)的代碼變壞的原因。
4、爭用條件
這種情況是由軟件系統依賴一個特殊的序列或在一個不受控制的事件(如:程序的並發任務的確切執行順序)執行時間下產生的。爭用情況可能產生不可預期的行為,而且不是立即可以通過代碼檢查就能發現的。
5、死鎖
在大多數情況下,兩個(有時更多)元素被說成是線程死鎖是因為他們陷入了彼此等待而不能正常的完成或執行其他行為。一個不能結束是因為正在等待另一個結束。另一個不能完成是以為在等待第一個結束。
6、線程安全
線程安全的代碼可以安全地被多個線程或並發任務調用而不會引起任何問題(如:數據異常、崩潰等)。線程不安全的代碼同一時間下僅僅可以在一個上下文中運行。一個線程安全的例子就是不可變字典,你可以在多個線程中同時使用而不會出問題。換句話說可變字典不是線程安全的,因為同一時間下,僅可以可以在一個線程中訪問(安全而不出問題)。
7、上下文切換
上下文切換是指當一個程序存儲和恢復執行狀態(當你在單個進程中在不同線程間切換時)。這中程序在你寫多任務程序時很常見,但是也帶來了一些額外的開銷作為代價。
並發以並行
並發和並行常常被同時提到,因此簡要的說明下兩者之間的區別還是值得的。
分離的並發代碼可以被“同時”執行。然而,這是由系統決定如何發生-或者如果完全發生的話。多核心得設備同時執行多個線程通過並行。然而在單核心設備中為了達到並發,運行一個線程時必須通過上下文切花來運行另一個線程。這通常發生的足夠快,我們可以假想它按下圖方式執行:

這裡寫圖片描述
盡管你可能會在GCD下寫代碼以使用並發執行,但最終是由GCD決定多少並行是必須的。並行必定並發,但是並發不能保證並行。
這裡更深層點的問題是,並發實際上是結構上的。當你在頭腦中構思GCD代碼時,就要規劃代碼結構以拆分為可同時運行的工作片和可以不必同時運行的任務。如果想更深入地研究這個問題,查閱這個精彩的演講(this excellent talk by Rob Pike.)。
隊列
GCD提供了調度隊列以處理代碼塊,這些隊列管理你提交到GCD的任務並按FIFO順序執行。這保證了第一個進入隊列的任務是第一個開始執行的,第二個添加到隊列的將第二被執行,接下來按序。
所有的調度隊列對他們自己而言是線程安全的,可以在不同的線程中同時訪問。GCD的優勢是明顯的(當你理解自己不同部分的代碼是如何線程安全地訪問調度隊列時)。關鍵就是選擇合適的調度隊列類型和合適的dispatching函數提交自己的任務到隊列。
這節中,將看到兩種類型的調度隊列,GCD提供的特定隊列以及通過一些列子說明如何使用GCD調度函數添加任務到調度隊列。
1、串行隊列
在串行隊列中的任務一次執行一個,每個任務的開始必須是前面的任務完成之後。當然,也無需知道一個代碼段何時結束及下一個何時開始,如圖所示:
這裡寫圖片描述vcq9vs3Kx82ouf3M4b27tb2197bIttPB0LXEyM7O8bfDzsqjrLGj1qTB2b3nx/iwssiroaM8YnIgLz4NCjKhorKit6K208HQPGJyIC8+DQrU2rKit6K208HQ1tC1xMjOzvG99r32xNyxo9aksLTV1cztvNO9+LXEy7PQ8sb0tq+jrGFuZNXi0rLKx8TcsaPWpLXEy/nT0KGj1KrL2L/JxNzSu8jOus7Ls9DyveHK+KOsxOPSsrK7xNzIt7aoz8LSu7j2YmxvY2u7udKqtuCzpMqxvOSyxcTcv6rKvKOszazKsdTa1rTQ0LXEYmxvY2tzyv3Ev9KysrvE3Mi3tqijrNXitrzKx0dDRL72tqi1xKGjPGJyIC8+DQrPwsPmtcTNvLHt1bnKvsHL0ru49sjOzvHWtNDQtcTKvsD9o6zG5NbQR0NEv9jWxsHLNLj2sqK3osjOzvGjujxiciAvPg0KPGltZyBhbHQ9"這裡寫圖片描述" src="/uploadfile/Collfiles/20160817/20160817094507308.png" title="\" />
備注:現在block1,2和3運行很快,一個接一個。block1開始執行花費了一點時間在block差不多執行結束後才開始。同樣的,在block2開始後block3也開始執行了但並不是block2結束後才開始。
何時開始一個block執行完全由GCD決定。如果執行一個block的時間超時了,GCD會決定是否在另一個可用的核心上開始另一個任務或者切換上下文去執行另一個不同的 代碼塊。
令人欣喜的是,GCD提供了至少5個特別的隊列類型可供選擇。
3、隊列類型
首先,系統提供了一個特別的串行隊列成為主隊列。像任何串行隊列一樣,在這個隊列中一次只能執行一個任務。然而,它可以保證所有的任務都在主線程(必須要保證所有更新UI的操作必須在這個線程執行)中執行。這個隊列是一個用於接收UIView消息和通知的隊列。
該系統還提供了其他幾個並發隊列。這些統稱為全局調度隊列。有4個不同優先級的全局隊列:background, low, default, high.值得一提的是,蘋果的api也使用這些隊列,因此你添加任何任務到這些隊列,其中任務不只有你添加的。
此外,你也可以創建你自定義的串行或並行隊列。這意味著至少有5個隊列任由你處置:主隊列,4個全局隊列,再加上任何一個你添加的自定義的隊列。
這就是調度隊列的“偉大藍圖”。
GCD的藝術來源於選擇合適的隊列去提交任務。最好的經驗就是通過下面的例子學習,在哪裡我們根據長期經驗提供了一些一般性的建議。

開始

 由於本教程的目的是既要簡單又要安全的從不同的線程調用代碼,你將從頭到尾完成這個GoodPuff項目。
      GoodPuff是一個非優化的,非線程安全的app。在這裡你要瞪大眼睛去分辨COre image API的使用。對於基本的圖片來說,你可以從相冊庫中選擇也可以從一系列未知的圖片url下載使用 。
      Download the project here.
      一旦下載好,提取到一個合適的位置,用Xcode打開並運行它,看起來會像下面一樣:

這裡寫圖片描述
注意:當你選擇下載圖片選項時,UIA了人VIew會過早的彈出,浙江在本系列的第二部分修復。
在這個項目中使用了四個類:
PhotoCollectionViewController:這是啟動app後的第一個視圖控制器。它以縮略圖的形式展示所有選中的圖片。
PhotoDetailViewController:這個以大圖的形式在UIScrollView中展示圖片
Photo:這是一個類聚合,其可以從NSURL或ALAsset創建圖片。該類提供圖片,縮略圖或一個下載圖片的狀態。
PhotoManager:該類管理所欲照片實例。

用dispatch_sync處理後台任務

回過頭來看該app,從相冊庫添加一些圖片或使用網絡下載一些。
關注下在點擊UICollectionViewCell後,花費了多長時間去實例化顯示一個新的PhotoDetailViewController,有明顯的滯後,尤其是在反應慢的設備上預覽大圖時。
在很多復雜的環境下,執行UIViewController’s viewDidLoad很容易過載,在新視圖顯示之前常常要等待較長時間,在加載時不是必要的工作可以放在後台處理。
這聽起來像是異步工作。
打開PhotoDetailViewController,替換viewDidLoad用下面的實現:

- (void)viewDidLoad
{ 
    [super viewDidLoad];
    NSAssert(_image, @"Image not set; required to use view controller");
    self.photoImageView.image = _image;

    //Resize if neccessary to ensure it's not pixelated
    if (_image.size.height <= self.photoImageView.bounds.size.height &&
        _image.size.width <= self.photoImageView.bounds.size.width) {
        [self.photoImageView setContentMode:UIViewContentModeCenter];
    }

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1
        UIImage *overlayImage = [self faceOverlayImageFromImage:_image];
        dispatch_async(dispatch_get_main_queue(), ^{ // 2
            [self fadeInNewImage:overlayImage]; // 3
        });
    });
}

上面是將要修改的代碼。
1、首先從把任務從主線程放到全局隊列中。因為這是dispatch_async(異步),代碼塊被異步提交意味著將在從線程中調用。這可以讓viewDidLoad可以盡快在主線程中執行完,讓加載感覺特別快。同時,圖片加載開始執行,將在之後某個時間完成。
2、這時候,圖片加載處理已完成,你已經生成一個新的圖片。你可以拿新圖片去更新顯示。到主隊列中添加一個新的工作。記住,只能在主線程中更新UI。
3、最後,在fadeInNewImage中更新UI。
生成並運行app,選擇圖片你會發現試圖控制加載明顯更快,並在短暫的時間後顯示大圖。這提供了一個不錯的查看大圖的效果。
同樣的,如果你試著加載一個出奇巨大的圖片時,這個app也不會再加載視圖控制器的時候卡住,同時app可以很好的擴展。
正如上文提到的,dispatch_async將添加block到一個隊列並立即返回。該任務將在一段時間之後被GCD決定執行。當需要執行一個網絡操作或cpu耗時的任務時放在後台不會阻塞當前線程的執行。
下面是一個如何、何時使用dispatch_async的各種隊列類型的快速向導:
自定義串行隊列:當要後台串行執行任務、要跟蹤它時,這是一個不錯的選擇。這消除了資源爭用,因為你已經知道同一時間只能有一個任務執行。注意,如果你需要從一個方法獲取數據,必須內嵌另一個 block進去同時考慮采用dispatch_sync方式。
主隊列(串行):在一個並行的隊列中完成任務後去更新UI,選擇它。這樣做的話,將內嵌另一個block到block中。同理,如果在主隊列中調用dispatch_async,只能保證新任務在當前方法結束後一段時間內將會執行。
並行隊列:要在後台執行非UI工作可以選擇它。

延時工作dispatch_after
考慮一下app的用戶體驗,當用戶第一次打開app時可能會很困惑,不知道要做什麼。
展示一個提示信息可能是一個不錯的主意,當在PhotoManager中沒有任何照片時。但是,你也需要考慮用戶的眼睛是如何浏覽屏幕主頁的,如果你展示圖示信息太快(一閃而過)的話,他們可能根本沒有看清視圖中顯示的內容。在顯示提示信息時加上1~2秒的延時足夠吸引用戶注意了。
添加下面的代碼再執行去試著實現顯示延時:(showOrHideNavPromote PhotoCollectionViewController)

- (void)showOrHideNavPrompt
{
    NSUInteger count = [[PhotoManager sharedManager] photos].count;
    double delayInSeconds = 1.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2
        if (!count) {
            [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"];
        } else {
            [self.navigationItem setPrompt:nil];
        }
    });
}

生成並運行app。輕微的延遲,將吸引用戶的注意,提示他們該怎麼做。

dispathc_after工作就像一個延時的dispatch_async。你依然沒有實際執行時間的控制權,但是可以在其返回之前取消。
想知道什麼時候使用dispatch_after嗎?
1、自定義串行的隊列:在自定義串行隊列上小心使用,最好在注隊列使用。
2、主隊列:主隊列可以使用dispatch_after,Xcode有一個nice模板去自動創建使用它。
3、並發隊列:在自定義的並發隊列上使用dispatch_after時要小心,而且很少用。堅持在主隊列使用它。

使單例模式線程安全

單例模式:既愛又恨,在iOS和在服務器器系統上的web一樣受歡迎。
復雜的單例關系常常不是線程安全的。這種關系要合理的使用:單例模式就是常常多個視圖控制器同時訪問單個單一實例。
對單例來說,線程關系涉及到初始化、讀取和寫入信息。
PhotoManager類就是單例類,在當前狀態下就面臨這些問題。為了更快的看到問題所在,將要在單例中創建一個受控的爭用條件。
定位到PhotoManager.m 然後找到sharedManger,代碼想下面這樣:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}
 當前狀態下代碼是很簡單的,你創建了一個單例然後初始化了一個私有數組(photoArray)。
但是,if條件分支不是線程安全的,如果你多次調用它,很有可能出現在線程A中進入if代碼段然後在執行sharedManager分配之前進行了上下文切換。然後在線程B中可能也進入if條件,分配了一個單實例而後退出。當系統上下文切換回線程A後,繼續分配領一個單實例後退出。同時將產生兩個單實例,這不是我們想看到的。
 為了防止這種情況發生,替換sharedmanager方法用下面的實現:
+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager = [[PhotoManager alloc] init];
        NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}
 上面代碼中,在線程休眠方法中強制進行上下文切換。打開AppDelegate.m添加如下代碼:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [PhotoManager sharedManager];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [PhotoManager sharedManager];
});
 這將創建多個異步並發調用來實例化單例並會出現上文所述的爭用情況。
 生成並運行項目,檢查控制台的輸出,將會看到多個單實例初始化,如下所示:

這裡寫圖片描述
注意:有幾行顯示了單例實例不同的地址,偏離了單例的初衷,不是嗎?
輸出顯示只應被執行一次的臨界區卻被執行了多次。誠然,現在是你強制這種情況發生,但是你可以想象一下這種情況也會在不經意間偶然出現。
注:基於系統之上的其他事件很難控制,一系列的NSLog打印證明這點。線程問題很難跟蹤因為它很難復現。
為了糾正這種問題,當運行在臨界區的if條件中時,初始化代碼應該僅被執行一次並阻塞其他實例。這個正是dispatch_once做的事情。
替換單例中中的if條件語句用下面的單例初始化實現:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager = [[PhotoManager alloc] init];
        NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    });
    return sharedPhotoManager;
}

生成並運行app,查閱控制台輸出,你講看到僅有一個單實例被初始化,這才是我們想要的單例模式。
現在既然理解了防止爭用條件的重要性,就刪除AppDelegate.m中添加的diapatch_async語句然後替換單裡初始化的實現用下面的實現:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    });
    return sharedPhotoManager;
}
 dispatch_once 執行塊一次,且以線程安全的方式僅僅執行以一次。不同的線程試圖訪問臨界區,代碼執行到dispatch_once,當一個線程已經在代碼塊中是將獨占臨界區知道完成。

這裡寫圖片描述
應該指出的是這僅僅是共享實例的線程安全,並不一定類線程安全。你也可以有其他的臨界區,例如:然和可操作的內部數據。這些需要使用線程的安全的其他方式,譬如:同步黨文數據,下面將會看到。

處理讀和寫的問題

線程安全的實例化單例不是唯一的問題。如果單例的屬性是一個可變的對象,你就需要考慮對象本身是否是線程安全的。
Foundation中的基礎容器是線程安全的嗎?答案是-不是的。蘋果維護了一系列有益的非線程安全的基礎數據類型。
盡管,很多線程可以讀取一個可變的數組而沒有問題,但讓一個線程去修改數組在其他線程讀取的時候是不安全的。你的單例模式沒有避免這種情況發生。
為了看到問題,看下addPhoto在PhotoManager.m中(已經被替換為如下:)

- (void)addPhoto:(Photo *)photo
{
    if (photo) {
        [_photosArray addObject:photo];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self postContentAddedNotification];
        });
    }
}
 這是一個寫操作去修改一個可變的數組。
 現在修改photos屬性如下:
- (NSArray *)photos
{
  return [NSArray arrayWithArray:_photosArray];
}
 這個屬性的getter方法是一個讀方法去訪問這個可變數組。這個調用獲得一份不可變的拷貝以免被不當破壞,但是沒有提供任何保護(當一個線程正在寫方法addPhoto時,另外線程去讀這個屬性)。
 這就是軟件系統的讀寫問題。GCD提供了一個優雅的解決方案通過使用調度障礙來創建讀寫鎖。
 調度障礙(柵欄)是一組函數像在並行隊列中的串行式障礙一樣。使用GCD阻塞API確保提交的閉包是該特定隊列上在特定時間是唯一的被執行元素。這就意味著所有被提交到隊列的元素必須在閉包被執行之前完成。
 當輪到該閉包時,阻塞執行閉包確保在這段時間內隊列不會執行其他閉包。一旦完成,隊列返回到默認實現位置。GCD提供同步和異步兩個障礙方法。
 下面的圖片說明了障礙函數在各種異步任務中的影響:

這裡寫圖片描述
請注意如何讓正常的操作隊列行為就像一個正常的並發隊列。但是當障礙執行的時候,它本質上就是一個串行隊列。只有該障礙在執行。在障礙完成之後,隊列回歸為正常的並發隊列。
下面是何時會用,何時不會用:
1、自定義串行隊列:在這裡選用很糟糕,因為一個串行隊列在任何時候都是僅有一個任務在執行。
2、全局並發隊列:這裡注意,不建議選用,因為系統可能正在使用該隊列而你不能自己獨占他們自己一個人使用。
3、自定義並發隊列:這是一個很好的選擇以原子操作的方式去訪問臨界區。任何你正在設置或初始化的且需要線程安全的都可以選用。
既然,唯一可以正當選用的選擇就是自定義並發隊列,你可以創建自己的處理在單獨的讀和寫函數中。並發隊列允許同時多個讀操作。
打開PhotoManager.m,添加下面的私有屬性到類的補充實現中:

@interface PhotoManager ()
@property (nonatomic,strong,readonly) NSMutableArray *photosArray;
@property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this
@end

找到addPhoto:,替換為下面的實現:

- (void)addPhoto:(Photo *)photo
{
    if (photo) { // 1
        dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2
            [_photosArray addObject:photo]; // 3
            dispatch_async(dispatch_get_main_queue(), ^{ // 4
                [self postContentAddedNotification];
            });
        });
    }
}
 下面介紹下你的寫函數是如何工作的:
      1、在做所有工作之前,確保圖片有效。
      2、使用自定的隊列去添加寫操作。在稍後的時間內在你的隊列中該元素將是臨界區的唯一執行元素。
      3、這是對象添加到數組的實際代碼。因為這是一個障礙閉包,這個閉包將永遠不會和其他閉包同時執行在該隊列中。
      4、最後你發送了一個通知表明添加了一個圖片。這個通知將從主線程發送因為它要處理UI工作。所以這裡調度了一個異步任務到主隊列去處理通知。
 注意寫操作,你也需要實現圖片讀操作。
 為了確保寫入方的線程安全,你需要在該隊列中執行讀操作。你需要從函數中返回,因此你不能異步調度執行因為那樣將導致在讀函數返回之前永遠不會執行。
 在這種情況下,同步將是一個很好的選擇。
 dispatch_sync同步提交任務然後等待直到完成才執行返回。使用dispatch_sync來保持跟蹤你的dispatch_barrier工作,或在你可以通過閉包使用數據之前你需要等待操作完成。
 你需要小心。想想一下,如果你調用dispatch_sync然而作用目標即當前隊列已經在執行了。這將導致死鎖因為調用將等待閉包完成,但是閉包(它甚至不能開始)將在當前正在執行的、不可能結束的閉包結束後才能結束。這將迫使你意識到你正在調用的隊列就是你正在傳遞的隊列。
 下面是一個快速的預覽何時何地可以使用dispatch_sync:
      1>、自定義串行隊列:這種情況下要非常小心,如果你正在運行的一個隊列正好是你dispatch_sync的目標隊列這將導致死鎖。
     2>、 主隊列:要小心使用,原因跟上面一樣。這種情況也同樣存在潛在的死鎖。
      3>、並行隊列:這是一個不錯的選擇通過dispatch_barrier或者當等待一個任務完成以便你可以進行下一步操作時。

還在PhotoManager.m,用下面的實現替換屬性:

- (NSArray *)photos
{
    __block NSArray *array; // 1
    dispatch_sync(self.concurrentPhotoQueue, ^{ // 2
        array = [NSArray arrayWithArray:_photosArray]; // 3
    });
    return array;
}
 這是一個讀函數,依次看注釋就會發現:
     1、__block關鍵字允許在塊內部改變該對象,沒有這個的話,array在塊內將是只讀的,你的代碼甚至不能通過編譯。
      2、隊列中的同步調度執行讀操作
      3、保存photoArray的拷貝然後返回。

祝賀你,你的PhotoManager單例現在是線程安全的。無論在哪兒或者以何種方式讀或寫圖片,它都將以線程安全的方式毫無意外的正常工作。
最後,你需要初始化你的並發隊列屬性。像下面這樣改變sharedManager去初始化:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];

        // ADD THIS:
        sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue",
                                                    DISPATCH_QUEUE_CONCURRENT);
    });

    return sharedPhotoManager;
}
 使用dispatch_queue_create創建一個並發的隊列。第一個參數是反向DNS域名。這樣的描述在調試的時候很有用。第二個參數標識隊列是串行還是並行。
 注:當在web上查找例子時,你常常看到別人穿0或者NULL作為dispatch_queue_Create的第二個參數。這是一種已經過時的方式,使用具體的(系統提供的枚舉類型DISPATCH_QUEUE_CONCURRENT等)作為參數總歸是更好的。
 祝賀你,你的PhotoManager單例現在是線程安全的。無論在哪兒或者以何種方式讀或寫圖片,它都將以線程安全的方式毫無意外的正常工作。

視圖形式查看排隊

 現在是不是還是不能完全掌握GCD的要點?確信可以使用GCD方法創建簡單的例子並且使用斷點和NSLog去了解正在發生的事情(GCD執行過程中)。
 下面提供了兩個GIFs的例子幫助你加強dispatch_async和dispatch_sync的理解。代碼以輔助可視化工具的形式包含在GIF圖中,注意左邊GIF中斷點的每一步以及右邊關聯隊列的狀態。

dispatch_sync初探:

override func viewDidLoad() {
  super.viewDidLoad()

  dispatch_sync(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {

    NSLog("First Log")

  }

  NSLog("Second Log")
}

這裡寫圖片描述
下面簡要介紹下關系圖表:
1、主隊列按序執行,接下來就是一個任務初始化(視圖控制器初始化加載viewDidLoad)。
2、viewDidLoad在主線程執行。
3、同步閉包被添加到全局隊列且稍後執行。進程上表現是主線程停止知道該閉包完成。同時,全局隊列是並發執行任務的,在全局隊列上是按FIFO的順序喚起閉包的執行,但是可能是並發執行的。全局隊列同時還處理在該同步閉包添加到隊列之前就已經存在的任務。
4、最後,該同步閉包按序執行。
5、該閉包完成之後,主線程才被喚醒。
6、viewDidLoad方法執行完畢,主隊列接著處理其他任務。
同步添加任務到隊列,然後等待直到該任務完成。異步添加的話,唯一不同的是在被調起的線程裡無需等待任務完成就可以繼續執行向下執行。

dispatch_async初探:
override func viewDidLoad() {
  super.viewDidLoad()

  dispatch_async(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {

    NSLog("First Log")

  }

  NSLog("Second Log")
}

這裡寫圖片描述
1、主隊列按序執行,接下來就是初始化一個視圖控制器任務,viewDidLoad在主線程執行。
2、viewDidLoad在主線程執行。
3、主線程現在在viewDidLoad裡,剛好執行到dispatch_async。
4、異步閉包被添加到全局隊列,稍後執行。
5、viewDidLoad在異步閉包添加到全局隊列之後繼續執行,主線程繼續執行未完成的任務。同時,全局隊列並發的執行其未完成的任務。記住:全局隊列任務是按FIFO順序出棧執行,但是執行過程中可以是並發的。
6、被添加的異步閉包正在執行。
7、異步閉包完成,同時控制台已經有NSLog輸出。
在該熱定情況下,第二個NSLog緊隨第一個NSLog執行。但並非總是如此,這取決於正在執行的硬件時間,你沒有辦法知道那個輸出先執行。在一些調用中先執行的NSLog可能是第一個NSLog中執行的。

接下來學習什麼?

在這個教程中,已經了解了如何使代碼線程安全,以及在CPU多任務處理下如何保持主線程的響應能力。
你可以下載GooglyPuff Project 工程,其中包含了所有目前教程中的實現。第二部分的教程中你將去改進這個項目。
如果你計劃優化你的app,你應該使用Instruments進行性能分析。使用這個工具超出了本教程的范圍,所以你應該查閱一些關於如何使用它的優秀文章。
確保有真機可以使用,因為模擬器測試出來的記過和真實用戶使用的體驗反饋是不一樣的。


  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved