第六章 block與GCD(上)

第六章 block與GCD

“塊”(block)是一種可在C、C++及Objective-C代碼中使用的“詞法閉包”(lexical closure)筒溃,它極為有用,這主要是因為借由此機制,開發(fā)者可將代碼像對象一樣傳遞页徐,令其在不同環(huán)境下運行豌习。還有個關(guān)鍵的地方是存谎,在定義“塊”的范圍內(nèi),它可以訪問到其中的全部變量肥隆。
GCD是一種與“塊”有關(guān)的技術(shù)既荚,它提供了對線程的抽象,而這種抽象則基于“派發(fā)隊列”(dispatch queue)栋艳。開發(fā)者可將塊排入隊列中恰聘,由GCD負責處理所有調(diào)度事宜。

37.理解“塊”這一概念

塊可以實現(xiàn)閉包吸占。

1.塊的基礎(chǔ)知識

塊與函數(shù)類似晴叨,只不過是直接定義在另一個函數(shù)里的,和定義它的那個函數(shù)共享同一個范圍內(nèi)的東西矾屯。塊用“^”符號來表示兼蕊,后面跟著一對花括號,括號里面是塊的實現(xiàn)代碼件蚕。例如:

^{
    //Block implementation here
}

塊其實就是個值遍略,而且自有其相關(guān)類型。與int骤坐、float或Objective-C對象一樣绪杏,也可以把塊賦給變量,然后像使用其他變量那樣使用它纽绍。塊類型的語法與函數(shù)指針近似蕾久。下面列出的這個塊很簡單,沒有參數(shù)拌夏,也不返回值:

void (^someBlock)() = ^{
    //Block implementation here
};

這段代碼定義了一個名為someBlock的變量僧著。由于變量名寫在正中間履因,所以看上去也許有點怪,不過一旦理解了語法盹愚,很容易就能讀懂栅迄。

塊類型的語法結(jié)構(gòu)如下:

return_type (^block_name)(parameters)

下面這種寫法所定義的塊,返回int值皆怕,并且接受兩個int做參數(shù):

int(^addBlock)(int a,int b) = ^(int a,int b){
    return a + b;
};

定義好之后毅舆,就可以像函數(shù)那樣使用了。比方說愈腾,addBlock塊可以這樣用:

int add =  addBlock(2,5);///< add = 7

塊的強大之處是:在聲明它的范圍里憋活,所有變量都可以為其所捕獲。這也就是說虱黄,那個范圍里的全部變量悦即,在塊里依然可用。比如橱乱,下面這段代碼所定義的塊辜梳,就使用了塊以外的變量:

int addtional = 5;
int(^addBlock)(int a,int b) = ^(int a,int b){
    return a + b + addtional;
};
int add =  addBlock(2,5);///< add = 12

默認情況下,為塊所捕獲的變量泳叠,是不可以在塊里修改的作瞄。在本例中,假如塊內(nèi)的代碼改動了additional變量的值析二,那么編譯器就會報錯粉洼。不過节预,聲明變量的時候可以加上__block修飾符叶摄,這樣就可以在塊內(nèi)修改了。例如安拟,可以用下面這個塊來枚舉數(shù)組中的元素(參見第48條)蛤吓,以判斷其中有多少個小于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)用里了。由這種常見的編碼習慣也可以看出塊為何如此有用。在Objective-C語言引入塊的這一特性之前恶阴,想要編出與剛才那段代碼相同的功能拯勉,就必須傳入函數(shù)指針或選擇子的名稱,以供枚舉方法調(diào)用顾瞻。狀態(tài)必須手工傳入傳出泼疑,這一版通過“不透明的void指針“實現(xiàn),如此一來荷荤,就得再寫幾行代碼了退渗,而且還會令方法變得有些松散移稳。與之相反,若聲明內(nèi)聯(lián)形式的塊会油,則可把所有業(yè)務邏輯都放在一處个粱。

如果塊所捕獲的變量是對象類型,那么就會自動保留它翻翩。系統(tǒng)在釋放這個塊的時候都许,也會將其一并釋放。這就引出了一個與塊有關(guān)的重要問題体斩。塊本身可視為對象梭稚。實際上,在其他Objective-C對象所能響應的選擇子中絮吵,有很多是塊也可以響應的弧烤。而最重要之處則在于,塊本身也和其他對象一樣蹬敲,有引用計數(shù)暇昂。當最后一個指向塊的引用移走之后,塊就回收了伴嗡〖辈ǎ回收時也會釋放塊所捕獲的變量,以便平衡捕獲時所執(zhí)行的保留操作瘪校。

如果將塊定義在Objective-C類的實例方法中澄暮,那么除了可以訪問類的所有實例變量之外,還可以使用self變量阱扬。塊總能修改實例變量泣懊,所以在聲明時無須加block。不過麻惶,如果通過讀取或?qū)懭氩僮鞑东@了實例變量馍刮,那么也會自動把self變量一并捕獲了,因為實例變量是與self所指代的實例關(guān)聯(lián)在一起的窃蹋。例如卡啰,下面這個塊聲明在EOCClass類的方法中:

#import "EOCClass.h"

@implementation EOCClass
-(void)anInstanceMethod{
    void (^someBlock)() = ^{
        _anInstanceVariable = @"Something";
        NSLog(@"_anInstanceVariable = %@",_anInstanceVariable);
    };
}
@end

如果某個EOCClass實例正在執(zhí)行anInstanceMethod方法,那么self變量就指向此實例警没。由于塊里沒有明確使用self變量匈辱,所以很容易就會忘記self變量其實也是為塊所捕獲了。直接訪問實例變量和通過self來訪問時等效的:

self-> _anInstanceVariable = @"Something";

之所以要捕獲self變量杀迹,原因正在于此亡脸。我們經(jīng)常通過屬性訪問實例變量,在這種情況下,就要指明self了:

self.aProperty = @“Something”;

然而梗掰,一定要記浊堆浴:self也是個對象,因而塊在捕獲它時也會將其保留及穗。如果self所指代的那個對象同時也保留了塊摧茴,那么這種情況通常就會導致”循環(huán)引用“。

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

每個Objective-C對象都占據(jù)著某個內(nèi)存區(qū)域埂陆。因為實例變量的個數(shù)及對象所包含的關(guān)聯(lián)數(shù)據(jù)互不相同苛白,所以每個對象所占的內(nèi)存區(qū)域也有大有小。塊本身也是對象焚虱,在存放塊對象的內(nèi)存區(qū)域中购裙,首個變量是指向Class對象的指針,該指針叫做isa鹃栽。其余內(nèi)存里含有塊對象正常運轉(zhuǎn)所需的各種信息躏率。下圖詳細描述了塊對象的內(nèi)存布局:

塊對象的內(nèi)存布局

在內(nèi)存布局中,最重要的就是invoke變量民鼓,這是個函數(shù)指針薇芝,指向塊的實現(xiàn)代碼。函數(shù)原型至少要接受一個void*型的參數(shù)丰嘉,此參數(shù)代表塊夯到。剛才說過,塊其實就是一種代替函數(shù)指針的語法結(jié)構(gòu)饮亏,原來使用函數(shù)指針時耍贾,需要用”不透明的void指針“來傳遞狀態(tài)。而改用塊之后路幸,則可以把原來用標準C語言特性所編寫的代碼封裝成簡明且易用的接口荐开。

descriptor變量是指向結(jié)構(gòu)體的指針,每個塊里都包含此結(jié)構(gòu)體劝赔,其中聲明了塊對象的總體大小誓焦,還聲明了copy與dispose這兩個輔助函數(shù)所對應的函數(shù)指針胆敞。輔助函數(shù)在拷貝及丟棄塊對象時運行着帽,其中會執(zhí)行一些操作,比方說移层,前者要保留捕獲的對象仍翰,而后者則將之釋放。

塊還會把它所捕獲的所有變量都拷貝一份观话。這些拷貝放在descriptor變量后面予借,捕獲了多少個變量,就要占據(jù)多少內(nèi)存空間。請注意灵迫,拷貝的并不是對象本身秦叛,而是指向這些對象的指針變量。invoke函數(shù)需要把塊對象作為參數(shù)傳進來是因為在執(zhí)行塊時瀑粥,要從內(nèi)存中把這些捕獲到的變量讀出來挣跋。

3.全局塊、棧塊及堆塊

定義塊的時候狞换,其所占的內(nèi)存區(qū)域是分配在棧中的避咆。這就是說,塊只在定義它的那個范圍內(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í)行的塊,則程序照常運行丈氓,若覆寫周循,則程序崩潰。

為解決此問題万俗,可給塊對象發(fā)送copy消息以拷貝之湾笛。這樣的話,就可以把塊從棧復制到堆了闰歪『垦校拷貝后的塊,可以在定義它的那個范圍之外使用库倘。而且临扮,一旦復制到堆中,塊就成了帶引用計數(shù)的對象了教翩。后續(xù)的復制操作都不會真的執(zhí)行復制杆勇,只是遞增塊對象的引用計數(shù)。如果不再使用這個塊饱亿,那就應該將其釋放蚜退,在ARC下會自動釋放闰靴,而手動管理引用計數(shù)時則需要自己來調(diào)用release方法。當引用計數(shù)降為0時钻注,”分配在堆上的塊“會像其他對象一樣蚂且,為系統(tǒng)所回收。而”分配在棧上的塊“則無須明確釋放幅恋,因為棧內(nèi)存本來就會自動回收膘掰。

我們只需給代碼加上兩個copy方法調(diào)用,就可令其變得安全了:

void (^block)();
if(/*some condition*/){
    block = [^{
        NSLog(@"Block A");
    }copy];
}else{
    block = [^{
        NSLog(@"Block B");
    }copy];
}
block();

現(xiàn)在代碼就安全了佳遣。如果手動管理引用計數(shù)识埋,那么在用完塊之后還需將其釋放。

除了”棧塊“和”堆塊“之外零渐,還有一類塊叫做”全局塊“窒舟。這種塊不會捕獲任何狀態(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

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

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

此塊接受的兩個類型分別為BOOL類型及int的參數(shù),并返回類型為int的值驮瞧。如果想把它賦值給變量氓扛,則需注意其類型。變量類型及相關(guān)賦值語句如下:

int (^variableName)(BOOL flag,int value) =
    ^(BOOL flag,int value){
        //Implementation
        return someInt;
    }

這個類型似乎和普通的類型大不相同论笔,然而如果習慣函數(shù)指針的話采郎,那么看上去就會覺得眼熟了。塊類型的語法結(jié)構(gòu)如下:

return_type (^block_name)(parameters)

與其他類型的變量不同狂魔,在定義塊變量時蒜埋,要把變量名放在類型之中,而不要放在右側(cè)最楷。這種語法非常難記整份,也非常難讀。鑒于此籽孙,我們應該為常用的塊類型起個別名烈评,尤其是打算把代碼發(fā)不成API供他人使用時,更應這樣做犯建。開發(fā)者可以起個更為易讀的名字來表示塊的用途讲冠,而把塊的類型隱藏在其后面。

為了隱藏復雜的塊類型适瓦,需要用到C語言中名為“類型定義”的特性沟启。typedef關(guān)鍵字用于給類型起個易讀的別名。比方說犹菇,想定義新類型德迹,用以表示接受BOOL及int參數(shù)并返回int值的塊,可通過下列語句來做:

typedef int(^EOCSomeBlock)(BOOL flag,int value);

聲明變量時揭芍,要把名稱放在類型中間胳搞,并在前面加上“^”符號,而定義新類型時也得這么做。上面這條語句向系統(tǒng)中新增了一個名為EOCSomeBlock的類型称杨。此后肌毅,不用再以復雜的塊類型來創(chuàng)建變量了,直接使用新類型即可:

EOCSomeBlock block = ^(BOOL flag,int value){
    //Implementation
};

這次代碼讀起來就順暢多了:與定義其他變量時一樣姑原,變量類型在左邊悬而,變量名在右邊。

通過這項特性锭汛,可以把使用塊的API做得更為易用些笨奠。類里面有些方法可能需要用塊來做參數(shù)袭蝗,比如執(zhí)行異步任務時所用的“completion handler”參數(shù)就是塊,凡遇到這種情況般婆,都可以通過定義別名使代碼變得更為易讀到腥。比方說,類里有個方法可以啟動任務蔚袍,它接受一個塊作為處理程序乡范,在完成任務之后執(zhí)行這個塊。若不定義別名啤咽,則方法簽名會像下面這樣:

-(void)startWithCompletionHandler:
(void(^)(NSData *data,NSError *error))completion;

注意晋辆,定義方法參數(shù)所用的塊類型語法,又和定義變量時不同宇整。若能把方法簽名中的參數(shù)類型寫成一個詞栈拖,那讀起來就順口多了。于是没陡,可以給參數(shù)類型起個別名涩哟,然后使用此名稱來定義:

typedef void(^EOCCompletionHandler)(NSData *data,NSError *error);

-(void)startWithCompletionHandler:(EOCCompletionHandler)completion;

現(xiàn)在參數(shù)看上去就簡單多了,而且易于理解盼玄。

使用類型定義還有個好處贴彼,就是當你打算重構(gòu)塊的類型簽名時會很方便。比方說埃儿,要給原來的completion handler塊再加一個參數(shù)器仗,用以表示完成任務所花的時間,那么只需修改類型定義語句即可:

typedef void(^EOCCompletionHandler)(NSData *data,NSTimeInterval duration,NSError *error);

修改之后童番,凡是使用了這個類型定義的地方精钮,比如方法簽名等處,都會無法編譯剃斧,而且報的是同一種錯誤轨香,于是開發(fā)者可據(jù)此逐個修復。若不用類型定義幼东,而直接寫塊類型臂容,那么代碼中要修改的地方就更多了。開發(fā)者很容易忘掉其中一兩處根蟹,從而引發(fā)難于排查的bug脓杉。

最好在使用塊類型的類中定義這些typedef,而且還應該把這個類的名字加在由typedef所定義的新類型名前面简逮,這樣可以闡明塊的用途球散。還可以用typedef給同一個塊簽名類型創(chuàng)建數(shù)個別名。在這件事上散庶,多多益善蕉堰。

Mac OS X與iOS的Accounts框架就是個例子凌净。在該框架中可以找到下面這兩個類型定義語句:

typedef void(^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);
typedef void(^ACAccountStoreRequestAccessCompletionHandler)(BOOL granted, NSError *error);

這兩個類型定義的簽名相同,但用在不同的地方嘁灯。開發(fā)者看到類型別名及簽名中的參數(shù)之后泻蚊,很容易就能理解此類型的用途躲舌。它們本來也可以合并成一個typedef丑婿,比如叫做ACAccountStorBooleanCompletionHandler,使用那兩個別名的地方没卸,都可以統(tǒng)一使用此名稱羹奉。然后,這么做之后约计,塊與參數(shù)的用途看上去就不那么明顯了诀拭。

與此相似,如果有好幾個類都要執(zhí)行相似但各有區(qū)別的異步任務煤蚌,而這幾個類又不能放入同一個繼承體系耕挨,那么,每個類就應該有自己的completion handler類型尉桩。這幾個completion handler的前面也許完全相同筒占,但最好還是在每個類里都各自定義一個別名,而不要共用同一個名稱蜘犁。反之翰苫,若這些類能納入同一個繼承中,則應該將類型定義語句放在超類中这橙,以供各子類使用奏窑。

要點:

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

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

與使用委托模式的代碼相比巍实,用塊寫出來的代碼顯得更為簡潔。異步任務執(zhí)行完畢后所需運行的業(yè)務邏輯哩牍,和啟動異步任務所用的代碼放在了一起棚潦。而且,由于塊聲明在創(chuàng)建獲取器的范圍內(nèi)膝昆,所以它可以訪問此范圍內(nèi)的全部變量丸边。

委托模式有個缺點:如果類要分別使用多個獲取器下載不同數(shù)據(jù)叠必,那么就得在delegate回調(diào)方法里根據(jù)傳入的獲取器參數(shù)來切換。

把成功情況和失敗情況放在同一個塊中妹窖,缺點是:由于全部邏輯都寫在一起纬朝,所以會令塊變得比較長,切比較復雜骄呼。然而只用一個塊的寫法也有好處共苛,那就是更為靈活。比方說蜓萄,在傳入錯誤信息時隅茎,可以把數(shù)據(jù)也傳進來。有時數(shù)據(jù)正下載到一半嫉沽,突然網(wǎng)絡(luò)故障了辟犀。在這種情況下,可以把數(shù)據(jù)及相關(guān)的錯誤都傳給塊绸硕。這樣的話堂竟,completion handler就能根據(jù)此判斷問題并適當處理了,而且還可利用已下載好的這部分數(shù)據(jù)做些事情玻佩。還有個優(yōu)點是:調(diào)用API的代碼可能會在處理成功響應的過程中發(fā)現(xiàn)錯誤出嘹。這種情況需要和網(wǎng)絡(luò)數(shù)據(jù)獲取器所認定的失敗情況按同一方式處理。此時夺蛇,如果采用單一塊的寫法疚漆,那么就能把這種情況和獲取器認定的失敗情況統(tǒng)一處理了。要是把成功情況和失敗情況交給兩個不同的處理程序來負責刁赦,那么就沒辦法共享同一份錯誤處理代碼了娶聘,除非把這段代碼單獨放在一個方法里,而這又違背了我們想把全部邏輯代碼都放在一處的初衷甚脉。

建議使用同一個塊來處理成功與失敗情況丸升。

基于handler來設(shè)計API還有個原因,就是某些代碼必須運行在特定的線程上牺氨。比方說狡耻,Cocoa與Cocoa Touch中的UI操作必須在主線程上執(zhí)行。這就相當于GCD中的“主隊列”猴凹。因此夷狰,最好能由調(diào)用API的人來決定handler應該運行在哪個線程上。NSNotificationCenter就屬于這種API郊霎,它提供了一個方法沼头,調(diào)用者可以經(jīng)由此方法來注冊想要接收的通知,等到相關(guān)事件發(fā)生時,通知中心就會執(zhí)行注冊好的那個塊进倍。調(diào)用者可以指定某個塊應該安排在哪個執(zhí)行隊列里土至,然而這不是必需的。若沒有指定隊列猾昆,則按默認方式執(zhí)行陶因,也就是說,將由投遞通知的那個線程來執(zhí)行垂蜗。下列方法可用來新增觀察者:

- (id <NSObject>)addObserverForName:(nullable NSString *)name 
                             object:(nullable id)obj 
                             queue:(nullable NSOperationQueue *)queue 
                        usingBlock:(void (^)(NSNotification *note))block

此處傳入的NSOperationQueue參數(shù)就表示觸發(fā)通知時用來執(zhí)行塊代碼的那個隊列楷扬。這是個“操作隊列”,而非“底層GCD隊列”么抗,不過兩者語義相同毅否。

要點:

  • 要創(chuàng)建對象時亚铁,可以使用內(nèi)聯(lián)的handler塊將相關(guān)業(yè)務邏輯一并聲明蝇刀。
  • 在有多個實例需要監(jiān)控時,如果采用委托模式徘溢,那么經(jīng)常需要根據(jù)傳入的對象來切換吞琐,而若改用handler塊來實現(xiàn),則可直接將塊與相關(guān)對象放在一起然爆。
  • 設(shè)計API時如果用到了handler塊站粟,那么可以增加一個參數(shù),使調(diào)用者可通過此參數(shù)來決定應該把塊安排在哪個隊列上執(zhí)行曾雕。

40.用Block引用其所屬對象時不要出現(xiàn)循環(huán)引用

使用塊時奴烙,若不仔細思量,則很容易導致循環(huán)引用剖张。比方說切诀,下面這個類就提供了一套接口,調(diào)用者可由此從某個URL中下載數(shù)據(jù)搔弄。在啟動獲取器時幅虑,可設(shè)置completion handler,這個塊會在下載結(jié)束之后以回調(diào)方式執(zhí)行顾犹。為了能在p_requestCompleted方法執(zhí)行調(diào)用者所指定的塊倒庵,這段代碼需要把completion handler保存到實例變量里面。

//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

某個類可能會創(chuàng)建這種網(wǎng)絡(luò)數(shù)據(jù)獲取器對象炫刷,并用其從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

這里就造成了一個循環(huán)引用擎宝。因為completion handler塊要設(shè)置_fetchedData實例變量,所以它必須捕獲self變量(第37條)浑玛。這就是說绍申,handler塊保留了創(chuàng)建網(wǎng)絡(luò)數(shù)據(jù)獲取器的那個EOCClass實例。而EOCClass實例則通過strong實例變量保留了獲取器锄奢,最后失晴,獲取器對象又保留了handler塊剧腻。下圖描述了這個環(huán):

用塊引用其所屬對象時出現(xiàn)的循環(huán)引用

要打破循環(huán)引用也很容易:要么令_networkFetcher實例變量不要引用獲取器,要么令獲取器的completionHandler屬性不再持有handler塊涂屁。在網(wǎng)絡(luò)數(shù)據(jù)獲取器這個例子中书在,應該等completion handler塊執(zhí)行完畢后,再去打破引用環(huán)拆又,以便使獲取器對象在handler塊執(zhí)行期間保持存活狀態(tài)儒旬。比方說,completion handler塊的代碼可以這么修改:

[_networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"Request URL %@ finished",_networkFetcher.url);
        _fetchedData = data;
        _networkFetcher = nil;
    }];

如果設(shè)計API時用到了completion handler這樣的回調(diào)塊帖族,那么很容易形成循環(huán)引用栈源,所以必須意識到這個重要問題。一般來說竖般,只要適時清理掉環(huán)中的某個引用甚垦,即可解決此問題,然而涣雕,未必總有這種機會艰亮。在本例中,唯有completion handler運行過后挣郭,方能解除引用環(huán)迄埃。若是completion handler一直不運行,那么引用環(huán)就無法打破兑障,于是內(nèi)存就會泄露侄非。

像completion handler塊這種寫法,還可能引入另外一種形式的引用環(huán)流译。如果completion handler塊所引用的對象最終又引用了這個塊本身逞怨,那么就會出現(xiàn)引用環(huán)。比方說先蒋,我們修改下這個例子骇钦,使調(diào)用API的那段代碼無須在執(zhí)行期間保留指向網(wǎng)絡(luò)數(shù)據(jù)獲取器的引用,而是設(shè)定一套機制竞漾,令獲取器對象自己設(shè)法保持存活眯搭。要想保持存活,獲取器對象可以在啟動任務時把自己加到全局的collection中(比如用set來實現(xiàn)這個collection)业岁,待任務完成后鳞仙,再移除。而調(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ò)通信庫都采用這種方法笔时,因為假如令調(diào)用者自己來將獲取器對象保持存活的話棍好,他們會覺得麻煩。Twitter框架的TWRequest對象也用的這個辦法。然后借笙,本例這樣做會引入引用環(huán)扒怖。completion handler塊其實要通過獲取器對象來引用其中的URL(引用了EOCNetworkFetcher的url屬性)。于是业稼,塊就要保留獲取器盗痒,而獲取器反過來又經(jīng)由其completion handler屬性保留了這個塊。所幸要修復這個問題也不難低散「┑耍回想一下,獲取器對象之所以要把completion handler塊保存在屬性里面熔号,其唯一目的就是想稍后使用這個塊稽鞭。于是,獲取器一旦運行過completion handler之后引镊,就沒必要再保留它了朦蕴。所以,只需將p_requestCompleted方法按照如下方式修改即可:

-(void)p_requestCompleted{
    if(_completionHandler){
        _completionHandler(_downloadedData);
    }
    self.completionHandler = nil;
}

這樣一來祠乃,只要下載請求執(zhí)行完畢梦重,引用環(huán)就解除了兑燥,而獲取器對象也將會在必要時為系統(tǒng)所回收亮瓷。請注意,之所以要在start方法中把completion handler作為參數(shù)傳進去降瞳,這也是一條重要原因嘱支。假如把completion handler暴露為獲取器對象的公共屬性,那么就不便在執(zhí)行完下載請求之后直接將其清理掉了挣饥。因為既然已經(jīng)把handler作為屬性公布了除师,那就意味著調(diào)用者可以自由使用它,若是此時又在內(nèi)部將其清理掉的話扔枫,則會破壞“封裝語義”汛聚。在這種情況下要想打破引用環(huán),只有一個辦法可用短荐,那就是強迫調(diào)用者在handler代碼里自己把completionHandler屬性清理干凈倚舀。可這并不是十分合理忍宋,因為你無法假定調(diào)用者一定會這么做痕貌。

這兩種引用環(huán)都很容易發(fā)生。使用塊來編程時糠排,一不小心就會出現(xiàn)這種bug舵稠,反過來說,只要小心謹慎,這種問題也很容易解決哺徊。關(guān)鍵在于室琢,要想清楚塊可能會捕獲并保留哪些對象。如果這些對象又直接或間接保留了塊落追,那么就要考慮怎樣在適當?shù)臅r機解除引用環(huán)研乒。

要點:

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

轉(zhuǎn)載請注明出處:第六章 block與GCD(上)

參考:《Effective Objective-C 2.0》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谣膳,一起剝皮案震驚了整個濱河市竿报,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌继谚,老刑警劉巖烈菌,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異花履,居然都是意外死亡芽世,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門诡壁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來济瓢,“玉大人,你說我怎么就攤上這事妹卿⊥” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵夺克,是天一觀的道長箕宙。 經(jīng)常有香客問我,道長铺纽,這世上最難降的妖魔是什么柬帕? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮狡门,結(jié)果婚禮上陷寝,老公的妹妹穿的比我還像新娘。我一直安慰自己融撞,他們只是感情好盼铁,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著尝偎,像睡著了一般饶火。 火紅的嫁衣襯著肌膚如雪鹏控。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天肤寝,我揣著相機與錄音当辐,去河邊找鬼。 笑死鲤看,一個胖子當著我的面吹牛缘揪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播义桂,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼找筝,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了慷吊?” 一聲冷哼從身側(cè)響起袖裕,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎溉瓶,沒想到半個月后急鳄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡堰酿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年疾宏,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片触创。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡坎藐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嗅榕,到底是詐尸還是另有隱情顺饮,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布凌那,位于F島的核電站,受9級特大地震影響吟逝,放射性物質(zhì)發(fā)生泄漏帽蝶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一块攒、第九天 我趴在偏房一處隱蔽的房頂上張望励稳。 院中可真熱鬧,春花似錦囱井、人聲如沸驹尼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽新翎。三九已至程帕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間地啰,已是汗流浹背愁拭。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留亏吝,地道東北人岭埠。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像蔚鸥,于是被迫代替她去往敵國和親惜论。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,305評論 25 707
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法止喷,類相關(guān)的語法来涨,內(nèi)部類的語法,繼承相關(guān)的語法启盛,異常的語法蹦掐,線程的語...
    子非魚_t_閱讀 31,664評論 18 399
  • 凡是涉及到小貓小狗小動物題材的電影,如果不是讓人發(fā)笑的喜劇僵闯,便一定是能把人虐哭的正劇卧抗。 《一條狗的使命》便是這樣一...
    我的楊桐去哪兒啦閱讀 190評論 0 0
  • 納尼?30分鐘看完一本書双仍?有沒有搞錯向图? 如果是以前泳秀,我也不信¢剩看書這么神圣的事情嗜傅,不是應該打掃完房間,沏一壺茶檩赢,換...
    瀟灑君閱讀 408評論 0 0
  • 染發(fā)膏
    Yule0411閱讀 118評論 0 1