block 與 GCD

第 37 條 理解“塊”這一概念

Clang 是開發(fā)Mac OS X 及 iOS程序所用的編譯器

塊的基礎(chǔ)知識

塊的語法:
return_Type (^blockName)(parameter)
塊的強大之處是:在聲明它的范圍里,所有變量都可以為其所捕獲。這也就是說,那個氛圍里的全部變量在塊里依然可用。比如,下面這段代碼所定義的塊,就是用了塊以外的變量:
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
retrun a + b + additional;
}
int add = addBlock(2,5);
默認情況下,為塊所捕獲的變量,是不可以在塊里修改的。在本例中,假如塊內(nèi)的代碼改動了additional變量的值,那么編譯器就會報錯。不過聲明變量的時候可以加上__block修飾符,這樣就可以在塊內(nèi)修改了。

內(nèi)聯(lián)塊

NSArray *array = @[@0, @1, @2, @3,@4, @5];
__block NSInteger count = 0;
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL *stop) {
               if ([number compare:@2] == NSOrderedAscending) {
                    count ++
               }
           }]

這段范例代碼也演示了“內(nèi)聯(lián)塊”的用法。傳給“enumerateObjectsUsingBlock:”方法的塊并未先賦給局部變量,而是直接內(nèi)聯(lián)在函數(shù)調(diào)用里了。

塊是對象

  • NSObject對象所能相應的選擇子中,有很多是塊也可以相應的
  • 塊的結(jié)構(gòu)
  • 于塊本身也和其他對象一樣,有引用計數(shù)
    如果塊所捕獲的變量是對象類型,那么就會自動保留它。系統(tǒng)在釋放這個塊的時候,也會將其一并釋放。這就引出了一個與塊有關(guān)的重要問題。塊本身可視為對象。實際上,在其他Objective-C對象所能相應的選擇子中,有很多是塊也可以響應的。二最重要之處則在于塊本身也和其他對象一樣,有引用計數(shù)。當最后一個指向塊的引用移走之后,塊就回收了?;厥諘r也會釋放塊所捕獲的變量。以便平衡捕獲時所執(zhí)行的保留操作。
`- (void)anInstanceMethod {
     void (^someBlock)() = ^ {
          _anInstanceVariable = @"someThing";
     }
 }

如果某個實例在執(zhí)行anInstanceMethod放法,那么self變量就會指向此實例。由于塊里沒有明確使用self變量,所以很容易就會忘記self變量其實也為塊所捕獲了。直接訪問實例遍歷和通過self來訪問時等效的:
self->_anInstanceVariable = @"someThing";

typedef void(^SomeBlock) (void);
@property (nonatomic, copy) BlockName someBlock;
`- (void)anInstanceMethod {
     self.someBlock = ^ {
          _anInstanceVariable = @"someThing";
     }
     
 }

self也是個對象,因而塊在捕獲它時也會將其保留。如果self所指代的那個對象同時也保留了塊,那么這種情況就會導致“保留環(huán)”
修改為以下代碼即可:

typedef void(^SomeBlock) (void);
@property (nonatomic, copy) BlockName someBlock;
__weak typeof(self)weakSelf = self;
`- (void)anInstanceMethod {
     self.someBlock = ^ {
          weakSelf.anInstanceVariable = @"someThing";
     }
     
 }

塊的內(nèi)部結(jié)構(gòu)

塊本身也是對象,在存放塊對象的內(nèi)存區(qū)域中,首個變量是指向Class對象的指針,該指針叫做isa.其余內(nèi)存里含有塊對象正常運轉(zhuǎn)所需的各種信息。

image.png
  • ivoke :在內(nèi)存布局中,最重要的就是ivoke變量,這是個函數(shù)指針,指向塊的實現(xiàn)代碼。函數(shù)原型至少要接受一個void*型的參數(shù),此參數(shù)代表塊。塊其實就是一種代替函數(shù)指針的語法結(jié)構(gòu),原來使用函數(shù)指針時,需要用“不透明的void指針”來傳遞狀態(tài)。而改用塊之后,則可以把原來用標準C語言特性所編寫的代碼封裝成簡明且易用的接口。
  • descriptor:descriptor變量是指向結(jié)構(gòu)體的指針,每個塊里都包含此結(jié)構(gòu)體,其中聲明了塊的大小,還聲明了copy與dispose這兩個輔助函數(shù)所對應的函數(shù)指針。輔助函數(shù)在拷貝及丟棄塊對象時運行,其中會執(zhí)行一些操作,比方說,前者要保留捕獲的對象,而后者將之釋放。
  • 塊還會把它捕獲的所有變量都拷貝一份。這些拷貝放在descriptor變量后面,捕獲了多少變量,就要占據(jù)多少內(nèi)存空間。請注意,拷貝的并不是對象本身,而是只想這些對象的指針變量。invoke函數(shù)為何需要把塊對象作為參數(shù)傳進來呢?原因在于,執(zhí)行塊時,要從內(nèi)存中把這些捕獲到的變量讀出來。

全局塊、棧塊及堆

一下代碼的寫法存在的問題

棧塊
void(^block)();
if(/* some condition */) {
     # 類似于塊的初始化
     block = ^{
         NSLog(@"Block A");
     };
}else {
     block = ^{
         NSLog(@"Block B");
     };
}
block();

定義在if及else語句中的兩個塊都分配在棧內(nèi)存中。編譯器會給每個塊分配好棧內(nèi)存,然而等離開了相應的范圍之后,編譯器有可能吧分配給塊的內(nèi)存覆寫掉。于是,這兩個塊只能保證在對應的if或else語句范圍內(nèi)有效。這樣寫出來的代碼可以編譯,但是運行起來時而正確事兒錯誤。若編譯器未覆寫待執(zhí)行的塊,則程序照常運行,若覆寫,則程序崩潰。(我用 button點擊多次,并沒有發(fā)生崩潰,說明問題不是必現(xiàn))
為解決此問題,可給塊對象發(fā)送copy消息以拷貝之。這樣的話就可以把塊從棧復制到堆了??截惡蟮膲K,可以在定義他的范圍外使用,而且,一旦復制到堆上,塊就成了帶有引用計數(shù)的對象了。后續(xù)的復制操作都不會真的執(zhí)行復制,知識遞增塊對象的引用計數(shù)。如果不再使用這個塊,那就應將其釋放,在ARC環(huán)境下會自動釋放,而手動管理引用計數(shù)時則需要自己來調(diào)用release方法。當引用計數(shù)降為0后,“分配在堆上的塊”會像其他對象一樣,為系統(tǒng)回收。而“分配在棧上的塊”則無須明確釋放,因為棧內(nèi)存本來就會自動回收,剛才那段代碼之所以危險,原因也在于此。
修改后的代碼

堆塊

void(^block)();
if(/* some condition */) {
     # 類似于塊的初始化
     block = [^{
         NSLog(@"Block A");
     } copy];
}else {
     [block = ^{
         NSLog(@"Block B");
     } copy]
}
block();

全局塊

除了棧塊,堆塊還有一類叫做“全局塊”。這種塊不會捕捉任何狀態(tài)(比如外圍的變量等),運行時也無須有狀態(tài)來參與。塊所使用的整個內(nèi)存區(qū)域,在編譯器已經(jīng)完全確定了,因此,全局塊可以聲明在全局內(nèi)存里,而不需要在每次用到的時候于棧中創(chuàng)建。另外,全局塊的拷貝操作是個空操作,因為全局塊絕不可能為系統(tǒng)所回收。這種塊實際上相當于單例。
下面就是個全局塊: (我沒懂)

void (^block)() = ^{
     NSLog(@"This is a block");
}

由于運行塊所需要的全部信息都能在編譯器確定,所以可把它做成全局塊。這完全是種優(yōu)化技術(shù):若把如此簡單的塊當成復雜的塊來處理,那就會在復制及丟棄該塊時執(zhí)行一些無謂的操作。

要點

  • 塊是C、C++、Objective-C中的詞法閉包
  • 塊可接受參數(shù)、也可返回值
  • 塊可以分配在?;蚨焉?,也可以是全局的。分配在棧上的塊可拷貝到堆里,這樣的話就和標準的Objective-C對象一樣,具備引用計數(shù)了。

第 38 條 為常用的塊類型創(chuàng)建typedef

塊具有其“固有類型”

每個塊都具備其“固有類型”(inherent type),因而可將其賦給適當類型的變量。這個類型由塊所接受的參數(shù)及返回值組成。例如有下面這個塊

^(BOOL flag, int value) {
      if(flag) {
         return value * 5;
      }else {
         return value * 10;
      }
}

變量類型及其相關(guān)賦值語句如下:

int (^variableName)(BOOL flag, int value) = ^(BOOL flag, int value) {
      if(flag) {
         return value * 5;
      }else {
         return value * 10;
      }
}

映射出塊類型的語法結(jié)構(gòu):

return_type (^blockName)(parameters)

為了方便可以為塊類型起一個別名

typedef return_type(^blockName)(parameters)
blackName tempName = ^(paramters) {
   // Implementation
}

當塊作為一個接受參數(shù)時:

原API
- (void)startWithCompletionHandler:(void(^)(NSData *data, NSError *error))completion;
別名之后的API
type void (^EOCCompletionHandler)(NSData data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;

這樣寫的好處:

  1. 修改之后,凡是使用了這個類型定義的地方,比如方法簽名等處,都會無法編譯,而且報的是同一種錯誤,于是開發(fā)者可以逐個修復
  2. 別名可以見名知意
  3. 用typedef給同一個塊簽名類型定義創(chuàng)建數(shù)個別名,可以按需修改不同別名。

要點

  • 以typedef重新定義塊類型,可令塊變量用起來更加簡單
  • 定義新類型時應遵從現(xiàn)有的命名習慣,勿使其名稱與別的類型相沖突
  • 不妨為同一個塊簽名定義多個類型別名。如果要重構(gòu)的代碼使用了塊類型的某個別名,那么只需修改姓應的typedef中的塊簽名即可,無須改動其他的typedef

第 39 條 用handler塊降低代碼分散程度

為用戶界面編碼是,一種常用的范式就是“異步執(zhí)行任務”(perform task asynchronously)。這種范式的好處在于:處理用戶界面的顯示及觸摸操作多用的線程,不會因為要執(zhí)行I/O或網(wǎng)絡通信這類耗時的任務而阻塞。這個線程通常稱為主線程。
如果應用程序在一定時間內(nèi)無響應,那么就會自動終止。iOS系統(tǒng)上的應用程序就是如此,“系統(tǒng)監(jiān)控器”在發(fā)現(xiàn)某個應用程序的主線程阻塞了一段時間之后,就會令其終止。

關(guān)于notification:

調(diào)用著可以指定某個塊應該安排在那個執(zhí)行隊列里,然而這不是必需的。若沒有指定隊列,則按默認執(zhí)行方式執(zhí)行,也就是說,將由投遞通知的那個線程來執(zhí)行。
`- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^)(NSNotification *))block;
此處傳入的NSOperationQueue參數(shù)就表示觸發(fā)通知時用來執(zhí)行塊代碼的那個隊列。

要點

  • 在創(chuàng)建對象時,可以使用內(nèi)聯(lián)的handler塊將相關(guān)業(yè)務邏輯一并聲明
  • 設(shè)計api時如果用到了handler塊,那么可以增加一個參數(shù),使調(diào)用者可通過此參數(shù)來決定應該把塊安排在哪個隊列上執(zhí)行。

第 40 條 用塊引用所屬對象時,不要出現(xiàn)保留環(huán)

要點:

  • 如果塊所捕獲的對象直接或間接的保留了塊本身,那么就得當心保留環(huán)問題
  • 一定要找個適當?shù)臅r機解除保留環(huán),而不能把責任推給API的調(diào)用者

第 41 條 多用派發(fā)隊列,少用同步鎖

同步塊

`- (void)synchronizedMethod {
    @synchronized(self) {
       // Safe
    }
}

這種寫法會根據(jù)給定的對象,自動創(chuàng)建一個鎖,并等待塊中的代碼執(zhí)行完畢。執(zhí)行到這段代碼結(jié)尾處,鎖就釋放了。

同步塊的缺點:

@synchronized(self) 有些時候會降低代碼效率。

NSLock

另一種方式時NSLock
_lock = [[NSLock alloc] init];
`- (void)synchronizedMethod {
[_lock lock];
// safe
[_lock unlock];
}

遞歸鎖

線程能夠多次持有該鎖,而不會出現(xiàn)死鎖

這些鎖的缺點

這些鎖方法都很好,不過也有缺陷。比方說:在極端情況下同步塊會導致死鎖,另外效率也不見得很高。
替代方案就是GCD

- (NSString *)someString{
  @synchronized(self) {
      return _someString;
  }
}

- (void)setSomeString:(NSString *)someString {
   @synchironized(self){
      _someString = someString;
   }
}

可以修改為:

// 串行隊列
_syncQueue = dispatch_queue_create("com.baidu",NULL);
- (NSString *)someString {
   __block NSString *localSomeString;
   dispatch_sync(_syncQueue,^{
       _localSomeString = someString;
   });
   return _localSomeString;
}

- (void)setSomeString:(NSString *)someString {
   dispatch_asyc(_syncQueue, ^{
       _someString = someString;
   });
}

set方法使用一步隊列可以提示方法的執(zhí)行速度,而讀取操作和寫入操作依然會按順序執(zhí)行。
但是測試性能會發(fā)現(xiàn),set的異步方法比同步方法要慢,因為執(zhí)行異步派發(fā)時,需要拷貝塊。若拷貝塊所用的時間明顯超過執(zhí)行塊所用的時間,則這種做法將會變慢。然而若是派發(fā)給隊列的塊執(zhí)行的任務更為繁重那么這種方案時可以加快運行速度的。
這個主線:多個獲取方法可以并發(fā),而獲取和設(shè)置方法不能并發(fā)。還有以下的代碼可再次提高速度:

// 并發(fā)隊列
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
- (NSString *)someString {
   __block NSString *localSomeString;
   dispatch_sync(_syncQueue,^{
        localSomeString = _someString;
   })
   return localSomeString;
}

- (void)setSomeStirng:(NSString *)someString{
   dispatch_async(_syncQueue,^{
       _someSting = someStirng;
   })
}

最優(yōu)方式:
使用柵欄。在隊列中柵欄塊必須單獨執(zhí)行,不能與其他塊并行。這只對并發(fā)隊列有意義,因為串行隊列中的塊總是按照順序逐個來執(zhí)行。并發(fā)隊列如果發(fā)現(xiàn)接下來要處理的塊時柵欄塊(barrier block)那么就一直要等當前所有并發(fā)塊都執(zhí)行完才會單獨執(zhí)行柵欄塊。

_syncQueue = dispatch_get_globa_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
- (NSString *)someString {
    __block NSString *localSomeString;
    dispathc_sync(_syncQueue,^{
        localString = _someStirng;
    })
    return _someString;
}

- (void)setSomeStirng:(NSString *)someStirng {
    dispatch_barrier_async(_syncQueue,^{
         _someStirng = someString
    })
}

兩個點:

  1. 無論是串行隊列還是并行隊列,讀取一定是同步的 dispatch_sync
  2. 柵欄操作用在set方法里
image.png

讀取操作使用普通塊實現(xiàn)的,而寫入操作則是用柵欄塊實現(xiàn)的。讀取操作可以并行,但是寫入操作必須單獨執(zhí)行,因為他是柵欄塊。

要點

  • 派發(fā)隊列可用來表述同步語義(synchronization semantic),這種做法要比使用@synchronized塊或NSLock對象更簡單。
  • 將同步與異步派發(fā)結(jié)合起來,可以實現(xiàn)與普通枷鎖機制一樣的同步方法,而這么做卻不會阻塞執(zhí)行異步派發(fā)的線程。
  • 使用同步隊列及柵欄塊,可以令同步行為更加高效

第 42 條 多用GCD 少用performSelector

NSObject 定義了幾個方法,令開發(fā)者可以隨意調(diào)用任何方法。這幾個方法可以推遲執(zhí)行方法調(diào)用,也可以指定運行方法所用的線程。這些功能本來都很有用,但是在出現(xiàn)了大中樞派發(fā)及塊這樣的新技術(shù)之后,就顯得不那么必要了。雖說有些代碼還是會經(jīng)常用到他們,但還是避開為妙。
下面就是這些代碼

- (id)performSelector:(SEL)selector;
該方法與選擇子等效。所以下面兩行代碼的執(zhí)行效果相同。
[object performSelector:@selector(selectorName)];
[object selectName];

因為選擇子是在運行期決定的,這就能體現(xiàn)出此方式的強大之處了

SEL selector;
if (/* some condition */) {
    selector = @selector(foo);
}else if (/* some other condition */) {
    selector = @selector(bar);
}else {
    selector = @selector(baz);
}
[object performSelector:selector];

不過這種方式會存在警告:

warning: performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leask]

因為編譯器并不知道要調(diào)用的選擇子是什么,因此也就不了解其方法簽名和返回值,甚至都不知道是否有返回值。而且,由于編譯器不知道方法名,所以就沒法運用arc的內(nèi)存管理規(guī)則來判定返回值是不是因該釋放。鑒于此,arc采用了比較謹慎的做法,就是不添加釋放操作。然而這么做可能導致內(nèi)存泄漏,因為方法在返回對象時,可能已經(jīng)將其保留了。
舉個例子

SEL selector;
if (/* some condition */){
   selector = @selector(newObject);
}else if (/* some other condition */){
   selector = @selector(copy);
}else {
   selector = @selector(someProperty);
}
id ret = [object performSelector:selector]

如果選用的是兩個選擇子之一,那么ret對象應由這段代碼來釋放,而如果是第三個選擇子,則無須釋放。不僅在arc環(huán)境下應該如此,而且在非arc環(huán)境下也應該這么做,這樣才算嚴格遵守了方法的命名規(guī)范。如果不使用arc(此時編譯器也就不發(fā)警告信息了)。那么在前兩種情況下需要手動釋放ret對象,而在后一種情況下不需要釋放。這個問題很容易忽視,而且就算用靜態(tài)分析器,也很難偵測到隨后的內(nèi)存泄漏。performSelector系列的方法之所以要謹慎使用,這就是其中一個原因。

要點

  • performSelector系列方法在內(nèi)存管理方面容易有疏忽。他無法確定將要執(zhí)行的選擇子具體是什么,因而arc編譯器也就無法插入適當?shù)膬?nèi)存管理方法。
  • performSelector系列方法所能處理的選擇子太過局限了,選擇子的返回值類型及發(fā)送給方法的參數(shù)個數(shù)都受限制
  • 如果想把任務放在另一個線程中執(zhí)行,那么最好不要用performSelector系列方法,而應該把任務封裝到塊里,然后調(diào)用大中樞派發(fā)機制的相關(guān)方法來實現(xiàn)。

第 43 條 掌握 GCD及操作隊列的使用時機

在執(zhí)行后臺任務時,GCD并不一定是最佳方式。還有一種技術(shù)叫做NSOperationQueue,他雖然與GCD不同,但是卻與之相關(guān),開發(fā)者可以把操作以NSOperation子類的形式放在隊列中。而這些操作也能夠并發(fā)執(zhí)行。其與GCD派發(fā)隊列有相似之處,這并非巧合。“操作隊列”(operation queue)在GCD之前就有了,其中某些設(shè)計原理因操作隊列而流行,GCD就是基于這些原理構(gòu)建的。實際上從iOS4與MacOS X10.6開始,操作隊列在底層使用GCD來實現(xiàn)的。
在兩者的諸多差別中,首先要注意:GCD時純C的API,而操作隊列則是Objective-C的對象。在GCD中,任務用塊來表示。而塊時輕量級數(shù)據(jù)結(jié)構(gòu)。與之相反,“操作”(operation)則是個更為重量級的Objective-C對象。雖說如此,但GCD并不總是最佳方案。有時候采用對象所帶來的開銷微乎其微,使用完整對象的好處反而大大超過起缺點。

使用NSOperation及NSOperationQueue的好處如下:

  • 取消某個操作??梢栽贜SOperation對象上調(diào)用cancel方法,不過已經(jīng)啟動的任務無法取消。GCD隊列是無法取消的。GCD時“安排好任務之后就不管了”
  • 指定操作間的依賴關(guān)系。
  • 通過鍵值觀察機制監(jiān)控NSOperation對象的屬性。比如可以通過isFinish 或者isCannelled來判斷狀態(tài)
  • 指定操作的優(yōu)先級。操作的優(yōu)先級表示此操作與隊列中其他操作之間的優(yōu)先關(guān)系。優(yōu)先級高的操作先執(zhí)行,優(yōu)先級低的后執(zhí)行。操作隊列的調(diào)度算法(scheduling algorithm)雖“不透明”(opaque),但必然是經(jīng)過一番深思熟慮才寫成的。反之,GCD則沒有直接實現(xiàn)此功能的辦法。GCD的隊列確實有優(yōu)先級,不過那是針對整個隊列來說的,而不是針對每個塊來說的。而令開發(fā)者在GCD之上自己來編寫調(diào)度算法又不太合適。因此,在優(yōu)先級這一點上,操作隊列所提供的功能要比GCD更為遍便利。NSOperation也有“線程優(yōu)先級”(thread priority)。這決定了運行此操作的線程處在何種優(yōu)先級上。用GCD也可以實現(xiàn)此操作,然而使用操作隊列,只需設(shè)置一個屬性。
  • 重用NSOperation對象,系統(tǒng)內(nèi)置了一些NSOperation的子類,(比如 NSBlockOperation)共開發(fā)者使用,要不是這些固有子類的話,那就得自己創(chuàng)建了。這些類就是普通的Objective-C對象,能夠存放任何信息。

應該盡可能使用高層的API,只在確有必要時才求助于底層

要點

  • 在解決多線程與任務管理問題時,派發(fā)隊列并非唯一方案
  • 操作隊列提供了一套高層的Objective-CAPI,能實現(xiàn)純GCD所具備的絕大部分功能,而且還能完成一些更為復雜的操作(操作的優(yōu)先級),那些操作若改用GCD來實現(xiàn),則需要另編代碼。

第 44 條 通過Dispatch Group機制,根據(jù)系統(tǒng)資源狀況來執(zhí)行任務

dispatch_group_t的好處:

  • dispatch group 是GCD的一項特權(quán),能夠把任務分組。調(diào)用者可以等待這組任務執(zhí)行完畢,也可以在提供回調(diào)函數(shù)之后繼續(xù)往下執(zhí)行,這素任務完成后,調(diào)用者會得到通知。這個功能有許多作用,其中最重要最值得注意的用法就是把將要并發(fā)執(zhí)行的多個任務合為一個,于是調(diào)用者就可以知道這些任務何時才能執(zhí)行完畢。
  • dispatch_group_enter(dispatch_group_t group)、dispatch_group_leave(dispatch_group_t group)前者能夠使分組里正要執(zhí)行的任務遞增,而后者則使之遞減。成對存在,與引用計數(shù)類似
  • dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout) 設(shè)置等待時間
  • dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block) 與wait函數(shù)平級。與wait函數(shù)不同的是,開發(fā)者向此函數(shù)傳入塊,等dispatch_group執(zhí)行完畢之后,塊會在待定的線程上執(zhí)行。假如當前線程不應阻塞,而開發(fā)者又想在那些任務全部完成時得到通知,那么此做法就很有必要了。如果想令數(shù)組中的每個對象都執(zhí)行某項任務,并且想等待所有任務執(zhí)行完畢,那么就可以使用這個GCD特性來實現(xiàn),代碼如下:
    dispatch_group_t queue  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
    dispatch_group_t dispatchGroup = dispatch_group_create();
    for (id object in collection){
         dispatch_group_async(dispatchGroup,queue,^{
               [object performTask];
         });
    }
    dispatch_group_wait(dispatchGroup,DISPATCH_TIME_FPREVER);
    若當前線程不應阻塞,則可用notifu函數(shù)來取代wait,如下:
    dispatch_queue_t notifyQueue = dispatch_get_main_queue();
    dispatch_group_notify(dispatchGroup,notifyQueue,^{
     // 回到主線程繼續(xù)執(zhí)行任務。notify回調(diào)時所選用的隊列根據(jù)具體情況來定。筆者在范例中使用了主隊列,這是中常見寫法。也可以用自定義的串行都列或全局并發(fā)隊列。
    });
    
    在本例中所有的任務都派發(fā)到同一個隊列之中,但實際上未必一定要這樣做。也可以把某些任務放在優(yōu)先級高的線程上執(zhí)行,同時仍然把所有任務都歸入同一個dispatch group 并在執(zhí)行完畢時獲得通知,代碼如下:
dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_PRIORITY_LOW, 0);
dispatch_queue_global_queue heighP
dispatch_queue_t heighPriorityQueue = dispatch_get_global_queue(DISPATCH_PRIORITY_HEIGH, 0);
dispatch_group_t dispatchGroup = dispatch_group_create();
for (id object in lowPriorityObjects) {
     dispatch_async(dispatchGroup,lowPriorityQueue,^{
         [object performTask];
     });
}

for (id object in heighPriorityObjects) {
     dispatch_async(dispatchGroup,heighPriorityQueue,^{
         [object performTask];
     });
}

dispatch_queue_t notifyQueue = dispatch_get_main();
dispatch_gropu_notify(dispatchGroup, notifyQueue,^{
    // dispatccGruop任務執(zhí)行完畢,通知主線程開始處理
})

除了像上面這樣把任務提交到并發(fā)隊列之外,也可以把任務提交至各個串行隊列中,并用dispatch group跟蹤執(zhí)行狀況。然而,如果所有任務都排在同一個隊列里面,那么dispatch grope就用處不大了。因此此時執(zhí)行任務總要逐個執(zhí)行,所以只需要在提交完全部任務之后再提交一個塊即可,這樣做與通過notify函數(shù)等待dispatch group執(zhí)行完畢然后再回調(diào)塊是等效的。代碼如下:

dispatch_queue_t queue = dispatch_queue_create("com.baidu",0);
for (id object in Objects) {
    dispatch_async(queue, ^{
        [object performTask];
    })
}
dispatch_async(queue, ^{
    // continue processing after completing tasks
})

上面這點代碼表明,開發(fā)者未必總需要使用dispatch group。有時候采用帶個隊列搭配標準的異步派發(fā),也可實現(xiàn)同樣效果。

根據(jù)希同資源狀況來執(zhí)行任務(并發(fā)的理解)

為了執(zhí)行隊列中的塊,GCD在適當?shù)臅r機自動創(chuàng)建新線程或復用舊線程。如果使用并發(fā)隊列,那么其中有可能會有多個線程,這也就意味著多個塊可以并發(fā)執(zhí)行。在并發(fā)隊列中,執(zhí)行任務所用的并發(fā)線程數(shù)量,取決于各種因素,而GCD主要根據(jù)系統(tǒng)資源狀況來判定這些因素的。假如CPU有多個核心,并且隊列中有大量任務等待執(zhí)行,那么GCD就可能會給該隊列配備多個線程。通過dispatch group所提供的這種簡便方式,既可以并發(fā)執(zhí)行一系列給定的任務,又能在全部任務結(jié)束時得到通知。由于GCD有并發(fā)隊列機制,所以能夠根據(jù)可用的系統(tǒng)資源狀況來并發(fā)執(zhí)行任務。而開發(fā)者則可以專注于業(yè)務邏輯代碼,無須再為了處理并發(fā)任務而編寫復雜的調(diào)度器。
理解:并發(fā)可以使用多個線程

dispatch 可以替換for循環(huán)的API

在前面的范例代碼中,我們遍歷某個collection,并在其每個元素上執(zhí)行任務,而這也可以用另一個GCD函數(shù)來實現(xiàn).

dispatch_apply(size_t interations, dispatch_queue_t queue, void(^block)(size_t));

dispatch_apply可以使用并發(fā)隊列也可以使用串行隊列
此函數(shù)會將塊反復執(zhí)行一定的次數(shù),每次傳給塊的參數(shù)值都會遞增,從0開始,直至“interations-1”。其用法如下:

dispatch_queue_t queue = dispatch_queue_create("com.baidu",0);
dispatch_apply(queue,^{
    // perform task
});

采用for循環(huán),從0到9遞增:

for(i = 0; i < 10; i++) {
   // perform task
}
這個執(zhí)行方式
dispatch_queue_t queue = dispatch_get_global_queue(DOISPATCH_QUQUQ_PRIORITY_DEFAULT,0);
dispatch_apply(array.count, queue, ^(size_t){
     id object = array[size_t];
     [object performTask];
});

這個例子再次表明,未必總要使用dispatch group 。然而,dispatch_apply會持續(xù)阻塞,直到所有任務都執(zhí)行完畢為止。由此可見:假如把塊派給了當前隊列(或者體系中高于當前隊列的某個串行隊列),就會導致死鎖。若想在后臺執(zhí)行任務,則應使用dispatch group

要點

  • 一系列任務可歸入一個dispatch group中。開發(fā)者可以在這組任務執(zhí)行完畢時獲得通知。
  • 通過dispatch group,可以在并發(fā)式派發(fā)隊列里同時執(zhí)行多項任務。此時GCD會根據(jù)系統(tǒng)資源狀況來調(diào)度這些并發(fā)執(zhí)行的任務。開發(fā)者若自己來實現(xiàn)此功能,則需編寫大量代碼。

第 45 條 使用dispatch_once 來執(zhí)行只需運行一次的線程安全代碼

單例模式

dispatch_once(dispatch_once_t *token, dispatch_block_t block);

此函數(shù)接收類型為dispatch_once_t的特殊參數(shù),筆者稱其為“標記”,此外還接受塊參數(shù)。對于給定的標記來說,該函數(shù)保證相關(guān)的塊必定會執(zhí)行,且僅執(zhí)行一次。首次執(zhí)行函數(shù)時,必定會執(zhí)行塊中的代碼,最重要的一點在于,此操作完全是線程安全的。請注意,對于只需執(zhí)行一次的塊來說,每次調(diào)用函數(shù)時傳入的標記都必須完全相同。因此,開發(fā)者通常將標記變量聲明在static或global作用域里。
剛才實現(xiàn)單例模式所用的shareInstance方法,可以用此函數(shù)來改寫:

@property (strong, nonamatic) NSString *string;
+ (id)shareInstance {
   static EOCClass *shareInstance = nil;
   static dispatch_once oncetoken;
   dispatch_once(&onceToken, ^{
       shareInstance = [[EOCClass alloc] init];
       // 如果有屬性的話
       shareInstance.string = [[NSString alloc] init]
   });
   return shareInstance;
}

由于每次調(diào)用時都必須使用完全相同的標記,所以標記要聲明稱static。把該變量定義在static作用域中,可以保證編譯器在每次執(zhí)行shareInstance方法是都會復用這個變量,而不會創(chuàng)建新變量。
dispatch_once 跟高效。他沒有使用重量級的同步機制。此函數(shù)采用“原子訪問”來查詢標記,以判斷其所對應的代碼原來是否已經(jīng)執(zhí)行過。

要點

  • 經(jīng)常編寫“只需執(zhí)行一次的線程安全代碼”(thread-safe single-code execution)。通過GCD所提供的dispatch_once函數(shù),很容易就能實現(xiàn)此功能。
  • 標記應該聲明在static或global作用域中,這樣的話,在把只需執(zhí)行一次的塊傳給dispatch_once函數(shù)時,穿進去的標記也是相同的。

第 46 條 不要使用dispatch_get_current_queue

該函數(shù)已被棄用

該函數(shù)有這種典型的錯誤用法(antipattern,"反模式"),就是用它檢測當前隊列是不是某個特定的隊列,試圖以此來避免執(zhí)行同步派發(fā)時可能遭遇的死鎖問題??紤]下面兩種存取方法,其代碼用隊列來保證對實例變量的訪問操作是同步的:

dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
         dispatch_block_t block = ^{};
         if (dispatch_get_current_queue() == queueA) {
             block()
         }else {
             // 依然死鎖
             dispatch_sync(queueA,^{
             })
         }
    })
})
image.png

由于隊列間有層級關(guān)系,所以“檢查當前隊列是否為執(zhí)行同步派發(fā)所用的隊列”這種辦法,并不總是奏效。
要解決這個問題,最好的辦法就是通過GCD所提供的功能來設(shè)定“隊列特有數(shù)據(jù)”(queue-specific data),此功能可以把任意數(shù)據(jù)以鍵值對的形式關(guān)聯(lián)到隊列里。最重要之處在于,假如根據(jù)指定的鍵獲取不到關(guān)聯(lián)數(shù)據(jù),那么系統(tǒng)就會沿著層級體系向上查找,直至找到數(shù)據(jù)或到達跟隊列為止。下面的例子:

dispatch_queue_t queueA = dispatch_queue_create("com.baidu",0);
dispatch_queue_t queueB = dispatch_queue_create("com.baidu",0);
// 將queueA設(shè)置為queueB的目標隊列
dispatch_set_target_queue(queueB, queueA);

static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA,&queueSpecificValue,(void *)queueSpecificValue, (dispatch_function_t)CFRelease);

dispatch_sync(queueB, ^{
     dispatch_block_t block = ^{NSlog("No deadlock!")};
     CFString retrievedValue = dispatch_get_specific(&kQueueSpecific);
     if (retrievedValue) {
         block();
     }else {
         dispatch_sync(queueA, block);
     }
})

要點

  • dispatch_get_current_queue 函數(shù)的行為常常與開發(fā)者所預期的不同。此函數(shù)已經(jīng)廢棄,只應做調(diào)試只用。
  • 由于派發(fā)隊列是按層級來組織的,所以無法單用某個隊列對象來描述“當前隊列”這一概念。
  • dispatch_get_current_queue函數(shù)用于解決由不可重入的代碼所引發(fā)的死鎖,然而能用此函數(shù)解決的問題,通常也能改用“隊列特定數(shù)據(jù)”來解決。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容