第 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)所需的各種信息。

- 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;
這樣寫的好處:
- 修改之后,凡是使用了這個類型定義的地方,比如方法簽名等處,都會無法編譯,而且報的是同一種錯誤,于是開發(fā)者可以逐個修復
- 別名可以見名知意
- 用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
})
}
兩個點:
- 無論是串行隊列還是并行隊列,讀取一定是同步的 dispatch_sync
- 柵欄操作用在set方法里

讀取操作使用普通塊實現(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),代碼如下:
在本例中所有的任務都派發(fā)到同一個隊列之中,但實際上未必一定要這樣做。也可以把某些任務放在優(yōu)先級高的線程上執(zhí)行,同時仍然把所有任務都歸入同一個dispatch group 并在執(zhí)行完畢時獲得通知,代碼如下: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ā)隊列。 });
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,^{
})
}
})
})

由于隊列間有層級關(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ù)”來解決。