37. 理解block這一概念
塊與函數(shù)類似,只不過是直接定義在另一個函數(shù)里,和定義他的那個函數(shù)共享一個范圍內(nèi)的東西。塊用“^”符號來表示,后面根這一對花括號,括號里面是塊的實現(xiàn)代碼。
^{
//Block implementation here
}
塊其實就是個值,而且有其相關(guān)類型。與int、float或Objective-C對象一樣,也可以把塊賦值給變量,然后像使用其他變量那樣使用它。塊類型的語法與函數(shù)指針近似。
void (^someBlock)() = ^{
//Block implementation here
};
塊類型的語法結(jié)構(gòu)如下:
return_type (^block_name)(parameters)
塊的強(qiáng)大之處是:在聲明它的范圍里,所有變量都可以為其所捕獲。這也就是說,那個范圍里的全部變量,在塊里依然可用。
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b){
return a + b + additional;
};
int add = addBlock(2, 5);
默認(rèn)情況下,為塊所捕獲的變量,是不可以在塊里修改的。在本例中,假如塊內(nèi)的代碼改動了additional變量的值,那么編譯器就會報錯。不過,聲明變量的時候可以加上__block修飾符,這樣就可以在塊內(nèi)修改了。
塊總能修改實例變量,所以在聲明時無須加__block。不過,如果通過讀取或?qū)懭氩僮鞑东@了實例變量,那么也會自動把self變量一并捕獲了,因為實例變量是與self所指代的實例關(guān)聯(lián)在一起的。(容易引起retain cycle哈)
注意即使block里面用下劃線的方式訪問實例變量,也是持有了self哈
如果塊所捕獲的變量是對象類型,那么就會自動保留它。系統(tǒng)在釋放這個塊的時候,也會將其一并釋放。這就引出了一個與塊有關(guān)的重要問題。塊本身可視為對象。實際上,在其他Objective-C對象所能相應(yīng)的選擇子中,有很多是塊也可以響應(yīng)的。而最重要之處則在于,塊本身也和其他對象一樣,有引用計數(shù)。當(dāng)最后一個指向塊的引用移走之后,塊就回收了。回收時也會釋放塊所捕獲的變量,以便平衡捕獲時所執(zhí)行的保留操作。
※ block的內(nèi)部結(jié)構(gòu)

在內(nèi)存布局中,最重要的就是invoke變量,這是個函數(shù)指針,指向塊的實現(xiàn)代碼。函數(shù)原型至少要接受一個void*型的參數(shù),此參數(shù)代表塊。block其實就是一種代替函數(shù)指針的語法結(jié)構(gòu),原來使用函數(shù)指針時,需要用“不透明的void指針”來傳遞狀態(tài)。而改用塊之后,則可以把原來用標(biāo)準(zhǔn)C語言特性所編寫的代碼封裝成簡明且易用的接口。
descriptor變量是指向結(jié)構(gòu)體的指針,每個塊里都包含此結(jié)構(gòu)體。塊還把會它所捕獲的所有變量都拷貝一份。這些拷貝放在descriptor變量后面,捕獲了多少個變量,就要占據(jù)多少內(nèi)存空間。請注意,拷貝的并不是對象本身,而是指向這些對象的指針變量。
對基本類型的變量,捕獲意味著程序會拷貝變量的值,并用Block對象內(nèi)的局部變量保存。對指針類型的變量,Block對象會使用強(qiáng)引用。這意味著凡是Block對象用到的對象,都會被保留。所以在相應(yīng)的Block對象被釋放前,這些對象一定不會被釋放(這也是Block對象和函數(shù)之間的差別,函數(shù)無法做到這點)。
NSMutableString *str = [@"ssss" mutableCopy];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"str: %@", str);
});
str = [@"huihui" mutableCopy];
輸出:
str: ssss
如果換成實例變量:
str = [@"ssss" mutableCopy];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"str: %@", self->str);
});
str = [@"huihui" mutableCopy];
輸出:
str: huihui
鑒于實例變量在block里面可以修改,而且改了以后block里面可以感知更新,看起來好像對于實例變量block并沒有復(fù)制到自己的內(nèi)存里面。
※ 堆or棧?以及全局block
這部分和copy有點關(guān)聯(lián),我之前也寫過:
總結(jié)一下大概就是MRC時代block是在棧里面的,函數(shù)執(zhí)行完就會被釋放掉,即使用了strong也沒有拷貝到堆區(qū),只是增加了指向,使用時可能會有野指針crash。
ARC下在生成的block也是棧塊,只是當(dāng)賦值給strong對象時,系統(tǒng)會主動對其進(jìn)行copy,從棧區(qū)自動拷貝到堆區(qū),所以其實只有兩個區(qū),全局區(qū)和堆區(qū),不會出現(xiàn)野指針的問題,故而ARC用strong/copy沒有太大區(qū)別。
---MRC分割線---
定義塊的時候,其所占的內(nèi)存區(qū)域是分配在棧中的。這就是說,塊只在定義他的那個范圍內(nèi)有效。例如,下面這段代碼就有危險:(這里其實我覺得好像木有問題誒,畢竟block聲明的作用域沒有過,是不會出棧的叭)
void (^block)();
if (/* some condition */) {
block = ^{
NSLog(@"Block A");
};
}else{
block = ^{
NSLog(@"Block B");
};
}
block();
因為block的定義在stack里面的時候,定義的有效期只在if{}或者else{}里面,退出大括號的時候會做出棧的操作。于是,只能保證在對應(yīng)的if或else語句范圍內(nèi)block定義有效。這樣寫出來的代碼可以編譯,但是運行起來時而正確,時而錯誤。若編譯器未覆寫待執(zhí)行的塊,則程序照常運行,若覆寫,則程序崩潰。
為解決此問題,可給塊對象發(fā)行copy消息以拷貝之。這樣的話,就可以把塊從棧復(fù)制到堆了??截惡蟮膲K,可以在定義它的范圍之外使用。而且,一旦復(fù)制到堆上,塊就成了帶引用計數(shù)的對象了。后續(xù)的復(fù)制操作都不會真的執(zhí)行復(fù)制,只是遞增對象的引用計數(shù)。
void (^block)();
if (/* some condition */) {
block = [^{
NSLog(@"Block A");
} copy];
}else{
block = [^{
NSLog(@"Block B");
} copy];
}
block();
---全局block分割線---
除了“棧塊”和“堆塊”之外,還有一類塊叫做“全局塊”(global block)。這種塊不會捕捉任何狀態(tài)(比如外圍的變量等),運行時也無須有狀態(tài)來參與。塊所使用的整個內(nèi)存區(qū)域,在編譯期已經(jīng)完全確定了,因此,全局塊可以聲明在全局內(nèi)存里,而不需要在每次用到的時候于棧中創(chuàng)建。另外,全局塊的拷貝操作是個空操作,因為全局塊絕不可能為系統(tǒng)所回收。這種塊實際上相當(dāng)于單例。
void (^block)() = ^{
NSLog(@"This is a block");
};
由于運行該塊所需的全部信息都能在編譯期確定,所以可把它做成全局塊。這幾種block的區(qū)分可以參考:http://www.itdecent.cn/p/0900fa7029a7
- 如果一個block中引用了全局變量,或者沒有引用任何外部變量(屬性、實例變量、局部變量),那么該block為全局塊。
- 其它引用情況(局部變量,實例變量,屬性)為棧塊。
38. 為常用的block類型創(chuàng)建typedef
每個塊都具備其“固有類型”,因而可將其賦給適當(dāng)類型的變量。這個類型由塊所接受的參數(shù)及其返回值組成。
int (^variableName) (BOOL flag, int value) = ^(BOOL flag, int value) {
return value + 1;
};
塊類型語法:
return_type (^block_name) (parameters)
為隱藏復(fù)雜的塊類型,用C語言中“類型定義”的特性,typedef關(guān)鍵字給類型起個易讀的別名。
typedef int (^EOCSomeBlock) (BOOL flag, int value);
上面是向系統(tǒng)中新增一個名為EOCSomeBlock的類型。
// 使用新類型
EOCSomeBlock block = ^(BOOL flag, int value) {
return value + 1;
};
使用塊的API:
- (void)startWithCompletionHandler:(void (^)(NSData *data, NSError *error))completion;
使用typedef修改后:
typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;
這樣的話將來想加減傳入的參數(shù)都很方便,不用把所有代碼中用到塊的地方都改掉,只要該typedef就好啦。
如果block的簽名相同,用途不同,不妨為同一個塊簽名定義多個類型別名。如果要重構(gòu)的代碼使用了塊類型的某個別名,那么只需修改相應(yīng)typedef中的塊簽名即可,無須改動其他typedef,例如:
typedef void (^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);
typedef void (^ACAccountStoreRequestAccessCompletionHandler)(BOOL granted, NSError *error);
39. 用handler塊降低代碼分散程度
在創(chuàng)建對象時,可以使用內(nèi)聯(lián)的handler塊將相關(guān)業(yè)務(wù)邏輯一并聲明。
在有多個實例需要監(jiān)控時,如果采用委托模式,那么經(jīng)常需要根據(jù)傳入的對象來切換,而若改用handler塊來實現(xiàn),則可直接將塊與相關(guān)對象放在一起。
如果有success也有failure情況的時候,最好用一個handler處理。
推薦:
NSURL *url = [[NSURL alloc] initWithString:@"http:www.baidu.com"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
_fetchedFooData = data;
}];
================
不推薦:
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void (^EOCNetworkFetcherErrorHandler)(NSError *error);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion failureHandler:(EOCNetworkFetcherErrorHandler)failure;
@end
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data, NSError *error) {
if (error) {
//Handler failure
} else {
//Handler success
}
}];
主要是放到一個里面更加靈活,交給調(diào)用者更多空間,他可以自己拿到數(shù)據(jù)判斷要怎么處理,可能他認(rèn)為的success和API提供者認(rèn)為的是不一樣的。
- 設(shè)計API時如果用到了handler塊,那么可以增加一個參數(shù),使調(diào)用者可通過此參數(shù)來決定應(yīng)該把塊安排在哪個隊列上執(zhí)行。
如果
某些代碼必須運行在特定的線程上。因此,最好能由調(diào)用API的人來決定handler應(yīng)該運行在那個線程上。NSNotificationCenter就屬于這種API,它提供了一個方法,調(diào)用者可以經(jīng)由此方法來注冊想要接收的通知,等到相關(guān)事件發(fā)生時,通知中心就會執(zhí)行注冊好的那個塊。調(diào)用者可以指定某個塊應(yīng)該安排在哪個執(zhí)行隊列里,然而這不是必需的。若沒有指定隊列,按默認(rèn)方式執(zhí)行。
- (id <NSObject>)addObserverForName:(nullable NSString *)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
40. 用block引用其所屬對象時不要出現(xiàn)retain cycle
- 如果塊所捕獲的對象直接或間接地保留了塊本身,那么就得當(dāng)心保留環(huán)問題。
- 一定要找個適當(dāng)?shù)臅r機(jī)解除保留環(huán),而不能把責(zé)任推給API的調(diào)用者。
41. 多用派發(fā)隊列,少用同步鎖
※ synchronized
在Objective-C中,如果有多個線程要執(zhí)行同一份代碼,那么有時可能會出問題。這種情況下,通常要使用鎖來實現(xiàn)某種同步機(jī)制。在GCD出現(xiàn)之前,有兩種辦法,第一種是采用內(nèi)置的“同步塊”(synchronization block):
- (void)synchronizedMethod
{
@synchronized (self) {
//Safe
}
}
這種寫法會根據(jù)給定的對象,自動創(chuàng)建一個鎖,并等待塊中的代碼執(zhí)行完畢。執(zhí)行到這段代碼結(jié)尾處,鎖就釋放了。在本例中,同步行為所針對的對象是self。這么寫通常沒錯,因為它可以保證每個對象實例都能不受干擾地運行其synchronizedMethod方法。然而,濫用@synchronized (self)則會降低代碼效率,因為共用同一個鎖的那些同步塊,都必須按順序執(zhí)行。
※ NSLock、NSRecursiveLock
另一個辦法是直接使用NSLock對象:
_lock = [[NSLock alloc]init];
- (void)synchronizedMethod
{
[_lock lock];
//Safe
[_lock unlock];
}
但是NSLock容易產(chǎn)生死鎖,例如下面這樣,第二次lock因為第一個還沒有釋放,永遠(yuǎn)拿不到鎖,于是NSLog也執(zhí)行不到:
NSLock *lock = [[NSLock alloc] init];
[lock lock];
[lock lock];
NSLog(@"發(fā)生了死鎖");
[lock unlock];
[lock unlock];
可以使用NSRecursiveLock這種“遞歸鎖”(recursize lock),線程能夠多次持有該鎖,而不會出現(xiàn)死鎖(deadlock)現(xiàn)象,可參考:http://www.itdecent.cn/p/777c28eface5
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
[lock lock];
[lock lock];
NSLog(@"沒有死鎖");
[lock unlock];
[lock unlock];
它可以允許同一線程多次加鎖,而不會造成死鎖。遞歸鎖會跟蹤它被lock的次數(shù)。每次成功的lock都必須平衡調(diào)用unlock操作。只有所有達(dá)到這種平衡,鎖最后才能被釋放,以供其它線程使用。
這兩種方法都很好,不過也有其缺陷。比方說,在極端情況下,同步塊會導(dǎo)致死鎖,另外,其效率也不見得很高,而如果直接使用鎖對象的話,一旦遇到死鎖,就會非常麻煩。
- (NSString *)something{
@synchronized (self) {
return _something;
}
}
- (void)setSomething:(NSString *)something{
@synchronized (self) {
_something = something;
}
}
剛才說過,濫用@synchronized (self)會很危險,因為所有同步塊都會彼此搶奪同一個鎖。要是有很多個屬性都這么寫的話,那么每個屬性的同步塊都要等其他所有同步塊執(zhí)行完畢才能執(zhí)行。這也許并不是開發(fā)者想要的效果。我們只是想令每個屬性各自獨立地同步。
※ GCD改寫
(1)串行隊列+同步等待
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);
- (NSString *)someString
{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = self.someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString
{
dispatch_sync(_syncQueue, ^{
self.someString = someString;
});
}
(2)串行隊列+異步設(shè)置
設(shè)置方法并不一定非得是同步的。設(shè)置實例變量所有的塊,并不需要向設(shè)置方法返回什么值。
- (void)setSomeString:(NSString *)someString
{
dispatch_async(_syncQueue, ^{
self.someString = someString;
});
}
這次只是把同步派發(fā)改成了異步派發(fā),從調(diào)用者的角度來看,這個小改動可以提升設(shè)置方法的執(zhí)行速度,而讀取操作與寫入操作依然會按順序執(zhí)行。但這么該有個壞處:可能會發(fā)現(xiàn)這種寫法比原來慢,因為執(zhí)行異步派發(fā)時,需要拷貝塊。弱拷貝塊所用的時間明顯超過執(zhí)行塊所花的時間,則這種做法將比原來更慢。然而,若是派發(fā)給隊列的塊要執(zhí)行更為繁重的任務(wù),那么仍然可以考慮這種備選方法。
(3)并行隊列+同步等待+柵欄任務(wù)
多個獲取方法可以并發(fā)執(zhí)行,而獲取方法與設(shè)置方法之間不能并發(fā)執(zhí)行,利用這個特點,還能寫出更快一些的代碼來。
_syncQueue = dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, NULL);
- (NSString *)someString
{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = self.someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString
{
dispatch_barrier_async(_syncQueue, ^{
self.someString = someString;
});
}
測試一下性能,你就會發(fā)現(xiàn),這種做法肯定比使用串行隊列要快。
42. 多用GCD,少用performSelector系列方法
NSObject定義了幾個方法,令開發(fā)者可以隨意調(diào)用任何方法。這幾個方法可以推遲執(zhí)行方法調(diào)用,也可以指定運行方法所用的線程。這些功能原來很有用,但是在出現(xiàn)了大中樞派發(fā)及塊這樣的新技術(shù)之后,就顯得不那么必要了。雖說有些代碼還是會經(jīng)常用到它們,但筆者勸你還是避開為妙。
這其中最簡單的是performSelector:。該方法與直接調(diào)用選擇子等效。所以下面兩行代碼的執(zhí)行效果相同:
[self performSelector:@selector(selectorName)];
[self selectorName];
如果選擇子是在運行期決定的,那么就能體現(xiàn)出此方式的強(qiáng)大之處了。這就等于在動態(tài)綁定之上再次使用動態(tài)綁定,因而可以實現(xiàn)出下面這種功能:
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];
編譯器并不知道將要調(diào)用的選擇子是什么,因此也就不了解其方法簽名及返回值,甚至連是否有返回值都不清楚。而且,由于編譯器不知道方法名,所以就沒辦法運用ARC的內(nèi)存管理規(guī)則來判定返回值是不是應(yīng)該釋放,鑒于此,ARC采用了比較謹(jǐn)慎的做法,就是不添加釋放操作。然而這么做可能導(dǎo)致內(nèi)存泄漏,因為方法在返回對象時 可能已經(jīng)將其保留了。
如果調(diào)用的是兩個選擇子之一,那么ret對象應(yīng)由這段代碼來釋放,如果是第三個選擇子,則無須釋放。這個問題很容易忽視,而且就算用靜態(tài)分析器,也很難偵測到隨后的內(nèi)存泄漏。performSelector系列的方法之所以要謹(jǐn)慎使用,這就是其中一個原因。
而且,performSelector方法的返回值類型畢竟是id。如果想返回整數(shù)或浮點數(shù)等類型的值,那么就需要執(zhí)行一些復(fù)雜的轉(zhuǎn)換操作了,而這種轉(zhuǎn)換很容易出錯。而且同系列方法很多參數(shù)類型是id,所以傳入的參數(shù)必須是對象才行。如果選擇子所接受的參數(shù)是整數(shù)或浮點數(shù),那就不能采用這些方法了,例如:
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
最主要的替代方案就是使用block。而且,performSelector系列方法所提供的線程功能,都可以通過在大中樞派發(fā)機(jī)制中使用塊來實現(xiàn)。延后執(zhí)行可以用dispatch_after來實現(xiàn),在另一個線程上執(zhí)行任務(wù)則可通過dispatch_sync及dispatch_async來實現(xiàn)。
43. 掌握GCD及操作隊列的使用時機(jī)
GCD是純C的API,而NSOperation與NSOperationQueue是基于 GCD 更高一層的封裝,是Objective-C的對象,但其實NSOperation的底層是用GCD來實現(xiàn)的。
GCD技術(shù)的同步機(jī)制非常優(yōu)秀,對于那些只需執(zhí)行一次的代碼來說,使用GCD最方便。但在執(zhí)行后臺任務(wù)時,還可以使用操作隊列(NSOperationQueue)。
操作隊列的優(yōu)勢:
- 運行任務(wù)之前,可以在NSOperation對象上調(diào)用cancel方法,即可取消操作,不過,已經(jīng)啟動的任務(wù)無法取消,而GCD把塊安排到隊列就無法取消。
- 可以指定操作間的依賴關(guān)系,使特定操作必須在另一個操作執(zhí)行完畢后方可執(zhí)行。
- 可以通過KVO(鍵值觀察)來監(jiān)控NSOperation對象的屬性變化(isCancelled,isFinished等)
- 可以指定操作的優(yōu)先級
- 可以通過重用NSOperation對象來實現(xiàn)更豐富的功能
- 可以設(shè)定并發(fā)數(shù)限制(自己的經(jīng)驗哈)
區(qū)別還可參考:https://blog.csdn.net/Setoge/article/details/52134247
44. 通過Dispatch Group機(jī)制,根據(jù)系統(tǒng)資源狀況執(zhí)行任務(wù)
串行隊列用dispatch_async其實就可以監(jiān)測之前的任務(wù)都完成了,不用偏要dispatch_group_notify。
dispatch_apply會循環(huán)執(zhí)行指定次數(shù),但是會阻塞,可能會引發(fā)死鎖。
45. 使用dispatch_once來執(zhí)行只需運行一次的線程安全代碼
標(biāo)準(zhǔn)單例寫法:(參考:http://www.itdecent.cn/p/96fa3c93df19)
#import "MySingle2.h"
@implementation MySingle2
+(instancetype)shareInstance
{
static MySingle2 *_mySingle = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_mySingle = [[super allocWithZone:NULL] init];
});
return _mySingle;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
return [self shareInstance];
}
- (id)copyWithZone:(NSZone *)zone
{
return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return self;
}
當(dāng)我們調(diào)用alloc方法時(為了方式外部調(diào)用alloc init而不調(diào)用sharedInstance),OC內(nèi)部會調(diào)用allocWithZone這個方法來申請內(nèi)存,我們覆寫這個方法,然后在這個方法中調(diào)用shareInstance方法返回單例對象,這樣就可以達(dá)到我們的目的。
由于每次調(diào)用時都必須使用完全相同的標(biāo)記,所以標(biāo)記要聲明成static。把該變量定義在static作用域中,可以保證編譯器在每次執(zhí)行shareInstance方法時都會復(fù)用這個變量,而不會創(chuàng)建新變量。dispatch_once采用“原子訪問”(atomic access)來查詢標(biāo)記,以判斷其所對應(yīng)的代碼原來是否已經(jīng)執(zhí)行過。
注意如果用以下的方式,first和second都會打印滴:
- (void)viewDidLoad {
[super viewDidLoad];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"first");
});
}
- (void)viewWillAppear:(BOOL)animated {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"second");
});
}
46. 不要使用dispatch_get_current_queue
該函數(shù)有種典型的錯誤用法(antipattern, “反模式”),就是用它檢測當(dāng)前隊列是不是某個特定的隊列,試圖以此來避免執(zhí)行同步派發(fā)時可能遭遇的死鎖問題。
但這并不靠譜,例如:
-(void)demo2{
dispatch_queue_t queueA = dispatch_queue_create("com.sky.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.sky.queueB", NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{};
if (dispatch_get_current_queue() == queueA) {
block();
} else {
dispatch_sync(queueA, block);
}
});
});
}
dispatch_get_current_queue獲取到的當(dāng)前隊列是queueB,所以結(jié)果依然執(zhí)行針對queueA的同步派發(fā)操作,依然死鎖。
正確做法是:不要把存取方法做成可重入的,而是應(yīng)該確保操作同步操作所用的隊列絕不會訪問屬性,也就是絕對不會調(diào)用 someString 方法。
此外,隊列之間會形成一套層級體系,這意味著排在某條隊列中的塊,會在其上級隊列(parent queue,也叫“父隊列”)里執(zhí)行。層級里地位最高的那個隊列總是 “全局并發(fā)隊列”(global concurrentqueue)圖描繪了一套簡單的隊列體系。

排在隊列B或隊列C中的塊,稍后會在隊列A里依序執(zhí)行。于是,排在隊列A、B、C 中的塊總是要彼此錯開執(zhí)行。然而,安排在隊列D 中的塊,則有可能與隊列A 里的塊(也包括隊列B 與 隊列C 里的塊)并行,因為A 與 D 的目標(biāo)隊列是個并發(fā)隊列。若有必要,并發(fā)隊列可以用多個線程并行執(zhí)行多個塊,而是否會這樣做,則需要根據(jù) CPU 的核心數(shù)量等系統(tǒng)資源狀況來定。
由于隊列間有層級關(guān)系,所以 “檢查當(dāng)前隊列是否為執(zhí)行同步派發(fā)所用的隊列”這種辦法,并不總是奏效。
※ dispatch_queue_set_specific標(biāo)識當(dāng)前隊列
要解決這個問題,最好的辦法就是通過 GCD 所提供的功能來設(shè)定“隊列特有數(shù)據(jù)”(queue-specific data),此功能可以把任意數(shù)據(jù)以鍵值對的形式關(guān)聯(lián)到隊列里。最重要之處在于,假如根據(jù)指定的鍵獲取不到關(guān)聯(lián)數(shù)據(jù),那么系統(tǒng)就會沿著層級體系向上查找,直至找到數(shù)據(jù)或到達(dá)根隊列為止。筆者這么說,大家也許還不太明白其用法,所以看下面這個例子:
dispatch_queue_t queueA = dispatch_queue_create("com.sky.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.sky.queueB", NULL);
dispatch_set_target_queue(queueB, queueA);
static int specificKey;
CFStringRef specificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA,
&specificKey,
(void*)specificValue,
(dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
//do something
};
CFStringRef retrievedValue = dispatch_get_specific(&specificKey);
if (retrievedValue) {
block();
} else {
dispatch_sync(queueA, block);
}
});
此函數(shù)的首個參數(shù)表示待設(shè)置數(shù)據(jù)隊列,其后面兩個參數(shù)是鍵與值。鍵與值都是不透明的void 指針。對于鍵來說,有個問題一定要注意:函數(shù)是按指針值來比較鍵的,而不是按照其內(nèi)容。所以,“隊列特定數(shù)據(jù)”的行為與 NSDictionary 對象不同,后者是比較鍵的 “對象等同性”。“隊列特定數(shù)據(jù)”更像是關(guān)聯(lián)引用。值(在函數(shù)原型里叫做 “context”(中文稱為“上下文”、“語境”、“環(huán)境參數(shù)”等))也是不透明的void 指針,于是可以在其中存放任意數(shù)據(jù)。然而,必須管理該對象的內(nèi)存。這使得在ARC 環(huán)境下很難使用Objective-C 對象作為值。范例代碼使用 coreFoundation 字符串作為值,因為ARC 并不會自動管理CoreFoundation 對象的內(nèi)存。所以說,這種對象非常適合充當(dāng)“隊列特定數(shù)據(jù)”,它們可以根據(jù)需要與相關(guān)的Objective-C Foundation 類無縫銜接。
函數(shù)的最后一個參數(shù)是“析構(gòu)函數(shù)”(destructor function),對于給定的鍵來說,當(dāng)隊列所占內(nèi)存為系統(tǒng)所回收,或者有新的值與鍵相關(guān)聯(lián)時,原有的值對象就會移除,而析構(gòu)函數(shù)也會于此時運行。dispatch_function_t 類型的定義如下:
typedef void (*dispatch_function_t) (void *)
由此可知,析構(gòu)函數(shù)只能帶有一個指針參數(shù)且返回值必須為 void。范例代碼采用 CFRelease 做析構(gòu)函數(shù),此函數(shù)符合要求,不過也可以采用開發(fā)者自定義的函數(shù),在其中調(diào)用 CFRelease 以清理舊值,并完成其他必要的清理工作。