許莉鑫 金海佳 李瑪 田英愛(ài)
北京信息科技大學(xué)計(jì)算機(jī)學(xué)院
Block分析
許莉鑫 金海佳 李瑪 田英愛(ài)
北京信息科技大學(xué)計(jì)算機(jī)學(xué)院
Block是蘋(píng)果公司在iOS4后引入的對(duì)C語(yǔ)言的擴(kuò)展。把Block的功能概括來(lái)說(shuō),即帶有自動(dòng)變量(即局部變量)的匿名函數(shù)指針。本文將對(duì)Block在這幾個(gè)方面進(jìn)行解讀:一、Block的語(yǔ)法。二、Block作為函數(shù)參數(shù)使用的方法。三、Block對(duì)自動(dòng)變量的截獲。四、__Block說(shuō)明符、存儲(chǔ)域。五、循環(huán)引用導(dǎo)致內(nèi)存泄漏的問(wèn)題。本文旨在使閱讀者深入認(rèn)識(shí)Block并更好地使用Block。
Block Objective-C iOS
在編程中閉包是非常常見(jiàn)的一種技術(shù)手段,在Objective-C中被稱(chēng)做Block。Block因其簡(jiǎn)潔的語(yǔ)法,特殊的存儲(chǔ)方式,被廣泛地使用在Objective-C工程中。很好地使用Block并不簡(jiǎn)單,本文將針對(duì)Block進(jìn)行深入分析。
Block本質(zhì)是一個(gè)函數(shù)指針,它的使用方法和C語(yǔ)言函數(shù)指針一樣,可以傳入?yún)?shù),且有返回值。但和函數(shù)指針相比,Block功能更強(qiáng)大,所以Block也復(fù)雜很多,它與函數(shù)指針的區(qū)別主要表現(xiàn)在以下方面:語(yǔ)法上存在區(qū)別、Block是一個(gè)匿名指針、Block會(huì)截獲自動(dòng)變量、內(nèi)存管理與釋放的區(qū)別。
2.1 聲明Block
在C語(yǔ)言中,可以將一個(gè)函數(shù)的地址賦值給函數(shù)指針類(lèi)型變量中,形式如:
int functionName(int count){
return count;
因?yàn)锽lock本質(zhì)是一個(gè)匿名函數(shù)指針,所以聲明一個(gè)Block和C語(yǔ)言中聲明函數(shù)指針十分類(lèi)似,形式如:
與C語(yǔ)言中聲明函數(shù)指針相比,聲明Block的區(qū)別即將“*”替換成。
Block類(lèi)型變量和一般的C語(yǔ)言變量的使用方法完全相同,它可作為自動(dòng)變量、函數(shù)參數(shù)、靜態(tài)變量、靜態(tài)全局變量、全局變量等使用。
2.2 對(duì)Block賦值
形式如:
“^”符號(hào)表明這是一個(gè)Block,“^”后的括號(hào)中包含著參數(shù),花括號(hào)中可以進(jìn)行一些操作,并根據(jù)需要在確定時(shí)候返回。
2.3 使用Block
可以像使用一個(gè)C語(yǔ)言函數(shù)一樣來(lái)使用Block:
int count = blo(10);
Block比C語(yǔ)言中的函數(shù)強(qiáng)大,比如Block可以作為函數(shù)參數(shù)??梢杂靡韵路绞铰暶饕粋€(gè)Objective-C的方法:
然后以以下方式調(diào)用這個(gè)方法:
這里hander變成了回調(diào),事實(shí)上Apple的大量api接口也是這么設(shè)計(jì)的。在functionName方法中也許進(jìn)行了大量的計(jì)算,開(kāi)辟了很多線程,等待了很長(zhǎng)的時(shí)間,但所有這些復(fù)雜的過(guò)程對(duì)于用戶(hù)(方法的使用者)來(lái)說(shuō)都是不關(guān)心的,用戶(hù)關(guān)心的只有在hander中返回的“count”參數(shù)。
這個(gè)方法可以被寫(xiě)得更加漂亮,即添加一個(gè)Block類(lèi)型變量,這其中用到C語(yǔ)言中的typedef。
typedef void(blo)(int count);
上例給帶有“count”參數(shù)的閉包起了一個(gè)blo的別名,所以在接下來(lái)的函數(shù)聲明中就可以使用blo來(lái)代替原本的參數(shù)類(lèi)型,如下:
-(void)functionName:(blo)hander;
以以下代碼為例,
此例中,blo();執(zhí)行時(shí)控制臺(tái)將輸出“I am Eric”,即便name代表的字符串在Block后已被修改成“I am Strong”。這就是Block對(duì)自動(dòng)變量的截獲,簡(jiǎn)單來(lái)說(shuō),Block對(duì)自動(dòng)變量的截獲是指在編譯Block時(shí),Block會(huì)保存(截獲)其中使用到的變量,不論Block中的變量的值在其后的語(yǔ)句中是否會(huì)被修改,Block中記錄的該變量的值永遠(yuǎn)不會(huì)改變。
Block對(duì)自動(dòng)變量的截獲只能用于獲取變量的值,而不能對(duì)其進(jìn)行更改。當(dāng)嘗試去更改截獲的自動(dòng)變量值的時(shí)候,編譯器將報(bào)錯(cuò)。例如下面這種情況,
此時(shí),編譯器會(huì)報(bào)出以下錯(cuò)誤:
Variable is not assignable (missing __block type specifier)
這個(gè)錯(cuò)誤提示我們,若想在Block中修改截獲的自動(dòng)變量的值,則需給變量加上“__Block”修飾符,如下所示,
使用附有__Block說(shuō)明符的自動(dòng)變量可以在Block中賦值,該變量稱(chēng)為_(kāi)_block變量。
再舉一例,
上例在Block中對(duì)arr變量進(jìn)行了初始化的賦值操作,執(zhí)行會(huì)發(fā)生錯(cuò)誤,同樣需要給arr變量加__block修飾符來(lái)解決。
但不是所有在Block中變更的對(duì)象都需要加上__Block說(shuō)明符。如果在Block中僅對(duì)OC對(duì)象進(jìn)行操作,而不對(duì)其進(jìn)行賦值,這樣的變更就不會(huì)報(bào)錯(cuò),故無(wú)需加上__Block說(shuō)明符。例如,
此例截獲的變量是一個(gè)NSMutableArray類(lèi)型的變量,Block中對(duì)一個(gè)可變數(shù)組進(jìn)行了操作,而沒(méi)有進(jìn)行賦值,所以可以正常執(zhí)行。
用C語(yǔ)言指針來(lái)解釋以上情形,即未附有__Block說(shuō)明符的自動(dòng)變量不能在Block中更改變量指針的指向,但可以對(duì)變量進(jìn)行操作(改變地址內(nèi)容)。
談到C語(yǔ)言指針,還要注意在Block中對(duì)C語(yǔ)言數(shù)組的使用方法。例如:
執(zhí)行上面這段代碼,編譯器會(huì)發(fā)出以下錯(cuò)誤:
Cannot refer to declaration with an array type inside block
Implicit conversion of an Objective-C pointer to ‘const char *’ is disallowed with ARC
這是因?yàn)樵诂F(xiàn)在的Block中,截獲自動(dòng)變量的方法沒(méi)有實(shí)現(xiàn)對(duì)C語(yǔ)言的截獲。對(duì)于這個(gè)問(wèn)題,可以使用指針來(lái)解決,如下:
存儲(chǔ)域一共分為三種:_NSConcreteStackBlock、_ NSConcreteGlobalBlock、_NSConcreteMallocBlock。即“棧存儲(chǔ)域”、“全局存儲(chǔ)域”、“堆存儲(chǔ)域”。Block與OC變量不同,它不全存儲(chǔ)在“棧存儲(chǔ)域”。
a.Block存儲(chǔ)在“全局作用域”中的情況:
如上所示,當(dāng)我們聲明一個(gè)全局的Block,Block就將被存儲(chǔ)在_NSConcreteGlobalBlock中。因?yàn)檫@種情況下在Block中無(wú)法對(duì)自動(dòng)變量進(jìn)行截獲,即Block的內(nèi)容不依賴(lài)于運(yùn)行時(shí)的狀態(tài),因此將Block放在“全局作用域”中是最合適的。事實(shí)上,只要Block的內(nèi)容不依賴(lài)于運(yùn)行時(shí)的狀態(tài),也就是不對(duì)自動(dòng)變量進(jìn)行截獲,那么不管Block的聲明實(shí)現(xiàn)位置在哪,這個(gè)Block都將被存儲(chǔ)在“全局作用域”當(dāng)中。
b.Block存儲(chǔ)在“堆作用域”中的情況:
當(dāng)將Block作為回調(diào)使用時(shí),可以發(fā)現(xiàn)當(dāng)Block超出了塊作用域時(shí)仍可以被使用,例如在網(wǎng)絡(luò)回調(diào)中:
我們經(jīng)常會(huì)使用類(lèi)似上面這種方式進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求,在Block中對(duì)請(qǐng)求返回?cái)?shù)據(jù)進(jìn)行處理。由于網(wǎng)絡(luò)請(qǐng)求是一個(gè)異步過(guò)程,所以在請(qǐng)求返回之后,已經(jīng)超出了Block的作用域。之所以這種情況下Block仍可以被使用,是因?yàn)檫@種情況下Block將被復(fù)制在“堆存儲(chǔ)域”中,包括Block中的自動(dòng)變量也將會(huì)被拷貝到堆存儲(chǔ)域當(dāng)中。
還有一種情況是當(dāng)將Block作為函數(shù)返回值返回時(shí),Block同樣會(huì)被拷貝到“堆存儲(chǔ)域”中,再來(lái)進(jìn)行返回。
大多數(shù)情況下,XCode(IDE)會(huì)自動(dòng)幫編程者判斷Block在什么情況下需要被拷貝到“堆存儲(chǔ)域”中,但是在某些情況下編程者需要手動(dòng)進(jìn)行這個(gè)過(guò)程,使用“copy”命令把Block從“?!笨截惖健岸选敝?。
前文提到Block在引用自動(dòng)變量時(shí)將把變量從棧中拷貝到堆中,所以,比如當(dāng)拷貝__strong屬性變量時(shí),十分容易引起循環(huán)引用,進(jìn)而造成內(nèi)存泄漏。下面這段代碼就會(huì)引起循環(huán)引用:
其中ViewController持有一個(gè)變量Block,但在Block中再次截獲了self,也就是Block持有self,ViewController的釋放需要Block來(lái)釋放self,而B(niǎo)lock同樣需要ViewController釋放才會(huì)釋放,這是標(biāo)準(zhǔn)的循環(huán)引用。解決這個(gè)問(wèn)題可以使用__weak說(shuō)明符,如下:
當(dāng)使用__weak說(shuō)明符后,Block不再持有self,于是打破了循環(huán)引用。
事實(shí)上并不是在Block中顯示的出現(xiàn)self以后才會(huì)發(fā)生循環(huán)引用,下面這種情況也會(huì)發(fā)生循環(huán)引用:
上例Block中沒(méi)有出現(xiàn)self,但在這種情況下也會(huì)發(fā)生循環(huán)引用。原因是這種情況雖然沒(méi)有使用get方法來(lái)獲取變量,但是直接通過(guò)內(nèi)存地址獲取了變量,等同于以下代碼:
這解釋了為什么第二種情況也會(huì)發(fā)生循環(huán)應(yīng)用。解決這樣的循環(huán)引用,同樣可以使用__weak說(shuō)明符:
需要注意的是,如果一個(gè)Block在運(yùn)行時(shí)沒(méi)有被調(diào)用,但是在Block中發(fā)生了循環(huán)引用,就也會(huì)發(fā)生內(nèi)存泄漏。原因是Block將自動(dòng)變量拷貝到“堆存儲(chǔ)域”的動(dòng)作是在編譯時(shí)期完成的,所以即便沒(méi)有調(diào)用Block,XCode也已經(jīng)在編譯時(shí)期將自動(dòng)變量拷貝到了“堆存儲(chǔ)域”當(dāng)中。
解決Block的循環(huán)引用問(wèn)題的方法除以上所述使用__weak說(shuō)明符外,還有另外一種方式。為了解決循環(huán)引用我們必須打破雙方其中一方的引用,所以上例中使用了__weak說(shuō)明符,但下面的代碼也可以達(dá)到相同的目的:
以上代碼中聲明了一個(gè)名為myObject的類(lèi),這個(gè)類(lèi)中的Block發(fā)生了循環(huán)引用,如果聲明了這個(gè)類(lèi)的一個(gè)實(shí)列對(duì)象,那么這么對(duì)象因?yàn)檠h(huán)引用而不會(huì)被釋放。
如上,當(dāng)聲明一個(gè)Object的myObject類(lèi)后,Object就已經(jīng)發(fā)生了內(nèi)存泄漏,但是如果在合適的位置來(lái)釋放Block就可以解決這個(gè)問(wèn)題:
如上,當(dāng)將Block置空以后,block就失去了對(duì)Object的引用,所以這種情況不會(huì)再發(fā)生循環(huán)引用。但這樣直接將Block置空的方式是十分危險(xiǎn)的,因?yàn)楦淖兞薆lock初始化的值,后面的代碼運(yùn)行結(jié)果就可能不同于所預(yù)料的了。所以選擇置空Block的時(shí)刻非常關(guān)鍵。
本文從Block的語(yǔ)法,Block作為函數(shù)參數(shù)使用的方法,Block對(duì)自動(dòng)變量的截獲,Block的使用方式:__Block說(shuō)明符、存儲(chǔ)域這些方面全面介紹了Block,并針對(duì)因循環(huán)引用導(dǎo)致內(nèi)存泄漏的問(wèn)題提出了解決辦法。通過(guò)本文幫助讀者深入認(rèn)識(shí)Block并更好地理解Block。
[1] Kazuki Sakamoto,Tomohiko Furumoto.Objective-C 高級(jí)編程.人民郵電出版社,2013-06-01
本項(xiàng)目由北京信息科技大學(xué)2016年人才培養(yǎng)質(zhì)量提高經(jīng)費(fèi)(5111610800)支持。