Block的理解與研究

前言

    一直在使用block,但殊不知道block是什麼。本篇文章用以學習並記錄。html


目錄

  • Block的聲明
  • Block的內部實現
  • Block循環引用的理解
  • block的類型,爲何要用copy修飾

Block的聲明

聲明一個blockios

返回類型 (^名稱)(形參列表) = ^(形參列表) {
    內容
}

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

clipboard.png

執行一個blockc++

addBlock(1, 2);

Block的內部實現

1.簡單block內部實現

咱們先來寫一個簡單的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函數。異步

爲方便理解,我加了註釋函數

clipboard.png

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__轉換而來的,經過一個小測試咱們能夠看到這三種類型。

clipboard.png

關於block的類型的區別後面會具體去解釋。

6. block匿名調用對應的靜態函數,其函數指針保存在FuncPtr中

1.3 那麼回過來再看main函數中的內容
main函數中的兩句代碼爲了方便理解我作了拆分。

clipboard.png

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後面則會出錯。
猜想這樣寫的緣由多是爲了代碼簡潔。= =!

2.block中使用外部參數狀況的內部實現

首先來看一個簡單的例子
clipboard.png

轉換成c++代碼後
clipboard.png
clipboard.png

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,至於爲何編譯器設定這樣寫會報錯?我想多是爲了便於理解,防止數據紊亂吧。)

過程以下圖所示:
clipboard.png
clipboard.png

另外下圖能夠證明,block內部的變量和外部的變量並非同一個。
clipboard.png
clipboard.png

2.3 可是有幾種狀況下能夠在block中修改外部變量的值

1.全局變量
2.靜態變量
3.變量使用__block修飾

看看對應的實現代碼

clipboard.png

轉換成C++代碼後:

clipboard.png
從圖中能夠看出,用__block修飾的變量block_int、block_number被包裝成告終構體,這個結構體實際上是OC對象。

clipboard.png
從圖中能夠看出,blockA對應的結構體中新增了3個指針變量static_int、block_int、block_number。

clipboard.png
從圖中能夠看出,blockA匿名調用對應的靜態函數,裏面使用的block_int和block_number也都是從blockA中獲取的包裝變量。OC代碼中在block內部修改值至關於修改該封裝變量中保存的值。

clipboard.png
圖中的__main_block_copy_0函數和__main_block_disopse_0是用來處理內存,相似於retain和release。__main_block_desc_0爲blockA的擴展信息結構體。

clipboard.png
最後咱們在main函數中能夠看到,定義一個__block修飾的變量,實際上是定義了一個包裝它的結構體變量。在結構體__main_block_impl_0的構造函數傳參中,做爲參數傳遞的就是這個結構體,且傳的是地址。另外靜態變量static_int傳的是地址,全局變量因爲全局都能獲取到的,因此不用傳參。

經過上述的代碼咱們能夠知道爲何這三種變量能夠在block內部修改其值:

1. 全局變量因爲全局均可以獲取到並修改它的值,因此能夠在內部進行修改。而且在block中使用不會有任何特殊處理。
2. 使用靜態變量與普通變量相同的是都會在blockA結構體中有定義,不一樣的地方是傳遞的是地址而不是值,因此它能夠在內部進行修改。
3. 使用__block修飾的變量會被包裝成一個對象,且傳參時傳遞的是該包裝對象的引用。所以這個變量不論是在block內部仍是在block外部都是以包裝對象的形式存在,而且修改該變量的值其實修改的是包裝對象內部持有的該變量的值(ps:咱們寫代碼只能操做該值,操做不了包裝對象)。所以該變量在block內外都是同一個對象,因此能夠修改。

Block循環引用的理解

經過上述的探究過程咱們能夠了解到block的結構和實現以及使用各類類型變量的狀況,不過通常來講咱們也不太常會用到,可是瞭解這些可讓咱們明白爲何block會致使循環引用。

咱們先來了解一下什麼是循環引用:
因爲iOS系統下使用引用計數的方式來管理內存,因此一個對象是否須要被釋放是由引用計數是否爲0決定的,可是若是出現了A類的實例強引用B類的實例,B類的實例又強引用了A類的實例(這裏我簡寫爲:A->B->A),或者是相似A->B->C->A這種狀況,這樣就會出現閉環,這幾個實例的引用計數都將沒法變爲0,所以應用運行期間也將沒法被釋放。這就是循環引用,它會致使內存泄漏的問題(由於有一塊內存一直沒法釋放,而且除了他們之間相互引用的指針以外沒有其餘指針指向他們,因此也沒辦法再操做他們了)。

以下圖,閉環爲ak47->gamer->ak47
clipboard.png

解決循環引用的方法就不詳細說了,只須要將閉環中某一個成員屬性用weak修飾便可打破循環。

那麼block爲何會致使循環引用呢?
首先咱們知道block是一個對象,經過上面對block內部結構的研究咱們還知道,一個不加修飾的對象在block內使用時,block會定義相應的成員變量,並使用外部變量對其賦值,這就至關於block強引用持有這些對象。因此在使用block時,只要出現A->block->B->...->A這種閉環的形式,就有可能因循環引用的問題致使內存泄漏。

以下圖:閉環爲self -> gun -> block -> self
clipboard.png

可是以下圖就不會出現循環引用,由於沒有產生閉環。
持有關係爲gun -> block -> self
clipboard.png

如何解決block的循環引用問題?
使用__weak打破閉環便可。
持有關係爲self -> gun -> block -X> self,由於block持有弱引用指針weakself不會改變其引用計數,所以打斷了block強引用持有self的關係。
clipboard.png

那麼爲何咱們常常會看到與weakself成對出現的strongself呢?strongself有什麼做用?
由於若是是異步執行block,或者在block中有異步執行的代碼,那麼有可能會出如今block執行到其中某一句代碼時weakself會忽然變成nil。

以下圖,我將代碼寫到了一個控制器類中,當執行到[weakself handleFire]這句代碼時我將控制器pop,控制器將會被銷燬,在3秒後執行到[weakself peopleDead]這句代碼時能夠看到weakself變成了nil。
clipboard.png

這就會產生一個問題,明明開了槍人卻沒死。類比一下,好比當你作某個本地存儲的功能,若是由於操做過快引發了了上述的情況,致使數據處理好了,狀態也更新了,可是數據沒有落庫,這種問題發生也仍是蠻可怕的。

因此說strongself其實就是確保了block執行時self一直是爲nil或一直有值,而不會出現前一半代碼self有值,後一半代碼self爲nil的狀況。

block的類型,爲何要用copy修飾

在說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指針變爲野指針,因此發生了崩潰。
clipboard.png
以下圖,當執行了copy方法以後,block被拷貝到堆上,生命週期延長,程序正常執行。
clipboard.png

另外再看一個例子:
Gun類的成員變量fireBlock使用了retain修飾,在load方法中將block賦值給fireBlock時並未執行copy方法,因此咱們能夠看到在賦值了以後block仍在棧上,load方法做用域結束block被釋放,因此main函數中使用fireBlock就會報野指針。
clipboard.png

使用copy修飾,就不會出錯了。
clipboard.png

結論:在MRC下對block類型的成員屬性修飾最好用copy,而不要用retain,因爲使用retain修飾只會改變引用計數而不會執行copy方法將block複製到堆上。此外block是一個對象就更不可能用assign修飾了。

(其實明白了block在內存中的存儲位置和規則,若是真的要較真的話在MRC下用retain也是能夠的,不過須要在block建立後調一下copy方法,若是是成員屬性須要重寫其set方法,並在set方法中調用copy,以達到將block複製到堆上的目的,對此我只能說何須呢)。
clipboard.png

2.咱們再來看一下block在ARC下的使用:

clipboard.png

頗有意思的是在ARC環境下,只要將block賦值就會自動拷貝到堆上。那麼ARC環境下什麼狀況block會被copy到堆上呢?

1.執行copy方法。
2.做爲方法返回值。
3.將Block賦值給非weak修飾的變量
4.做爲UsingBlock或者GCD的方法入參時。(暫沒法驗證)

例子以下:
clipboard.png

結論:通常來講咱們使用block都是會有賦值操做的,因爲有上述條件的存在,因此基本上不會遇到在棧上的block的狀況。因此在ARC環境下,block類型的成員屬性使用strong或copy修飾其實均可以,可是爲了延續MRC的習慣,另外避免真的出現一些奇怪問題的狀況,一般仍是使用copy修飾。

總結:在MRC環境下須要用copy修飾,由於若是不對block執行copy操做,它在所在方法執行完成後會被釋放。但ARC環境下因爲有內部機制因此能夠免去麻煩,但延續習慣也用copy修飾。


參考文章

深究Block的實現
iOS Block源碼分析系列(三)————隱藏的三種Block本體以及爲何要使用copy修飾符
深刻研究Block用weakSelf、strongSelf、@weakify、@strongify解決循環引用

相關文章
相關標籤/搜索