探索 Block (一) (手把手講解Block 底層實現原理)

  前言 html

  要探索Block前先說一下我對Block的理解,我把它理解爲:可以捕獲它所在函數內部的變量的函數指針、匿名函數或者閉包。注意紅色部份說的是它的精髓所在。但願看我這篇文章的人可以跟我說的步驟去作,作起來也比較簡單,基本上是手把手,這樣會有更好的效果,更深入,固然若是隻看文章就可以讓讀者明白,那是我更加但願的。ios


  1、首先,咱們準備一個.m文件。我這裏是main.m。內容以下:objective-c

int main(int argc, char * argv[]) {
    void (^test)() = ^(){
    };
    test();
}

  接下來我要用到一個命令clang src.m -rewrite-objc -o dest.cpp.這個意思是用clang編譯器對源文件src.m中的objective-c代碼轉換成C代碼放在dest.cpp文件。其實xode編譯時也會幫咱們轉換。咱們這樣就能夠dest.cpp在看到咱們定義和調用的block轉換成C是怎麼樣的。執行命令後查看這個dest.cpp會發現有一大堆代碼。下面我把對咱們有用並可以說清楚原理的關鍵貼上來並加以註釋:閉包

//__block_imp: 這個是編譯器給咱們生成的結構體,每個block都會用到這個結構體
struct
__block_impl { void *isa;         //對於本文能夠忽略 int Flags;         //對於本文能夠忽略 int Reserved;       //對於本文能夠忽略        void *FuncPtr;       //函數指針,這個會指向編譯器給咱們生成的下面的靜態函數__main_block_func_0 };


/*__main_block_impl_0:
是編譯器給咱們在main函數中定義的block

void (^test)() = ^(){
};
生成的對應的結構體
*/
struct __main_block_impl_0 {
struct __block_impl impl;         //__block_impl 變量impl struct __main_block_desc_0* Desc;    //__main_block_desc_0 指針,指向編譯器給咱們生成的結構體變量__main_block_desc_0_DATA __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {  //結構體的構造函數
    impl.isa = &_NSConcreteStackBlock; //說明block是棧blockimpl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;}};
 
 
//__main_block_func_0: 編譯器根據block代碼生成的全局態函數,會被賦值給impl.FuncPtr
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    }

//__main_block_desc_0: 編譯器根據block代碼生成的block描述,主要是記錄下__main_block_impl_0結構體大小
static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; //這裏就生成了__main_block_desc_0的變量__main_block_desc_0_DATA

//這裏就是main函數了
int main(int argc, char * argv[]) {
    void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); //下面單獨講
    ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);                          //下面單獨講
}

      what the hell is that!!!! 沒錯,這也是我一開始看到這堆東西的感覺。由於不少人講Block原理都貼這個並且沒有註釋或不多註釋,我也從網上搜出來看了幾個。接下來就要說明白這堆代碼。必定要有耐心,首先,對着上面代碼註釋過幾遍main函數前系統給咱們生成的這些結構體函數之間的關係,若是一次能明白天然是好,過了幾遍都沒明白也不要緊,但若是不明必定回頭要再理清。svn

     迴歸一開始我對block的理解,先忽略它可以捕獲所在函數內部的變量,那麼它就是一個函數指。函數

void (^test)() = ^(){
    };
就對應着
void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
這個總的來講就是定義一個函數指針指向一個地址,可是這個地址並非我樣日常的函數的入口地址

 轉換後代碼的要一段段從後往前組合分析:學習

__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))就是建立了一個__main_block_impl_0結構體的一個實例

&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))取這個實例的地址

((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))把實例地址強轉爲一個函數地址

void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
那麼這整句就是說定義一個函數指針指向一個新建立的__main_block_impl_0實例的地址。注意建立這個實例時構選函數傳的兩個參數,
正是編譯器幫咱們生成的靜態函數__main_block_func_0及__main_block_desc_0的變量__main_block_desc_0_DATA
test();
對應着
((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
總的來講意思就是經過函數指針test調用函數FnucPtr,傳的參數爲指針test自己。

雖然可以理解這句的意思,但這裏有點隱晦,仍是要進行說明一下
一、調用時不是應該這樣調纔對嗎 test(test它指向__main_block_impl_0)->impl.FuncPtr,其實它跟((__block_impl *)test)->FuncPtr)是同等做用。

 二、FuncPtr(即__main_block_func_0)的參數類型不是__main_block_impl_0 *,爲何clang編譯出來後是__block_impl*。其實這裏無論類型是什麼,它仍是傳了test做爲參數進去,所是不會有錯的。spa

      好了講到這裏,就能夠進行一箇中途簡單性的總結:忽略中間的複雜分支,留下主線,當咱們聲明一個block變量a併爲它賦值時,其實就是建立一個函數指針ptrA,再根據block a賦值的代碼生成一個靜態函數,而指針ptrA就指向這個靜態函數。block a調用時就是使用函數指ptrA調用生成的靜態函數。指針

講到這裏第一部分就結束了,接下來進行第二部分。code


 

  2、這部分就要開始講精髓的部分,捕獲它所在函數內部的變量,接下來的部分都不會像第一部分那樣寫那麼詳細的註釋,只會在關鍵和不同的地方加上註釋。而且經過觀看不一樣變化,從實踐中得出結論並明白它實現原理。即然要說捕獲它所在函數內部的變量,那麼接下來咱們就把main.m修改一下,加個變量(基本類型變量)唄。代碼變成這樣:

int main(int argc, char * argv[]) {
    int value = 1;
    void (^test)() = ^(){
        int valueTest = value;
    };
    test();
}

  那麼通過clang轉換以後會變成這樣, 與第一部份不同的地方我會把它變成粗體,仔細對比第一部分並思考,應該不難理解。

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int value;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int value = __cself->value; // bound by copy
        int valueTest = value;
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
    int value = 1; void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, value));
    ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
}

  首先咱們能夠看到的變化點有:一、__main_block_impl_0結構體中多了個value,其實它就是用來保存main函數中的value,還有它的構造函數多了一個參數二、__main_block_func_0這個函數的實現會新增一個變量value並被賦值。

從對比中咱們能夠知道,變量實際上是在構造__main_block_impl_0實例時傳進去了並被保存,當回調時經過把test(其實就是指向一個__main_block_impl_0實例)做爲參數傳進來,經過它拿到了變量。這樣就實現了捕獲局部變量。當block要捕獲多個變量時會是怎麼的呢?其實不難猜,有N個變量要被捕__main_block_impl_0結構體中就會有N個變量用於保存,它的構造函數就會有N個參數是用來傳這N個變量進來保存。回調時經過test(指向__main_block_impl_0實例)一一拿到。這裏就不貼代碼了,有興趣能夠本身驗證一下。

   原來捕獲函數內部變量其實就是這樣實現的呀。有了上面的基礎,你是否會想那麼__block修飾的變量是怎麼樣的?變量是個NSObject對象是怎麼樣的?

回調傳參又是怎麼樣的?還有人們常常說的對self的引用什麼的會是怎麼樣?接下來就進入第三部分。有了前面兩部份的基礎,後面的基本就是看代碼得結論,會少不少文字說明了。因此前面兩部份必定要理解好。


      3、 在寫這部份前想着,不就像前面同樣用clang一下,對比一下代碼就能夠知道了嗎。先來簡單一點的。

(1)帶有參數和返回值的block.

把main.m改爲這樣

int main(int argc, char * argv[]) {
    int (^test)(int a) = ^(int a){
        return a;
    };
    test(1);
}

  接着它轉換後的:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __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;
  }
};
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int a) {
        return a;
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {

    int (*test)(int a) = ((int (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((int (*)(__block_impl *, int))((__block_impl *)test)->FuncPtr)((__block_impl *)test, 1);
}

    這裏應該沒什麼難度,不難理解,就是__main_block_func_0函數多了個參數和返回值,就不細說了。

(2)加上了__block修飾符的基本變量時:

把main.m代碼改爲這樣:

int main(int argc, char * argv[]) {
    __block int value = 1;
    void (^test)() = ^(){
        value = 2;
    };
    test();
    int value1 = value;
}

轉換後就變成:(接下來會稍微有點複雜,沒關係,只要耐心點也是能夠明白的)

 
 

//這個是導出的一些接口,用於管理__block變量value內存的一些接口

extern "C" __declspec(dllexport) void _Block_object_assign(void *, const void *, const int);

extern "C" __declspec(dllexport) void _Block_object_dispose(const void *, const int);


//根據帶__block修飾符的變量value,編譯器給咱們生成了個結構體
struct
__Block_byref_value_0 { void *__isa; __Block_byref_value_0 *__forwarding; //這個會指向被建立出來的__Block_byref_value_0實例
int __flags; int __size; int value; }; struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_value_0 *value;  //保存__Block_byref_value_0變量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_value_0 *value = __cself->value; // bound by ref
 (value->__forwarding->value) = 2;
    }

//這兩個函數分別會在test block 被拷貝到堆和釋構時調用的,做用是對
__Block_byref_value_0實例的內存進行管理,至於怎麼管理,這裏就不討論了,這裏就會調用上面導出來的接口。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);} static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); //回調函數指針,會被賦值爲__main_block_copy_0

void (*dispose)(struct __main_block_impl_0*);            //回調函數指針,會被賦值爲__main_block_dispose_0
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; /*{ 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0},這句就是建立一個例的意思,這是結構體的一種構造方式。*/

int main(int argc, char * argv[]) {
/*咱們定義的__block int value轉換後並非一個簡單的棧變量,而會是新建的
__Block_byref_value_0堆變量*/

     __attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 1};

void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
  
//最後面使這句int value1 = value;使用value時,在咱們表面看到是好像是使用main函數裏的一個局部棧變量,其實不是,使用的是堆裏面的容int value1 = (value.__forwarding->value); }

 

從代碼裏的註釋再加上前面兩部份這講解,應該是能夠看明白這段代碼的。簡單作個說明:一開始我會猜測__block修飾的變量的值能在block代碼塊中被修改,不就是在第二部分中的傳一個變量值變成傳這個變量的地址進去嗎?其實這樣是有問題的,要明白若是這樣,就是至關是傳了一個棧變量的地址進去,函數結束這個地址就不可用了,編譯器纔會給咱們建立一個新的結構__Block_byref_value_0


 

小結:

  本文是我在學習block的過程當中,經過看別人文章,源碼並本身親本身動手實踐得出來的結果,宗旨是讓你們更容易明白block的底層實現原理。

 若是想了解本文之外block的知識或者更深刻了解,參考:bioslitripleCCllvm.orgclang對block的編譯規則,這裏就不展開了,有時間再寫。

相關文章
相關標籤/搜索