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