block能夠說是OC一項很是好用的功能。block的本質,其實是『帶有自動變量值的匿名函數』。可是在block的使用上,有各類江湖傳說,說在某某狀況下,block的使用是不安全的,會形成崩潰。因而也有不少面試題喜歡考察block。可是,實際的block的不安全使用,貌似除了循環引用,也沒遇到過什麼狀況啊?我敢說,block在現現在的iOS開發中,99%的崩潰都是由於你沒有給block判空。而其餘問題,都是由於循環引用。那麼block到底啥時候不安全呢?c++
其實關於block,咱們不用那麼懼怕。面試
首先,block的數據結構其實能夠經過查看源碼來得到。關於block的數據結構和runtime是開源的,能夠在llvm項目看到,或者下載蘋果的libclosure庫的源碼來看。蘋果也提供了在線的代碼查看方式,其中包含了不少示例和文檔說明。安全
因此,block真正的結構,就是這個樣子:數據結構
struct Block_descriptor_1 { uintptr_t reserved; uintptr_t size; }; struct Block_layout { void *isa; volatile int32_t flags; // contains ref count int32_t reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 *descriptor; // imported variables };
在objc中,根據對象的定義,凡是首地址是*isa的結構體指針,均可以認爲是對象(id)。這樣在objc中,block實際上就算是對象。app
那麼既然block是個對象,那麼block就應該有Class,那麼block的Class是什麼呢?svn
在block runtime中,定義了6種類:函數
_NSConcreteStackBlock 棧上建立的block測試
_NSConcreteMallocBlock 堆上建立的blockui
_NSConcreteGlobalBlock 做爲全局變量的blockspa
_NSConcreteWeakBlockVariable
_NSConcreteAutoBlock
_NSConcreteFinalizingBlock
其中咱們能接觸到的主要是前3種,後三種用於GC不作討論。
其實,這三種block類型的狀況很是好理解。
首先咱們要明確,在編譯完成後,block內部的代碼將會提取出來,成爲一個單獨的C函數。建立block時,實際就是在方法中聲明一個struct,而且初始化該struct的成員。而執行block時,就是調用那個單獨的C函數,並把該struct指針傳遞過去。block的的實際做用效果,至關於C語言中的匿名函數。
因而,就能夠理解_NSConcreteGlobalBlock
的使用了。由於全局block是當一個block內部沒有捕獲任何外部變量時,就會是一個全局block類型。此時,這個block與一個函數無異。因此,那麼它就應該有和函數同樣的靜態特性。並且,咱們在調用block的時候,其實和普通C函數的調用很類似,都是名稱加括號:block()
。
那麼有函數同樣靜態特性的block,顯然不須要再取考慮他的生命週期。
這個類型的block,是在編譯器發現block內部引用了外部變量後,會生成的block類型。
在block內部有引用外部變量時,當struct第一次被建立時,它是存在於該函數的棧幀上的,其Class是固定的_NSConcreteStackBlock。其捕獲的變量是會賦值到結構體的成員上,因此當block初始化完成後,捕獲到的變量不能更改。
當函數返回時,函數的棧幀被銷燬,這個block的內存也會被清除。因此在函數結束後仍然須要這個block時,就必須用Block_copy()方法將它拷貝到堆上。這個方法的核心動做很簡單:申請內存,將棧數據複製過去,將Class改一下,最後向捕獲到的對象發送retain,增長block的引用計數。詳細代碼能夠直接點這裏查看。
之因此這樣設計,實際上,能夠認爲成,當block有了外部變量的捕獲,那麼它就須要持有這個外部變量,就是賦值到結構體成員上。這種捕獲,形成了block對應struct結構體大小的動態變化,因此,在設計上適合放在棧上更合理。
在棧block中,說到過,當函數的棧幀的銷燬,那麼棧block也會被隨之清楚。可是咱們通常都須要在函數結束後仍然能使用這個block,因此,須要把棧block拷貝到堆上。在copy時,就把棧block的類型轉換成了堆block。
因此在MRC時代,block屬性的關鍵字必須是copy。這樣就能夠保證在給block屬性賦值的時候,能把在棧上的block給copy到堆區。
而講得再細一點,爲何非要把block放到堆區才安全。
由於你能夠這麼理解,block就是個匿名函數,只不過咱們給了一個變量來引用這個匿名函數,在須要的時候調用。可是,棧block會隨着函數棧幀的銷燬而銷燬,這樣一來,咱們用以前作引用的變量再去調用這麼一塊被銷燬的內存,就會出現內存崩潰。
因此,只有把block放到由咱們來控制生命週期的堆區中,才能安全地使用block。
咱們知道,在OC中,對象都會在堆區存儲。實際上,此時的堆block,它的確就是一個對象。並且,你還須要對它手動release。
固然,當ARC時代來臨,這就又有所不一樣了。
首先先看我寫的一段測試代碼:
這是在ARC下的Command line tools工程。
這段代碼中的str
是作常量區地址參考的,最後的obj
是作堆區地址參考的。
能夠看到str
的地址是0x1000021f0
,obj
的地址是0x100406b40
。的確符合預期,常量區地址很是小,堆區地址稍微大一些。
先看gBlock
。這個block沒有捕獲任何外部變量,僅僅是打印一句文字。因此,理所固然,它是一個全局block。經過地址的觀察,的確如此,比str
常量的地址還小。
再看mBlock
。這個block是捕獲了一個外部變量,打印一個外部聲明的字符串。這種狀況下,在MRC小中應該是屬於棧block。可是這裏的執行結果顯示,它實際是一個堆block。
實際上,這是由於在ARC下,對block作了大量處理。如今的狀況是,只要一個block被賦值給一個strong
變量,會自動copy。因此,咱們看到mBlock
這個地址,和參考對象obj
的地址很是接近。
這樣一來,實際上在ARC下,很難在寫出一個棧block的狀況,由於一旦有賦值給strong
變量,那麼就獲得的是堆block。因此,爲了寫出一個棧block,我使用一個函數,入參是block類型,可是這個block參數沒有通過任何一次賦值操做,直接放在了函數參數裏。因此,這樣就能夠再函數體內獲得這個棧block類型的參數。
經過地址,能夠看到,這個棧block的確地址很大。在給這個棧block進行一次手動copy後,block也變成了堆block。
不過,這裏雖然倒弄出來一個棧block,不過這種狀況是不會有棧block被提早釋放的問題。由於這個棧block做爲入參,他的生命週期自己也是跟隨這個函數的函數體。函數體棧幀釋放,block也被釋放,因爲函數外並無對block有引用,因此,這個block也能夠被安全的釋放。
網上不少關於block的資料,說使用block會形成崩潰,實際都是由於文章太老。現階段的block是很是安全的。並且LLVM編譯器的檢查也十分完善,能夠提早發現僅有的一些block被提早釋放的狀況。
因此,在ARC下,你能夠大膽地使用block,而不太須要在乎block自己的生命週期。由於他實際和咱們日常用的其餘NSObject對象的表現,並沒有二致。
另外歡迎訪問個人我的博客:http://suntao.me