一直在使用block,但殊不知道block是什麼。本篇文章用以學習並記錄。html
- Block的聲明
- Block的內部實現
- Block循環引用的理解
- block的類型,爲何要用copy修飾
聲明一個blockios
返回類型 (^名稱)(形參列表) = ^(形參列表) { 內容 } int (^addBlock)(int, int) = ^(int a, int b) { return (a + b); };
執行一個blockc++
addBlock(1, 2);
咱們先來寫一個簡單的block數據結構
// main.m #import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { void(^blockA)(void) = ^(void) { NSLog(@"1"); }; blockA(); return 0; }
經過clang的命令能夠將oc代碼轉換爲c++代碼
在命令行中進入到main.m文件目錄,並輸入:
(ps:該命令有相關參數,可在轉換時引入框架等功能,當命令執行報錯時能夠嘗試下,參數百度能搜獲得。)框架
clang -rewrite-objc main.m
同級目錄下會獲得一個main.cpp文件,打開它以後能夠看到很是很是多的內容,上面的一堆是Foundation框架轉換後的內容。咱們直接拖到最下面,能夠找到咱們的main函數。異步
爲方便理解,我加了註釋函數
1.1 咱們先來看main函數中的內容
咱們能夠看到,本來main函數中寫的block定義和執行的地方被轉換成告終構體和函數指針的調用。blockA被轉換成了2個結構體__main_block_impl_0、__main_block_desc_0,其匿名調用轉換成了靜態函數__main_block_func_0。
由此咱們能夠得知block本質上是結構體,而block的匿名調用本質上是靜態函數。源碼分析
1.2 再來看blockA轉換獲得的結構體__main_block_impl_0學習
// __main_block_impl_0 struct __main_block_impl_0 { struct __block_impl impl; // __block_impl是系統的block結構體,包含block的基礎信息 struct __main_block_desc_0* Desc; // block詳細信息 // 構造函數 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; // __block_impl,包含了block的基礎信息。其定義在main.cpp中搜索一下就能夠找到 struct __block_impl { void *isa; // 和全部oc對象同樣有一個isa指針,指向block的類型 int Flags; // 標識 int Reserved; // 保留值 void *FuncPtr; // 函數指針,指向block匿名調用對應的靜態函數 };
從結構體的定義咱們能夠知道:測試
1. __main_block_impl_0對應blockA的本體。2. 其中__block_impl類型的變量impl包含了每一個block結構體都有的基本屬性。這有點相似於面向對象的思想,相似於__main_block_impl_0繼承了__block_impl。
3. __block_impl中的isa指針,表示block其實是一個OC對象。類比NSObject的isa指針,相同的是它們都指向對象的前8字節。不一樣的是NSObject及派生類對象的isa指針指向Class的元類,而block的isa指針指向「block的類型」。
4. __block_impl中的函數指針FuncPtr,指向block的匿名調用對應的靜態函數。
5. 對於block的isa指針指向「block的類型」的解釋。block的類型有三種:_NSContreteGlobalBlock、_NSContreteStackBlock、_NSContreteMallocBlock,這三種類型都是由OC中的類__NSGlobalBlock__、__NSMallocBlock__、__NSStackBlock__轉換而來的,經過一個小測試咱們能夠看到這三種類型。
關於block的類型的區別後面會具體去解釋。
6. block匿名調用對應的靜態函數,其函數指針保存在FuncPtr中
1.3 那麼回過來再看main函數中的內容
main函數中的兩句代碼爲了方便理解我作了拆分。
block的定義和實現其實就是:經過構造函數建立了一個__main_block_impl_0類型的結構體變量,並將其首地址賦值給函數指針blockA。
block的執行其實就是:經過blockA這個指針去執行FuncPtr指向的靜態函數
擴展:
在探究過程當中發現上圖中的兩步強轉類型有些不太對勁。
// 構造__main_block_impl_0變量 __main_block_impl_0 block_impl_0 = __main_block_impl_0(fp, desc); // 強轉類型 void(*blockA)(void) = (void (*)())&block_impl_0; // 強轉類型 __block_impl *block_impl = (__block_impl *)blockA; 其實代碼合併起來至關於: __block_impl *block_impl = &block_impl_0;
問題是:__main_block_impl_0和__block_impl的類型都不一樣,爲何賦值後使用卻不會出錯。
不是應該這樣寫嗎?
__block_impl *block_impl = block_impl_0.impl;
後來忽然想到了緣由,這是由於內存分配順序的緣由。由於結構體__main_block_impl_0中impl的定義是寫在最前面的,因此該結構體變量在分配內存時會先從impl開始。而上述的block_impl指針指向block_impl_0的首地址,也就至關於恰好指向其中的impl。
好比:__main_block_impl_0的大小是100位,__block_impl的大小是50位,那麼使用block_impl指針時操做的是__main_block_impl_0裏的前50位內存地址,恰好是__main_block_impl_0裏面的impl。
若是將struct __block_impl impl定義寫在struct __main_block_desc_0* Desc後面則會出錯。
猜想這樣寫的緣由多是爲了代碼簡潔。= =!
首先來看一個簡單的例子
轉換成c++代碼後
2.1 咱們能夠看到普通變量a_int和a_number在block中的傳值過程:
1. 結構體__main_block_impl_0的構造函數接收變量a_int和a_number的值,且傳參形式爲值傳遞。
2. 結構體__main_block_impl_0中新增了與之對應的成員變量a_int和a_number用來保存這兩個值。
3. 靜態函數中使用的a_int和a_number是從新定義的局部變量,其值爲結構體中保存的值。
4. 在block中使用的變量實際上是靜態函數中的局部變量
2.2 經過上述的傳值過程,咱們應該可以明白兩個問題:
1.爲何block中使用的普通外部參數,外部修改其值不會影響到block內部?
這是由於在構造結構體__main_block_impl_0時,a_int和a_number是值傳遞。在外部修改a_int的值,或是修改a_number指針的指向,是不會影響到結構體中保存的值的,因此固然也沒法改變在靜態函數中使用時的值。2.爲何在block內部沒法修改外部變量的值?
咱們能夠得知,因爲是值傳遞,block內部a_int、a_number和外部的a_int、a_number並非同一個變量,因此在block內部是並不能獲取到外部的變量的,固然也不能在block內部修改他們的值。(ps:其實即便是真的在block內部修改a_int也應該能夠,只不過修改的是靜態函數內部的局部變量a_int,至於爲何編譯器設定這樣寫會報錯?我想多是爲了便於理解,防止數據紊亂吧。)
過程以下圖所示:
另外下圖能夠證明,block內部的變量和外部的變量並非同一個。
2.3 可是有幾種狀況下能夠在block中修改外部變量的值
1.全局變量
2.靜態變量
3.變量使用__block修飾
看看對應的實現代碼
轉換成C++代碼後:
從圖中能夠看出,用__block修飾的變量block_int、block_number被包裝成告終構體,這個結構體實際上是OC對象。
從圖中能夠看出,blockA對應的結構體中新增了3個指針變量static_int、block_int、block_number。
從圖中能夠看出,blockA匿名調用對應的靜態函數,裏面使用的block_int和block_number也都是從blockA中獲取的包裝變量。OC代碼中在block內部修改值至關於修改該封裝變量中保存的值。
圖中的__main_block_copy_0函數和__main_block_disopse_0是用來處理內存,相似於retain和release。__main_block_desc_0爲blockA的擴展信息結構體。
最後咱們在main函數中能夠看到,定義一個__block修飾的變量,實際上是定義了一個包裝它的結構體變量。在結構體__main_block_impl_0的構造函數傳參中,做爲參數傳遞的就是這個結構體,且傳的是地址。另外靜態變量static_int傳的是地址,全局變量因爲全局都能獲取到的,因此不用傳參。
經過上述的代碼咱們能夠知道爲何這三種變量能夠在block內部修改其值:
1. 全局變量因爲全局均可以獲取到並修改它的值,因此能夠在內部進行修改。而且在block中使用不會有任何特殊處理。
2. 使用靜態變量與普通變量相同的是都會在blockA結構體中有定義,不一樣的地方是傳遞的是地址而不是值,因此它能夠在內部進行修改。
3. 使用__block修飾的變量會被包裝成一個對象,且傳參時傳遞的是該包裝對象的引用。所以這個變量不論是在block內部仍是在block外部都是以包裝對象的形式存在,而且修改該變量的值其實修改的是包裝對象內部持有的該變量的值(ps:咱們寫代碼只能操做該值,操做不了包裝對象)。所以該變量在block內外都是同一個對象,因此能夠修改。
經過上述的探究過程咱們能夠了解到block的結構和實現以及使用各類類型變量的狀況,不過通常來講咱們也不太常會用到,可是瞭解這些可讓咱們明白爲何block會致使循環引用。
咱們先來了解一下什麼是循環引用:
因爲iOS系統下使用引用計數的方式來管理內存,因此一個對象是否須要被釋放是由引用計數是否爲0決定的,可是若是出現了A類的實例強引用B類的實例,B類的實例又強引用了A類的實例(這裏我簡寫爲:A->B->A),或者是相似A->B->C->A這種狀況,這樣就會出現閉環,這幾個實例的引用計數都將沒法變爲0,所以應用運行期間也將沒法被釋放。這就是循環引用,它會致使內存泄漏的問題(由於有一塊內存一直沒法釋放,而且除了他們之間相互引用的指針以外沒有其餘指針指向他們,因此也沒辦法再操做他們了)。
以下圖,閉環爲ak47->gamer->ak47
解決循環引用的方法就不詳細說了,只須要將閉環中某一個成員屬性用weak修飾便可打破循環。
那麼block爲何會致使循環引用呢?
首先咱們知道block是一個對象,經過上面對block內部結構的研究咱們還知道,一個不加修飾的對象在block內使用時,block會定義相應的成員變量,並使用外部變量對其賦值,這就至關於block強引用持有這些對象。因此在使用block時,只要出現A->block->B->...->A這種閉環的形式,就有可能因循環引用的問題致使內存泄漏。
以下圖:閉環爲self -> gun -> block -> self
可是以下圖就不會出現循環引用,由於沒有產生閉環。
持有關係爲gun -> block -> self
如何解決block的循環引用問題?
使用__weak打破閉環便可。
持有關係爲self -> gun -> block -X> self,由於block持有弱引用指針weakself不會改變其引用計數,所以打斷了block強引用持有self的關係。
那麼爲何咱們常常會看到與weakself成對出現的strongself呢?strongself有什麼做用?
由於若是是異步執行block,或者在block中有異步執行的代碼,那麼有可能會出如今block執行到其中某一句代碼時weakself會忽然變成nil。
以下圖,我將代碼寫到了一個控制器類中,當執行到[weakself handleFire]這句代碼時我將控制器pop,控制器將會被銷燬,在3秒後執行到[weakself peopleDead]這句代碼時能夠看到weakself變成了nil。
這就會產生一個問題,明明開了槍人卻沒死。類比一下,好比當你作某個本地存儲的功能,若是由於操做過快引發了了上述的情況,致使數據處理好了,狀態也更新了,可是數據沒有落庫,這種問題發生也仍是蠻可怕的。
因此說strongself其實就是確保了block執行時self一直是爲nil或一直有值,而不會出現前一半代碼self有值,後一半代碼self爲nil的狀況。
在說block的類型以前咱們先來了解一下內存的劃分:
1. 棧區(stack):由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。操做方式相似於數據結構中的棧。
2. 堆區(heap):通常經過代碼分配釋放,若未釋放,則程序結束時由系統回收。操做方式相似於鏈表。
3. 全局區(靜態區static):存儲全局變量和靜態變量,初始化的全局變量和靜態變量在一塊區域(.data),未初始化的全局變量和未初始化的靜態變量在相鄰的另外一塊區域(.bss)。程序結束後由系統釋放。
4. 文字常量區:常量字符串就是放在這裏的(.rodata)。程序結束後由系統釋放。
5. 程序代碼區:存放函數體的二進制代碼(.text)。
如今來看block,在上面的探究過程當中咱們知道了block實際上是對象,其類型有三種:
1. __NSGlobalBlock__:定義在.data區,block內部未使用任何外部變量。
咱們知道在block內部使用的外部變量會在block對應的結構體中有所定義,而全局類型的block內部沒有使用外部變量,不管如何執行都不依賴於執行狀態,所以定義在全局區。例如^(void) { }2. __NSStackBlock__:定義在棧區,使用了外部變量的block默認是建立在棧上的。
3. __NSMallocBlock__:定義在堆區,當block執行copy方法時會自動從棧拷貝到堆上。
那麼爲何block類型的成員變量須要用copy修飾呢?
1.咱們先來看一下block在MRC下的使用:
因爲block默認建立在棧上(此默認的說法先不考慮全局block的狀況),其生命週期即爲建立時所在方法的做用域,當方法執行完以後block就會被自動釋放,指向它的指針也都會變成野指針。若是想在超出block定義時的生命週期範圍以外使用,那麼須要執行block的copy方法將其複製到堆上。
以下圖,在MRC環境下沒有對block拷貝就直接返回,在block離開getABlock方法的做用域以後被釋放,main函數中的block指針變爲野指針,因此發生了崩潰。
以下圖,當執行了copy方法以後,block被拷貝到堆上,生命週期延長,程序正常執行。
另外再看一個例子:
Gun類的成員變量fireBlock使用了retain修飾,在load方法中將block賦值給fireBlock時並未執行copy方法,因此咱們能夠看到在賦值了以後block仍在棧上,load方法做用域結束block被釋放,因此main函數中使用fireBlock就會報野指針。
使用copy修飾,就不會出錯了。
結論:在MRC下對block類型的成員屬性修飾最好用copy,而不要用retain,因爲使用retain修飾只會改變引用計數而不會執行copy方法將block複製到堆上。此外block是一個對象就更不可能用assign修飾了。
(其實明白了block在內存中的存儲位置和規則,若是真的要較真的話在MRC下用retain也是能夠的,不過須要在block建立後調一下copy方法,若是是成員屬性須要重寫其set方法,並在set方法中調用copy,以達到將block複製到堆上的目的,對此我只能說何須呢)。
2.咱們再來看一下block在ARC下的使用:
頗有意思的是在ARC環境下,只要將block賦值就會自動拷貝到堆上。那麼ARC環境下什麼狀況block會被copy到堆上呢?
1.執行copy方法。
2.做爲方法返回值。
3.將Block賦值給非weak修飾的變量
4.做爲UsingBlock或者GCD的方法入參時。(暫沒法驗證)
例子以下:
結論:通常來講咱們使用block都是會有賦值操做的,因爲有上述條件的存在,因此基本上不會遇到在棧上的block的狀況。因此在ARC環境下,block類型的成員屬性使用strong或copy修飾其實均可以,可是爲了延續MRC的習慣,另外避免真的出現一些奇怪問題的狀況,一般仍是使用copy修飾。
總結:在MRC環境下須要用copy修飾,由於若是不對block執行copy操做,它在所在方法執行完成後會被釋放。但ARC環境下因爲有內部機制因此能夠免去麻煩,但延續習慣也用copy修飾。
深究Block的實現
iOS Block源碼分析系列(三)————隱藏的三種Block本體以及爲何要使用copy修飾符
深刻研究Block用weakSelf、strongSelf、@weakify、@strongify解決循環引用