谈Objective-C block的实现

MadWehner 11年前
   <h2>前言</h2>    <p><a href="/misc/goto?guid=4959671137715357249" rel="external">这里</a> 有关于 block 的 5 道测试题,建议你阅读本文之前先做一下测试。</p>    <p>先介绍一下什么是闭包。在 wikipedia 上,<a href="/misc/goto?guid=4959671137796154174" rel="external">闭包的定义</a>) 是:</p>    <blockquote>     <p>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.</p>    </blockquote>    <p>翻译过来,闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。</p>    <p>block 实际上就是 Objective-C 语言对于闭包的实现。<br> block 配合上 dispatch_queue,可以方便地实现简单的多线程编程和异步编程,关于这个,我之前写过一篇文章介绍:<a href="http://www.open-open.com/lib/view/open1460869542785.html">《使用 GCD》</a>。</p>    <p>本文主要介绍 Objective-C 语言的 block 在编译器中的实现方式。主要包括:</p>    <ol>     <li>block 的内部实现数据结构介绍</li>     <li>block 的三种类型及其相关的内存管理方式</li>     <li>block 如何通过 capture 变量来达到访问函数外的变量</li>    </ol>    <h2>实现方式</h2>    <h3>数据结构定义</h3>    <p>block 的数据结构定义如下(图片来自 <a href="/misc/goto?guid=4959671137903422723" rel="external">这里</a>):</p>    <p><img src="https://simg.open-open.com/show/ffc862dd47f44195a47751d4de98225d.jpg" alt="谈Objective-C block的实现" width="510" height="511"></p>    <p>对应的结构体定义如下:</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">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. */  };  </code></pre> </td>      </tr>     </tbody>    </table>    <p>通过该图,我们可以知道,一个 block 实例实际上由 6 部分构成:</p>    <ol>     <li>isa 指针,所有对象都有该指针,用于实现对象相关的功能。</li>     <li>flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。</li>     <li>reserved,保留变量。</li>     <li>invoke,函数指针,指向具体的 block 实现的函数调用地址。</li>     <li>descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。</li>     <li>variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。</li>    </ol>    <p>该数据结构和后面的 clang 分析出来的结构实际是一样的,不过仅是结构体的嵌套方式不一样。但这一点我一开始没有想明白,所以也给大家解释一下,如下 2 个结构体 SampleA 和 SampleB 在内存上是完全一样的,原因是结构体本身并不带有任何额外的附加信息。</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">struct SampleA {      int a;      int b;      int c;  };    struct SampleB {      int a;      struct Part1 {          int b;      };      struct Part2 {          int c;      };  };  </code></pre> </td>      </tr>     </tbody>    </table>    <p>在 Objective-C 语言中,一共有 3 种类型的 block:</p>    <ol>     <li>_NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。</li>     <li>_NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。</li>     <li>_NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。</li>    </ol>    <p>我们在下面会分别来查看它们各自的实现方式上的差别。</p>    <h3>研究工具:clang</h3>    <p>为了研究编译器是如何实现 block 的,我们需要使用 clang。clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,借此可以研究 block 具体的源码实现方式。该命令是</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">clang -rewrite-objc block.c  </code></pre> </td>      </tr>     </tbody>    </table>    <h2>NSConcreteGlobalBlock 类型的 block 的实现</h2>    <p>我们先新建一个名为 block1.c 的源文件:</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">#include <stdio.h>    int main()  {      ^{ printf("Hello, World!\n"); } ();      return 0;  }  </code></pre> </td>      </tr>     </tbody>    </table>    <p>然后在命令行中输入<code>clang -rewrite-objc block1.c</code>即可在目录中看到 clang 输出了一个名为 block1.cpp 的文件。该文件就是 block 在 c 语言实现,我将 block1.cpp 中一些无关的代码去掉,将关键代码引用如下:</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">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;  }  </code></pre> </td>      </tr>     </tbody>    </table>    <p>下面我们就具体看一下是如何实现的。__main_block_impl_0 就是该 block 的实现,从中我们可以看出:</p>    <ol>     <li>一个 block 实际是一个对象,它主要由一个 isa 和 一个 impl 和 一个 descriptor 组成。</li>     <li>在本例中,isa 指向 _NSConcreteGlobalBlock, 主要是为了实现对象的所有特性,在此我们就不展开讨论了。</li>     <li>由于 clang 改写的具体实现方式和 LLVM 不太一样,并且这里没有开启 ARC。所以这里我们看到 isa 指向的还是<code>_NSConcreteStackBlock</code>。但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock 类型,具体可以看 <a href="/misc/goto?guid=4959671137715357249" rel="external">《objective-c-blocks-quiz》</a> 第二题的解释。</li>     <li>impl 是实际的函数指针,本例中,它指向 __main_block_func_0。这里的 impl 相当于之前提到的 invoke 变量,只是 clang 编译器对变量的命名不一样而已。</li>     <li>descriptor 是用于描述当前这个 block 的附加信息的,包括结构体的大小,需要 capture 和 dispose 的变量列表等。结构体大小需要保存是因为,每个 block 因为会 capture 一些变量,这些变量会加到 __main_block_impl_0 这个结构体中,使其体积变大。在该例子中我们还看不到相关 capture 的代码,后面将会看到。</li>    </ol>    <h2>NSConcreteStackBlock 类型的 block 的实现</h2>    <p>我们另外新建一个名为 block2.c 的文件,输入以下内容:</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">#include <stdio.h>    int main() {      int a = 100;      void (^block2)(void) = ^{          printf("%d\n", a);      };      block2();        return 0;  }  </code></pre> </td>      </tr>     </tbody>    </table>    <p>用之前提到的 clang 工具,转换后的关键代码如下:</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">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;  }  </code></pre> </td>      </tr>     </tbody>    </table>    <p>在本例中,我们可以看到:</p>    <ol>     <li>本例中,isa 指向 _NSConcreteStackBlock,说明这是一个分配在栈上的实例。</li>     <li><strong>main_block_impl_0 中增加了一个变量 a,在 block 中引用的变量 a 实际是在申明 block 时,被复制到</strong>main_block_impl_0 结构体中的那个变量 a。因为这样,我们就能理解,在 block 内部修改变量 a 的内容,不会影响外部的实际变量 a。</li>     <li><strong>main_block_impl_0 中由于增加了一个变量 a,所以结构体的大小变大了,该结构体大小被写在了 </strong>main_block_desc_0 中。</li>    </ol>    <p>我们修改上面的源码,在变量前面增加 __block 关键字:</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">#include <stdio.h>    int main()  {      __block int i = 1024;      void (^block1)(void) = ^{          printf("%d\n", i);          i = 1023;      };      block1();      return 0;  }  </code></pre> </td>      </tr>     </tbody>    </table>    <p>生成的关键代码如下,可以看到,差异相当大:</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">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;  }  </code></pre> </td>      </tr>     </tbody>    </table>    <p>从代码中我们可以看到:</p>    <ol>     <li>源码中增加一个名为 __Block_byref_i_0 的结构体,用来保存我们要 capture 并且修改的变量 i。</li>     <li><strong>main_block_impl_0 中引用的是 </strong>Block_byref_i_0 的结构体指针,这样就可以达到修改外部变量的作用。</li>     <li>__Block_byref_i_0 结构体中带有 isa,说明它也是一个对象。</li>     <li>我们需要负责 <strong>Block_byref_i_0 结构体相关的内存管理,所以 </strong>main_block_desc_0 中增加了 copy 和 dispose 函数指针,对于在调用前后修改相应变量的引用计数。</li>    </ol>    <h2>NSConcreteMallocBlock 类型的 block 的实现</h2>    <p>NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,因为默认它是当一个 block 被 copy 的时候,才会将这个 block 复制到堆中。以下是一个 block 被 copy 时的示例代码 (来自 <a href="/misc/goto?guid=4959671137903422723" rel="external">这里</a>),可以看到,在第 8 步,目标的 block 类型被修改为 _NSConcreteMallocBlock。</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">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;  }  </code></pre> </td>      </tr>     </tbody>    </table>    <h2>变量的复制</h2>    <p>对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的,如下图所示(图片来自 <a href="/misc/goto?guid=4959671138006592840" rel="external">这里</a>):</p>    <p><img src="https://simg.open-open.com/show/f63e249b2abf2f1fc941443117964687.jpg" alt="谈Objective-C block的实现" width="319" height="162"></p>    <p>对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的,如下图所示(图片来自 <a href="/misc/goto?guid=4959671138006592840" rel="external">这里</a>):</p>    <p><img src="https://simg.open-open.com/show/f8aeb63aea82f794d8a0d70393c5c71a.jpg" alt="谈Objective-C block的实现" width="252" height="143"></p>    <h2>LLVM 源码</h2>    <p>在 LLVM 开源的关于 <a href="/misc/goto?guid=4959671138107860370" rel="external">block 的实现源码</a>,其内容也和我们用 clang 改写得到的内容相似,印证了我们对于 block 内部数据结构的推测。</p>    <h2>ARC 对 block 类型的影响</h2>    <p>在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。</p>    <p>原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。证明方式是以下代码在 XCode 中,会输出 <code><__NSMallocBlock__: 0x100109960></code>。在苹果的 <a href="/misc/goto?guid=4958823231951989454" rel="external">官方文档</a> 中也提到,当把栈中的 block 返回时,不需要调用 copy 方法了。</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-objectivec">#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;  }  </code></pre> </td>      </tr>     </tbody>    </table>    <p>我个人认为这么做的原因是,由于 ARC 已经能很好地处理对象的生命周期的管理,这样所有对象都放到堆上管理,对于编译器实现来说,会比较方便。</p>    <h2>参考链接</h2>    <p>希望本文能加深你对于 block 的理解。我在学习中,查阅了以下文章,一并分享给大家。祝大家玩得开心~</p>    <ul>     <li><a href="/misc/goto?guid=4959671138216899574" rel="external">A look inside blocks: Episode 1</a></li>     <li><a href="/misc/goto?guid=4959671138308889087" rel="external">A look inside blocks: Episode 2</a></li>     <li><a href="/misc/goto?guid=4959671137903422723" rel="external">A look inside blocks: Episode 3</a></li>     <li><a href="/misc/goto?guid=4959671138403979842" rel="external">对 Objective-C 中 Block 的追探</a></li>     <li><a href="/misc/goto?guid=4959671138107860370" rel="external">LLVM 中 block 实现源码</a></li>     <li><a href="/misc/goto?guid=4959671137715357249" rel="external">objective-c-blocks-quiz</a></li>     <li><a href="/misc/goto?guid=4959671138006592840" rel="external">Blocks</a></li>    </ul>    <p>来源:<a href="/misc/goto?guid=4959669714886886295">唐巧的技术博客</a> </p>