第六章 block與GCD
“塊”(block)是一種可在C、C++及Objective-C代碼中使用的“詞法閉包”(lexical closure),它極為有用,這主要是因?yàn)榻栌纱藱C(jī)制,開(kāi)發(fā)者可將代碼像對(duì)象一樣傳遞,令其在不同環(huán)境下運(yùn)行。還有個(gè)關(guān)鍵的地方是,在定義“塊”的范圍內(nèi),它可以訪問(wèn)到其中的全部變量。
GCD是一種與“塊”有關(guān)的技術(shù),它提供了對(duì)線程的抽象,而這種抽象則基于“派發(fā)隊(duì)列”(dispatch queue)。開(kāi)發(fā)者可將塊排入隊(duì)列中,由GCD負(fù)責(zé)處理所有調(diào)度事宜。
37.理解“塊”這一概念
塊可以實(shí)現(xiàn)閉包。
1.塊的基礎(chǔ)知識(shí)
塊與函數(shù)類(lèi)似,只不過(guò)是直接定義在另一個(gè)函數(shù)里的,和定義它的那個(gè)函數(shù)共享同一個(gè)范圍內(nèi)的東西。塊用“^”符號(hào)來(lái)表示,后面跟著一對(duì)花括號(hào),括號(hào)里面是塊的實(shí)現(xiàn)代碼。例如:
^{
//Block implementation here
}
塊其實(shí)就是個(gè)值,而且自有其相關(guān)類(lèi)型。與int、float或Objective-C對(duì)象一樣,也可以把塊賦給變量,然后像使用其他變量那樣使用它。塊類(lèi)型的語(yǔ)法與函數(shù)指針近似。下面列出的這個(gè)塊很簡(jiǎn)單,沒(méi)有參數(shù),也不返回值:
void (^someBlock)() = ^{
//Block implementation here
};
這段代碼定義了一個(gè)名為someBlock的變量。由于變量名寫(xiě)在正中間,所以看上去也許有點(diǎn)怪,不過(guò)一旦理解了語(yǔ)法,很容易就能讀懂。
塊類(lèi)型的語(yǔ)法結(jié)構(gòu)如下:
return_type (^block_name)(parameters)
下面這種寫(xiě)法所定義的塊,返回int值,并且接受兩個(gè)int做參數(shù):
int(^addBlock)(int a,int b) = ^(int a,int b){
return a + b;
};
定義好之后,就可以像函數(shù)那樣使用了。比方說(shuō),addBlock塊可以這樣用:
int add = addBlock(2,5);///< add = 7
塊的強(qiáng)大之處是:在聲明它的范圍里,所有變量都可以為其所捕獲。這也就是說(shuō),那個(gè)范圍里的全部變量,在塊里依然可用。比如,下面這段代碼所定義的塊,就使用了塊以外的變量:
int addtional = 5;
int(^addBlock)(int a,int b) = ^(int a,int b){
return a + b + addtional;
};
int add = addBlock(2,5);///< add = 12
默認(rèn)情況下,為塊所捕獲的變量,是不可以在塊里修改的。在本例中,假如塊內(nèi)的代碼改動(dòng)了additional變量的值,那么編譯器就會(huì)報(bào)錯(cuò)。不過(guò),聲明變量的時(shí)候可以加上__block修飾符,這樣就可以在塊內(nèi)修改了。例如,可以用下面這個(gè)塊來(lái)枚舉數(shù)組中的元素(參見(jiàn)第48條),以判斷其中有多少個(gè)小于2的數(shù):
NSArray *array = @[@0,@1,@2,@3,@4,@5];
__block NSInteger count = 0;
[array enumerateObjectsUsingBlock:
^(NSNumber *number, NSUInteger idx, BOOL * _Nonnull stop) {
if([number compare:@2]==NSOrderedAscending){
count++;
}
}];
//count = 2
這段范例代碼也演示了”內(nèi)聯(lián)塊“(inline block)的用法。傳給”enumerateObjectsUsingBlock:”方法的塊并未先賦給局部變量,而是直接內(nèi)聯(lián)在函數(shù)調(diào)用里了。由這種常見(jiàn)的編碼習(xí)慣也可以看出塊為何如此有用。在Objective-C語(yǔ)言引入塊的這一特性之前,想要編出與剛才那段代碼相同的功能,就必須傳入函數(shù)指針或選擇子的名稱,以供枚舉方法調(diào)用。狀態(tài)必須手工傳入傳出,這一版通過(guò)“不透明的void指針“實(shí)現(xiàn),如此一來(lái),就得再寫(xiě)幾行代碼了,而且還會(huì)令方法變得有些松散。與之相反,若聲明內(nèi)聯(lián)形式的塊,則可把所有業(yè)務(wù)邏輯都放在一處。
如果塊所捕獲的變量是對(duì)象類(lèi)型,那么就會(huì)自動(dòng)保留它。系統(tǒng)在釋放這個(gè)塊的時(shí)候,也會(huì)將其一并釋放。這就引出了一個(gè)與塊有關(guān)的重要問(wèn)題。塊本身可視為對(duì)象。實(shí)際上,在其他Objective-C對(duì)象所能響應(yīng)的選擇子中,有很多是塊也可以響應(yīng)的。而最重要之處則在于,塊本身也和其他對(duì)象一樣,有引用計(jì)數(shù)。當(dāng)最后一個(gè)指向塊的引用移走之后,塊就回收了?;厥諘r(shí)也會(huì)釋放塊所捕獲的變量,以便平衡捕獲時(shí)所執(zhí)行的保留操作。
如果將塊定義在Objective-C類(lèi)的實(shí)例方法中,那么除了可以訪問(wèn)類(lèi)的所有實(shí)例變量之外,還可以使用self變量。塊總能修改實(shí)例變量,所以在聲明時(shí)無(wú)須加block。不過(guò),如果通過(guò)讀取或?qū)懭氩僮鞑东@了實(shí)例變量,那么也會(huì)自動(dòng)把self變量一并捕獲了,因?yàn)閷?shí)例變量是與self所指代的實(shí)例關(guān)聯(lián)在一起的。例如,下面這個(gè)塊聲明在EOCClass類(lèi)的方法中:
#import "EOCClass.h"
@implementation EOCClass
-(void)anInstanceMethod{
void (^someBlock)() = ^{
_anInstanceVariable = @"Something";
NSLog(@"_anInstanceVariable = %@",_anInstanceVariable);
};
}
@end
如果某個(gè)EOCClass實(shí)例正在執(zhí)行anInstanceMethod方法,那么self變量就指向此實(shí)例。由于塊里沒(méi)有明確使用self變量,所以很容易就會(huì)忘記self變量其實(shí)也是為塊所捕獲了。直接訪問(wèn)實(shí)例變量和通過(guò)self來(lái)訪問(wèn)時(shí)等效的:
self-> _anInstanceVariable = @"Something";
之所以要捕獲self變量,原因正在于此。我們經(jīng)常通過(guò)屬性訪問(wèn)實(shí)例變量,在這種情況下,就要指明self了:
self.aProperty = @“Something”;
然而,一定要記住:self也是個(gè)對(duì)象,因而塊在捕獲它時(shí)也會(huì)將其保留。如果self所指代的那個(gè)對(duì)象同時(shí)也保留了塊,那么這種情況通常就會(huì)導(dǎo)致”循環(huán)引用“。
2.塊的內(nèi)部結(jié)構(gòu)
每個(gè)Objective-C對(duì)象都占據(jù)著某個(gè)內(nèi)存區(qū)域。因?yàn)閷?shí)例變量的個(gè)數(shù)及對(duì)象所包含的關(guān)聯(lián)數(shù)據(jù)互不相同,所以每個(gè)對(duì)象所占的內(nèi)存區(qū)域也有大有小。塊本身也是對(duì)象,在存放塊對(duì)象的內(nèi)存區(qū)域中,首個(gè)變量是指向Class對(duì)象的指針,該指針叫做isa。其余內(nèi)存里含有塊對(duì)象正常運(yùn)轉(zhuǎn)所需的各種信息。下圖詳細(xì)描述了塊對(duì)象的內(nèi)存布局:

在內(nèi)存布局中,最重要的就是invoke變量,這是個(gè)函數(shù)指針,指向塊的實(shí)現(xiàn)代碼。函數(shù)原型至少要接受一個(gè)void*型的參數(shù),此參數(shù)代表塊。剛才說(shuō)過(guò),塊其實(shí)就是一種代替函數(shù)指針的語(yǔ)法結(jié)構(gòu),原來(lái)使用函數(shù)指針時(shí),需要用”不透明的void指針“來(lái)傳遞狀態(tài)。而改用塊之后,則可以把原來(lái)用標(biāo)準(zhǔn)C語(yǔ)言特性所編寫(xiě)的代碼封裝成簡(jiǎn)明且易用的接口。
descriptor變量是指向結(jié)構(gòu)體的指針,每個(gè)塊里都包含此結(jié)構(gòu)體,其中聲明了塊對(duì)象的總體大小,還聲明了copy與dispose這兩個(gè)輔助函數(shù)所對(duì)應(yīng)的函數(shù)指針。輔助函數(shù)在拷貝及丟棄塊對(duì)象時(shí)運(yùn)行,其中會(huì)執(zhí)行一些操作,比方說(shuō),前者要保留捕獲的對(duì)象,而后者則將之釋放。
塊還會(huì)把它所捕獲的所有變量都拷貝一份。這些拷貝放在descriptor變量后面,捕獲了多少個(gè)變量,就要占據(jù)多少內(nèi)存空間。請(qǐng)注意,拷貝的并不是對(duì)象本身,而是指向這些對(duì)象的指針變量。invoke函數(shù)需要把塊對(duì)象作為參數(shù)傳進(jìn)來(lái)是因?yàn)樵趫?zhí)行塊時(shí),要從內(nèi)存中把這些捕獲到的變量讀出來(lái)。
3.全局塊、棧塊及堆塊
定義塊的時(shí)候,其所占的內(nèi)存區(qū)域是分配在棧中的。這就是說(shuō),塊只在定義它的那個(gè)范圍內(nèi)有效。例如,下面這段代碼就有危險(xiǎn):
void (^block)();
if(/*some condition*/){
block = ^{
NSLog(@"Block A");
};
}else{
block = ^{
NSLog(@"Block B");
};
}
block();
定義在if及else語(yǔ)句中的兩個(gè)塊都分配在棧內(nèi)存中。編譯器會(huì)給每個(gè)塊分配好棧內(nèi)存,然而等離開(kāi)了相應(yīng)的范圍之后,編譯器有可能把分配給塊的內(nèi)存覆寫(xiě)掉。于是,這兩個(gè)塊只能保證在對(duì)應(yīng)的if或else語(yǔ)句范圍內(nèi)有效。這樣寫(xiě)出來(lái)的代碼可以編譯,但是運(yùn)行起來(lái)時(shí)而正確,時(shí)而錯(cuò)誤。若編譯器未覆寫(xiě)待執(zhí)行的塊,則程序照常運(yùn)行,若覆寫(xiě),則程序崩潰。
為解決此問(wèn)題,可給塊對(duì)象發(fā)送copy消息以拷貝之。這樣的話,就可以把塊從棧復(fù)制到堆了??截惡蟮膲K,可以在定義它的那個(gè)范圍之外使用。而且,一旦復(fù)制到堆中,塊就成了帶引用計(jì)數(shù)的對(duì)象了。后續(xù)的復(fù)制操作都不會(huì)真的執(zhí)行復(fù)制,只是遞增塊對(duì)象的引用計(jì)數(shù)。如果不再使用這個(gè)塊,那就應(yīng)該將其釋放,在ARC下會(huì)自動(dòng)釋放,而手動(dòng)管理引用計(jì)數(shù)時(shí)則需要自己來(lái)調(diào)用release方法。當(dāng)引用計(jì)數(shù)降為0時(shí),”分配在堆上的塊“會(huì)像其他對(duì)象一樣,為系統(tǒng)所回收。而”分配在棧上的塊“則無(wú)須明確釋放,因?yàn)闂?nèi)存本來(lái)就會(huì)自動(dòng)回收。
我們只需給代碼加上兩個(gè)copy方法調(diào)用,就可令其變得安全了:
void (^block)();
if(/*some condition*/){
block = [^{
NSLog(@"Block A");
}copy];
}else{
block = [^{
NSLog(@"Block B");
}copy];
}
block();
現(xiàn)在代碼就安全了。如果手動(dòng)管理引用計(jì)數(shù),那么在用完塊之后還需將其釋放。
除了”棧塊“和”堆塊“之外,還有一類(lèi)塊叫做”全局塊“。這種塊不會(huì)捕獲任何狀態(tài)(比如外圍的變量等),運(yùn)行時(shí)也無(wú)須有狀態(tài)來(lái)參與。塊所使用的整個(gè)內(nèi)存區(qū)域,在編譯期已經(jīng)完全確定了,因此,全局塊可以聲明在全局內(nèi)存里,而不需要在每次用到的時(shí)候于棧中創(chuàng)建。另外,全局塊的拷貝操作是個(gè)空操作,因?yàn)槿謮K決不可能為系統(tǒng)所回收。這種塊實(shí)際上相當(dāng)于單例。下面就是個(gè)全局塊:
void (^block)() = ^{
NSLog(@"This is a block");
};
由于運(yùn)行該塊所需的部信息都能在編譯期確定,所以可把它做成全局塊。這完全是種優(yōu)化技術(shù):若把如此簡(jiǎn)單的塊當(dāng)成復(fù)雜的塊來(lái)處理,那就會(huì)在復(fù)制及丟棄該塊時(shí)執(zhí)行一些無(wú)謂的操作。
要點(diǎn):
- 塊是C、C++、Objective-C中的詞法閉包。
- 塊可接受參數(shù),也可返回值。
- 塊可以分配在?;蚨焉?,也可以是全局的。分配在棧上的塊可拷貝到堆里,這樣的話,就和標(biāo)準(zhǔn)的Objective-C對(duì)象一樣,具備引用計(jì)數(shù)了。
38.為常用的塊類(lèi)型創(chuàng)建typedef
每個(gè)塊都具備其”固有類(lèi)型“,因而可將其賦值給適當(dāng)類(lèi)型的變量。這個(gè)類(lèi)型由塊所接受的參數(shù)及其返回值組成。例如有如下這個(gè)塊:
^(BOOL flag,int value){
if(flag){
return value * 5;
}else{
return value * 10;
}
}
此塊接受的兩個(gè)類(lèi)型分別為BOOL類(lèi)型及int的參數(shù),并返回類(lèi)型為int的值。如果想把它賦值給變量,則需注意其類(lèi)型。變量類(lèi)型及相關(guān)賦值語(yǔ)句如下:
int (^variableName)(BOOL flag,int value) =
^(BOOL flag,int value){
//Implementation
return someInt;
}
這個(gè)類(lèi)型似乎和普通的類(lèi)型大不相同,然而如果習(xí)慣函數(shù)指針的話,那么看上去就會(huì)覺(jué)得眼熟了。塊類(lèi)型的語(yǔ)法結(jié)構(gòu)如下:
return_type (^block_name)(parameters)
與其他類(lèi)型的變量不同,在定義塊變量時(shí),要把變量名放在類(lèi)型之中,而不要放在右側(cè)。這種語(yǔ)法非常難記,也非常難讀。鑒于此,我們應(yīng)該為常用的塊類(lèi)型起個(gè)別名,尤其是打算把代碼發(fā)不成API供他人使用時(shí),更應(yīng)這樣做。開(kāi)發(fā)者可以起個(gè)更為易讀的名字來(lái)表示塊的用途,而把塊的類(lèi)型隱藏在其后面。
為了隱藏復(fù)雜的塊類(lèi)型,需要用到C語(yǔ)言中名為“類(lèi)型定義”的特性。typedef關(guān)鍵字用于給類(lèi)型起個(gè)易讀的別名。比方說(shuō),想定義新類(lèi)型,用以表示接受BOOL及int參數(shù)并返回int值的塊,可通過(guò)下列語(yǔ)句來(lái)做:
typedef int(^EOCSomeBlock)(BOOL flag,int value);
聲明變量時(shí),要把名稱放在類(lèi)型中間,并在前面加上“^”符號(hào),而定義新類(lèi)型時(shí)也得這么做。上面這條語(yǔ)句向系統(tǒng)中新增了一個(gè)名為EOCSomeBlock的類(lèi)型。此后,不用再以復(fù)雜的塊類(lèi)型來(lái)創(chuàng)建變量了,直接使用新類(lèi)型即可:
EOCSomeBlock block = ^(BOOL flag,int value){
//Implementation
};
這次代碼讀起來(lái)就順暢多了:與定義其他變量時(shí)一樣,變量類(lèi)型在左邊,變量名在右邊。
通過(guò)這項(xiàng)特性,可以把使用塊的API做得更為易用些。類(lèi)里面有些方法可能需要用塊來(lái)做參數(shù),比如執(zhí)行異步任務(wù)時(shí)所用的“completion handler”參數(shù)就是塊,凡遇到這種情況,都可以通過(guò)定義別名使代碼變得更為易讀。比方說(shuō),類(lèi)里有個(gè)方法可以啟動(dòng)任務(wù),它接受一個(gè)塊作為處理程序,在完成任務(wù)之后執(zhí)行這個(gè)塊。若不定義別名,則方法簽名會(huì)像下面這樣:
-(void)startWithCompletionHandler:
(void(^)(NSData *data,NSError *error))completion;
注意,定義方法參數(shù)所用的塊類(lèi)型語(yǔ)法,又和定義變量時(shí)不同。若能把方法簽名中的參數(shù)類(lèi)型寫(xiě)成一個(gè)詞,那讀起來(lái)就順口多了。于是,可以給參數(shù)類(lèi)型起個(gè)別名,然后使用此名稱來(lái)定義:
typedef void(^EOCCompletionHandler)(NSData *data,NSError *error);
-(void)startWithCompletionHandler:(EOCCompletionHandler)completion;
現(xiàn)在參數(shù)看上去就簡(jiǎn)單多了,而且易于理解。
使用類(lèi)型定義還有個(gè)好處,就是當(dāng)你打算重構(gòu)塊的類(lèi)型簽名時(shí)會(huì)很方便。比方說(shuō),要給原來(lái)的completion handler塊再加一個(gè)參數(shù),用以表示完成任務(wù)所花的時(shí)間,那么只需修改類(lèi)型定義語(yǔ)句即可:
typedef void(^EOCCompletionHandler)(NSData *data,NSTimeInterval duration,NSError *error);
修改之后,凡是使用了這個(gè)類(lèi)型定義的地方,比如方法簽名等處,都會(huì)無(wú)法編譯,而且報(bào)的是同一種錯(cuò)誤,于是開(kāi)發(fā)者可據(jù)此逐個(gè)修復(fù)。若不用類(lèi)型定義,而直接寫(xiě)塊類(lèi)型,那么代碼中要修改的地方就更多了。開(kāi)發(fā)者很容易忘掉其中一兩處,從而引發(fā)難于排查的bug。
最好在使用塊類(lèi)型的類(lèi)中定義這些typedef,而且還應(yīng)該把這個(gè)類(lèi)的名字加在由typedef所定義的新類(lèi)型名前面,這樣可以闡明塊的用途。還可以用typedef給同一個(gè)塊簽名類(lèi)型創(chuàng)建數(shù)個(gè)別名。在這件事上,多多益善。
Mac OS X與iOS的Accounts框架就是個(gè)例子。在該框架中可以找到下面這兩個(gè)類(lèi)型定義語(yǔ)句:
typedef void(^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);
typedef void(^ACAccountStoreRequestAccessCompletionHandler)(BOOL granted, NSError *error);
這兩個(gè)類(lèi)型定義的簽名相同,但用在不同的地方。開(kāi)發(fā)者看到類(lèi)型別名及簽名中的參數(shù)之后,很容易就能理解此類(lèi)型的用途。它們本來(lái)也可以合并成一個(gè)typedef,比如叫做ACAccountStorBooleanCompletionHandler,使用那兩個(gè)別名的地方,都可以統(tǒng)一使用此名稱。然后,這么做之后,塊與參數(shù)的用途看上去就不那么明顯了。
與此相似,如果有好幾個(gè)類(lèi)都要執(zhí)行相似但各有區(qū)別的異步任務(wù),而這幾個(gè)類(lèi)又不能放入同一個(gè)繼承體系,那么,每個(gè)類(lèi)就應(yīng)該有自己的completion handler類(lèi)型。這幾個(gè)completion handler的前面也許完全相同,但最好還是在每個(gè)類(lèi)里都各自定義一個(gè)別名,而不要共用同一個(gè)名稱。反之,若這些類(lèi)能納入同一個(gè)繼承中,則應(yīng)該將類(lèi)型定義語(yǔ)句放在超類(lèi)中,以供各子類(lèi)使用。
要點(diǎn):
- 以typedef重新定義塊類(lèi)型,可令塊變量用起來(lái)更加簡(jiǎn)單。
- 定義新類(lèi)型時(shí)應(yīng)遵從現(xiàn)有的命名規(guī)范,勿使其名稱與別的類(lèi)型相沖突。
- 不妨為同一個(gè)塊簽名定義多個(gè)類(lèi)型別名。如果要重構(gòu)的代碼使用了塊類(lèi)型的某個(gè)別名,那么只需修改相應(yīng)的typedef中的塊簽名即可,無(wú)須改動(dòng)其他typedef。
39.用handler塊降低代碼分散程度
與使用委托模式的代碼相比,用塊寫(xiě)出來(lái)的代碼顯得更為簡(jiǎn)潔。異步任務(wù)執(zhí)行完畢后所需運(yùn)行的業(yè)務(wù)邏輯,和啟動(dòng)異步任務(wù)所用的代碼放在了一起。而且,由于塊聲明在創(chuàng)建獲取器的范圍內(nèi),所以它可以訪問(wèn)此范圍內(nèi)的全部變量。
委托模式有個(gè)缺點(diǎn):如果類(lèi)要分別使用多個(gè)獲取器下載不同數(shù)據(jù),那么就得在delegate回調(diào)方法里根據(jù)傳入的獲取器參數(shù)來(lái)切換。
把成功情況和失敗情況放在同一個(gè)塊中,缺點(diǎn)是:由于全部邏輯都寫(xiě)在一起,所以會(huì)令塊變得比較長(zhǎng),切比較復(fù)雜。然而只用一個(gè)塊的寫(xiě)法也有好處,那就是更為靈活。比方說(shuō),在傳入錯(cuò)誤信息時(shí),可以把數(shù)據(jù)也傳進(jìn)來(lái)。有時(shí)數(shù)據(jù)正下載到一半,突然網(wǎng)絡(luò)故障了。在這種情況下,可以把數(shù)據(jù)及相關(guān)的錯(cuò)誤都傳給塊。這樣的話,completion handler就能根據(jù)此判斷問(wèn)題并適當(dāng)處理了,而且還可利用已下載好的這部分?jǐn)?shù)據(jù)做些事情。還有個(gè)優(yōu)點(diǎn)是:調(diào)用API的代碼可能會(huì)在處理成功響應(yīng)的過(guò)程中發(fā)現(xiàn)錯(cuò)誤。這種情況需要和網(wǎng)絡(luò)數(shù)據(jù)獲取器所認(rèn)定的失敗情況按同一方式處理。此時(shí),如果采用單一塊的寫(xiě)法,那么就能把這種情況和獲取器認(rèn)定的失敗情況統(tǒng)一處理了。要是把成功情況和失敗情況交給兩個(gè)不同的處理程序來(lái)負(fù)責(zé),那么就沒(méi)辦法共享同一份錯(cuò)誤處理代碼了,除非把這段代碼單獨(dú)放在一個(gè)方法里,而這又違背了我們想把全部邏輯代碼都放在一處的初衷。
建議使用同一個(gè)塊來(lái)處理成功與失敗情況。
基于handler來(lái)設(shè)計(jì)API還有個(gè)原因,就是某些代碼必須運(yùn)行在特定的線程上。比方說(shuō),Cocoa與Cocoa Touch中的UI操作必須在主線程上執(zhí)行。這就相當(dāng)于GCD中的“主隊(duì)列”。因此,最好能由調(diào)用API的人來(lái)決定handler應(yīng)該運(yùn)行在哪個(gè)線程上。NSNotificationCenter就屬于這種API,它提供了一個(gè)方法,調(diào)用者可以經(jīng)由此方法來(lái)注冊(cè)想要接收的通知,等到相關(guān)事件發(fā)生時(shí),通知中心就會(huì)執(zhí)行注冊(cè)好的那個(gè)塊。調(diào)用者可以指定某個(gè)塊應(yīng)該安排在哪個(gè)執(zhí)行隊(duì)列里,然而這不是必需的。若沒(méi)有指定隊(duì)列,則按默認(rèn)方式執(zhí)行,也就是說(shuō),將由投遞通知的那個(gè)線程來(lái)執(zhí)行。下列方法可用來(lái)新增觀察者:
- (id <NSObject>)addObserverForName:(nullable NSString *)name
object:(nullable id)obj
queue:(nullable NSOperationQueue *)queue
usingBlock:(void (^)(NSNotification *note))block
此處傳入的NSOperationQueue參數(shù)就表示觸發(fā)通知時(shí)用來(lái)執(zhí)行塊代碼的那個(gè)隊(duì)列。這是個(gè)“操作隊(duì)列”,而非“底層GCD隊(duì)列”,不過(guò)兩者語(yǔ)義相同。
要點(diǎn):
- 要?jiǎng)?chuàng)建對(duì)象時(shí),可以使用內(nèi)聯(lián)的handler塊將相關(guān)業(yè)務(wù)邏輯一并聲明。
- 在有多個(gè)實(shí)例需要監(jiān)控時(shí),如果采用委托模式,那么經(jīng)常需要根據(jù)傳入的對(duì)象來(lái)切換,而若改用handler塊來(lái)實(shí)現(xiàn),則可直接將塊與相關(guān)對(duì)象放在一起。
- 設(shè)計(jì)API時(shí)如果用到了handler塊,那么可以增加一個(gè)參數(shù),使調(diào)用者可通過(guò)此參數(shù)來(lái)決定應(yīng)該把塊安排在哪個(gè)隊(duì)列上執(zhí)行。
40.用Block引用其所屬對(duì)象時(shí)不要出現(xiàn)循環(huán)引用
使用塊時(shí),若不仔細(xì)思量,則很容易導(dǎo)致循環(huán)引用。比方說(shuō),下面這個(gè)類(lèi)就提供了一套接口,調(diào)用者可由此從某個(gè)URL中下載數(shù)據(jù)。在啟動(dòng)獲取器時(shí),可設(shè)置completion handler,這個(gè)塊會(huì)在下載結(jié)束之后以回調(diào)方式執(zhí)行。為了能在p_requestCompleted方法執(zhí)行調(diào)用者所指定的塊,這段代碼需要把completion handler保存到實(shí)例變量里面。
//EOCNetworkFetcher.h
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
@property(nonatomic,strong,readonly)NSURL *url;
-(instancetype)initWithURL:(NSURL *)url;
-(void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)completion;
@end
//EOCNetworkFetcher.m
@interface EOCNetworkFetcher ()
@property(nonatomic,strong,readwrite)NSURL *url;
@property(nonatomic,copy)EOCNetworkFetcherCompletionHandler completionHandler;
@property(nonatomic,strong)NSData *downloadedData;
@end
@implementation EOCNetworkFetcher
-(instancetype)initWithURL:(NSURL *)url{
if(self = [super init]){
_url = url;
}
return self;
}
-(void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion{
self.completionHandler = completion;
//Start the request
//Request sets downloadedData property
//When request is finished,p_requestCompleted is called
}
-(void)p_requestCompleted{
if(_completionHandler){
_completionHandler(_downloadedData);
}
}
@end
某個(gè)類(lèi)可能會(huì)創(chuàng)建這種網(wǎng)絡(luò)數(shù)據(jù)獲取器對(duì)象,并用其從URL中下載數(shù)據(jù):
#import "EOCClass.h"
#import "EOCNetworkFetcher.h"
@interface EOCClass ()
{
EOCNetworkFetcher *_networkFetcher;
NSData *_fetchedData;
}
@end
@implementation EOCClass
-(void)downloadData{
NSURL *url = [[NSURL alloc]initWithString:@"http://www.example.com/something.dat"];
_networkFetcher = [[EOCNetworkFetcher alloc]initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
NSLog(@"Request URL %@ finished",_networkFetcher.url);
_fetchedData = data;
}];
}
@end
這里就造成了一個(gè)循環(huán)引用。因?yàn)閏ompletion handler塊要設(shè)置_fetchedData實(shí)例變量,所以它必須捕獲self變量(第37條)。這就是說(shuō),handler塊保留了創(chuàng)建網(wǎng)絡(luò)數(shù)據(jù)獲取器的那個(gè)EOCClass實(shí)例。而EOCClass實(shí)例則通過(guò)strong實(shí)例變量保留了獲取器,最后,獲取器對(duì)象又保留了handler塊。下圖描述了這個(gè)環(huán):

要打破循環(huán)引用也很容易:要么令_networkFetcher實(shí)例變量不要引用獲取器,要么令獲取器的completionHandler屬性不再持有handler塊。在網(wǎng)絡(luò)數(shù)據(jù)獲取器這個(gè)例子中,應(yīng)該等completion handler塊執(zhí)行完畢后,再去打破引用環(huán),以便使獲取器對(duì)象在handler塊執(zhí)行期間保持存活狀態(tài)。比方說(shuō),completion handler塊的代碼可以這么修改:
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
NSLog(@"Request URL %@ finished",_networkFetcher.url);
_fetchedData = data;
_networkFetcher = nil;
}];
如果設(shè)計(jì)API時(shí)用到了completion handler這樣的回調(diào)塊,那么很容易形成循環(huán)引用,所以必須意識(shí)到這個(gè)重要問(wèn)題。一般來(lái)說(shuō),只要適時(shí)清理掉環(huán)中的某個(gè)引用,即可解決此問(wèn)題,然而,未必總有這種機(jī)會(huì)。在本例中,唯有completion handler運(yùn)行過(guò)后,方能解除引用環(huán)。若是completion handler一直不運(yùn)行,那么引用環(huán)就無(wú)法打破,于是內(nèi)存就會(huì)泄露。
像completion handler塊這種寫(xiě)法,還可能引入另外一種形式的引用環(huán)。如果completion handler塊所引用的對(duì)象最終又引用了這個(gè)塊本身,那么就會(huì)出現(xiàn)引用環(huán)。比方說(shuō),我們修改下這個(gè)例子,使調(diào)用API的那段代碼無(wú)須在執(zhí)行期間保留指向網(wǎng)絡(luò)數(shù)據(jù)獲取器的引用,而是設(shè)定一套機(jī)制,令獲取器對(duì)象自己設(shè)法保持存活。要想保持存活,獲取器對(duì)象可以在啟動(dòng)任務(wù)時(shí)把自己加到全局的collection中(比如用set來(lái)實(shí)現(xiàn)這個(gè)collection),待任務(wù)完成后,再移除。而調(diào)用方則需將其代碼修改如下:
-(void)downloadData{
NSURL *url = [[NSURL alloc]initWithString:@"http://www.example.com/something.dat"];
EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc]initWithURL:url];
[networkFetcher startWithCompletionHandler:^(NSData *data) {
NSLog(@"Request URL %@ finished",_networkFetcher.url);
_fetchedData = data;
}];
}
大部分網(wǎng)絡(luò)通信庫(kù)都采用這種方法,因?yàn)榧偃缌钫{(diào)用者自己來(lái)將獲取器對(duì)象保持存活的話,他們會(huì)覺(jué)得麻煩。Twitter框架的TWRequest對(duì)象也用的這個(gè)辦法。然后,本例這樣做會(huì)引入引用環(huán)。completion handler塊其實(shí)要通過(guò)獲取器對(duì)象來(lái)引用其中的URL(引用了EOCNetworkFetcher的url屬性)。于是,塊就要保留獲取器,而獲取器反過(guò)來(lái)又經(jīng)由其completion handler屬性保留了這個(gè)塊。所幸要修復(fù)這個(gè)問(wèn)題也不難?;叵胍幌拢@取器對(duì)象之所以要把completion handler塊保存在屬性里面,其唯一目的就是想稍后使用這個(gè)塊。于是,獲取器一旦運(yùn)行過(guò)completion handler之后,就沒(méi)必要再保留它了。所以,只需將p_requestCompleted方法按照如下方式修改即可:
-(void)p_requestCompleted{
if(_completionHandler){
_completionHandler(_downloadedData);
}
self.completionHandler = nil;
}
這樣一來(lái),只要下載請(qǐng)求執(zhí)行完畢,引用環(huán)就解除了,而獲取器對(duì)象也將會(huì)在必要時(shí)為系統(tǒng)所回收。請(qǐng)注意,之所以要在start方法中把completion handler作為參數(shù)傳進(jìn)去,這也是一條重要原因。假如把completion handler暴露為獲取器對(duì)象的公共屬性,那么就不便在執(zhí)行完下載請(qǐng)求之后直接將其清理掉了。因?yàn)榧热灰呀?jīng)把handler作為屬性公布了,那就意味著調(diào)用者可以自由使用它,若是此時(shí)又在內(nèi)部將其清理掉的話,則會(huì)破壞“封裝語(yǔ)義”。在這種情況下要想打破引用環(huán),只有一個(gè)辦法可用,那就是強(qiáng)迫調(diào)用者在handler代碼里自己把completionHandler屬性清理干凈。可這并不是十分合理,因?yàn)槟銦o(wú)法假定調(diào)用者一定會(huì)這么做。
這兩種引用環(huán)都很容易發(fā)生。使用塊來(lái)編程時(shí),一不小心就會(huì)出現(xiàn)這種bug,反過(guò)來(lái)說(shuō),只要小心謹(jǐn)慎,這種問(wèn)題也很容易解決。關(guān)鍵在于,要想清楚塊可能會(huì)捕獲并保留哪些對(duì)象。如果這些對(duì)象又直接或間接保留了塊,那么就要考慮怎樣在適當(dāng)?shù)臅r(shí)機(jī)解除引用環(huán)。
要點(diǎn):
- 如果塊所捕獲的對(duì)象直接或間接地保留了塊本身,那么就得當(dāng)心循環(huán)引用問(wèn)題。
- 一定要找個(gè)適當(dāng)?shù)臅r(shí)機(jī)解除循環(huán)引用,而不能把責(zé)任推給API的調(diào)用者。
轉(zhuǎn)載請(qǐng)注明出處:第六章 block與GCD(上)
參考:《Effective Objective-C 2.0》