談Objective-C Block的實現

 

來源:http://blog.devtang.com/blog/2013/07/28/a-look-inside-blocks/html

前言

這裏有關於block的5道測試題,建議你閱讀本文以前先作一下測試。ios

先介紹一下什麼是閉包。在wikipedia上,閉包的定義)是:objective-c

In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.編程

翻譯過來,閉包是一個函數(或指向函數的指針),再加上該函數執行的外部的上下文變量(有時候也稱做自由變量)。數據結構

block實際上就是Objective-C語言對於閉包的實現。 block配合上dispatch_queue,能夠方便地實現簡單的多線程編程和異步編程,關於這個,我以前寫過一篇文章介紹:《使用GCD》多線程

本文主要介紹Objective-C語言的block在編譯器中的實現方式。主要包括:閉包

  1. block的內部實現數據結構介紹
  2. block的三種類型及其相關的內存管理方式
  3. block如何經過capture變量來達到訪問函數外的變量

實現方式

數據結構定義

block的數據結構定義以下(圖片來自這裏):app

對應的結構體定義以下:異步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Block_descriptor {  unsigned long int reserved;  unsigned long int size;  void (*copy)(void *dst, void *src);  void (*dispose)(void *); }; struct Block_layout {  void *isa;  int flags;  int reserved;  void (*invoke)(void *, ...);  struct Block_descriptor *descriptor;  /* Imported variables. */ }; 

經過該圖,咱們能夠知道,一個block實例實際上由6部分構成:ide

  1. isa指針,全部對象都有該指針,用於實現對象相關的功能。
  2. flags,用於按bit位表示一些block的附加信息,本文後面介紹block copy的實現代碼能夠看到對該變量的使用。
  3. reserved,保留變量。
  4. invoke,函數指針,指向具體的block實現的函數調用地址。
  5. descriptor, 表示該block的附加描述信息,主要是size大小,以及copy和dispose函數的指針。
  6. variables,capture過來的變量,block可以訪問它外部的局部變量,就是由於將這些變量(或變量的地址)複製到告終構體中。

該數據結構和後面的clang分析出來的結構實際是同樣的,不過僅是結構體的嵌套方式不同。但這一點我一開始沒有想明白,因此也給你們解釋一下,以下2個結構體SampleA和SampleB在內存上是徹底同樣的,緣由是結構體自己並不帶有任何額外的附加信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct SampleA {  int a;  int b;  int c; }; struct SampleB {  int a;  struct Part1 {  int b;  };  struct Part2 {  int c;  }; }; 

在Objective-C語言中,一共有3種類型的block:

  1. _NSConcreteGlobalBlock 全局的靜態block,不會訪問任何外部變量。
  2. _NSConcreteStackBlock 保存在棧中的block,當函數返回時會被銷燬。
  3. _NSConcreteMallocBlock 保存在堆中的block,當引用計數爲0時會被銷燬。

咱們在下面會分別來查看它們各自的實現方式上的差異。

研究工具:clang

爲了研究編譯器是如何實現block的,咱們須要使用clang。clang提供一個命令,能夠將Objetive-C的源碼改寫成c語言的,藉此能夠研究block具體的源碼實現方式。該命令是

1
clang -rewrite-objc block.c 

NSConcreteGlobalBlock 類型的block的實現

咱們先新建一個名爲block1.c的源文件:

1
2
3
4
5
6
7
#include <stdio.h> int main() {  ^{ printf("Hello, World!\n"); } ();  return 0; } 

而後在命令行中輸入clang -rewrite-objc block1.c便可在目錄中看到clang輸出了一個名爲block1.cpp的文件。該文件就是block在c語言實現,我將block1.cpp中一些無關的代碼去掉,將關鍵代碼引用以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;  __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 void __main_block_func_0(struct __main_block_impl_0 *__cself) {  printf("Hello, World!\n"); } 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() {  (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();  return 0; } 

下面咱們就具體看一下是如何實現的。__main_block_impl_0就是該block的實現,從中咱們能夠看出:

  1. 一個block實際是一個對象,它主要由一個 isa 和 一個 impl 和 一個descriptor組成。
  2. 在本例中,isa指向 _NSConcreteGlobalBlock, 主要是爲了實現對象的全部特性,在此咱們就不展開討論了。
  3. 因爲clang改寫的具體實現方式和LLVM不太同樣,而且這裏沒有開啓ARC。因此這裏咱們看到isa指向的仍是_NSConcreteStackBlock。但在LLVM的實現中,開啓ARC時,block應該是_NSConcreteGlobalBlock類型,具體能夠看《objective-c-blocks-quiz》第二題的解釋。
  4. impl是實際的函數指針,本例中,它指向__main_block_func_0。這裏的impl至關於以前提到的invoke變量,只是clang編譯器對變量的命名不同而已。
  5. descriptor是用於描述當前這個block的附加信息的,包括結構體的大小,須要capture和dispose的變量列表等。結構體大 小須要保存是由於,每一個block由於會capture一些變量,這些變量會加到__main_block_impl_0這個結構體中,使其體積變大。在 該例子中咱們還看不到相關capture的代碼,後面將會看到。

NSConcreteStackBlock 類型的block的實現

咱們另外新建一個名爲block2.c的文件,輸入如下內容:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h> int main() {  int a = 100;  void (^block2)(void) = ^{  printf("%d\n", a);  };  block2();  return 0; } 

用以前提到的clang工具,轉換後的關鍵代碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct __main_block_impl_0 {  struct __block_impl impl;  struct __main_block_desc_0* Desc;  int a;  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {  impl.isa = &_NSConcreteStackBlock;  impl.Flags = flags;  impl.FuncPtr = fp;  Desc = desc;  } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) {  int a = __cself->a; // bound by copy  printf("%d\n", 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 a = 100;  void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);  ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);  return 0; } 

在本例中,咱們能夠看到:

  1. 本例中,isa指向_NSConcreteStackBlock,說明這是一個分配在棧上的實例。
  2. main_block_impl_0 中增長了一個變量a,在block中引用的變量a實際是在申明block時,被複制到main_block_impl_0結構體中的那個變量a。由於這樣,咱們就能理解,在block內部修改變量a的內容,不會影響外部的實際變量a。
  3. main_block_impl_0 中因爲增長了一個變量a,因此結構體的大小變大了,該結構體大小被寫在了main_block_desc_0中。

咱們修改上面的源碼,在變量前面增長__block關鍵字:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h> int main() {  __block int i = 1024;  void (^block1)(void) = ^{  printf("%d\n", i);  i = 1023;  };  block1();  return 0; } 

生成的關鍵代碼以下,能夠看到,差別至關大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
struct __Block_byref_i_0 {  void *__isa;  __Block_byref_i_0 *__forwarding;  int __flags;  int __size;  int i; }; struct __main_block_impl_0 {  struct __block_impl impl;  struct __main_block_desc_0* Desc;  __Block_byref_i_0 *i; // by ref  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref  printf("%d\n", (i->__forwarding->i));  (i->__forwarding->i) = 1023; } static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 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*);  void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; int main() {  __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};  void (*block1)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344);  ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);  return 0; } 

從代碼中咱們能夠看到:

  1. 源碼中增長一個名爲__Block_byref_i_0 的結構體,用來保存咱們要capture而且修改的變量i。
  2. main_block_impl_0 中引用的是Block_byref_i_0的結構體指針,這樣就能夠達到修改外部變量的做用。
  3. __Block_byref_i_0結構體中帶有isa,說明它也是一個對象。
  4. 咱們須要負責Block_byref_i_0結構體相關的內存管理,因此main_block_desc_0中增長了copy和dispose函數指針,對於在調用先後修改相應變量的引用計數。

NSConcreteMallocBlock 類型的block的實現

NSConcreteMallocBlock類型的block一般不會在源碼中直接出現,由於默認它是當一個block被copy的時候,纔會將這個block複製到堆中。如下是一個block被copy時的示例代碼(來自這裏),能夠看到,在第8步,目標的block類型被修改成_NSConcreteMallocBlock。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static void *_Block_copy_internal(const void *arg, const int flags) {  struct Block_layout *aBlock;  const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;  // 1  if (!arg) return NULL;  // 2  aBlock = (struct Block_layout *)arg;  // 3  if (aBlock->flags & BLOCK_NEEDS_FREE) {  // latches on high  latching_incr_int(&aBlock->flags);  return aBlock;  }  // 4  else if (aBlock->flags & BLOCK_IS_GLOBAL) {  return aBlock;  }  // 5  struct Block_layout *result = malloc(aBlock->descriptor->size);  if (!result) return (void *)0;  // 6  memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first  // 7  result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed  result->flags |= BLOCK_NEEDS_FREE | 1;  // 8  result->isa = _NSConcreteMallocBlock;  // 9  if (result->flags & BLOCK_HAS_COPY_DISPOSE) {  (*aBlock->descriptor->copy)(result, aBlock); // do fixup  }  return result; } 

變量的複製

對於block外的變量引用,block默認是將其複製到其數據結構中來實現訪問的,以下圖所示(圖片來自這裏):

對於用__block修飾的外部變量引用,block是複製其引用地址來實現訪問的,以下圖所示(圖片來自這裏):

LLVM源碼

在LLVM開源的關於block的實現源碼,其內容也和咱們用clang改寫獲得的內容類似,印證了咱們對於block內部數據結構的推測。

ARC對block類型的影響

在ARC開啓的狀況下,將只會有 NSConcreteGlobalBlock和 NSConcreteMallocBlock類型的block。

本來的NSConcreteStackBlock的block會被NSConcreteMallocBlock類型的block替代。證實方式是如下代碼在XCode中,會輸出 <__NSMallocBlock__: 0x100109960>。在蘋果的官方文檔中也提到,當把棧中的block返回時,不須要調用copy方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) {  @autoreleasepool {  int i = 1024;  void (^block1)(void) = ^{  printf("%d\n", i);  };  block1();  NSLog(@"%@", block1);  }  return 0; } 

我我的認爲這麼作的緣由是,因爲ARC已經能很好地處理對象的生命週期的管理,這樣全部對象都放到堆上管理,對於編譯器實現來講,會比較方便。

參考連接

但願本文能加深你對於block的理解。我在學習中,查閱瞭如下文章,一併分享給你們。祝你們玩得開心~

相關文章
相關標籤/搜索