談Objective-C block的實現

MadWehner 11年前發布 | 7K 次閱讀 iOS開發 移動開發

前言

這里 有關于 block 的 5 道測試題,建議你閱讀本文之前先做一下測試。

先介紹一下什么是閉包。在 wikipedia 上,閉包的定義) 是:

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 的數據結構定義如下(圖片來自 這里):

談Objective-C block的實現

對應的結構體定義如下:

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>

通過該圖,我們可以知道,一個 block 實例實際上由 6 部分構成:

  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 在內存上是完全一樣的,原因是結構體本身并不帶有任何額外的附加信息。

  • 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>

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

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

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

    研究工具:clang

    為了研究編譯器是如何實現 block 的,我們需要使用 clang。clang 提供一個命令,可以將 Objetive-C 的源碼改寫成 c 語言的,借此可以研究 block 具體的源碼實現方式。該命令是

    clang -rewrite-objc block.c
    

    NSConcreteGlobalBlock 類型的 block 的實現

    我們先新建一個名為 block1.c 的源文件:

  • #include <stdio.h>

    int main() { ^{ printf("Hello, World!\n"); } (); return 0; } </code></pre> </td> </tr> </tbody> </table>

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

    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>

    下面我們就具體看一下是如何實現的。__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 的文件,輸入以下內容:

    #include <stdio.h>

    int main() { int a = 100; void (^block2)(void) = ^{ printf("%d\n", a); }; block2();

    return 0;
    

    } </code></pre> </td> </tr> </tbody> </table>

    用之前提到的 clang 工具,轉換后的關鍵代碼如下:

    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>

    在本例中,我們可以看到:

    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 關鍵字:

  • #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>

    生成的關鍵代碼如下,可以看到,差異相當大:

    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_0dst, struct __main_block_impl_0src) {_Block_object_assign((void)&dst->i, (void)src->i, 8/BLOCK_FIELD_IS_BYREF/);}

    static void main_block_dispose_0(struct main_block_impl_0src) {_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>

    從代碼中我們可以看到:

    1. 源碼中增加一個名為 Block_byref_i_0 的結構體,用來保存我們要 capture 并且修改的變量 i。</li>
    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 函數指針,對于在調用前后修改相應變量的引用計數。
    5. </ol>

      NSConcreteMallocBlock 類型的 block 的實現

      NSConcreteMallocBlock 類型的 block 通常不會在源碼中直接出現,因為默認它是當一個 block 被 copy 的時候,才會將這個 block 復制到堆中。以下是一個 block 被 copy 時的示例代碼 (來自 這里),可以看到,在第 8 步,目標的 block 類型被修改為 _NSConcreteMallocBlock。

    6. 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>

      變量的復制

      對于 block 外的變量引用,block 默認是將其復制到其數據結構中來實現訪問的,如下圖所示(圖片來自 這里):

      談Objective-C block的實現

      對于用 __block 修飾的外部變量引用,block 是復制其引用地址來實現訪問的,如下圖所示(圖片來自 這里):

      談Objective-C block的實現

      LLVM 源碼

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

      ARC 對 block 類型的影響

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

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

      #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>

      我個人認為這么做的原因是,由于 ARC 已經能很好地處理對象的生命周期的管理,這樣所有對象都放到堆上管理,對于編譯器實現來說,會比較方便。

      參考鏈接

      希望本文能加深你對于 block 的理解。我在學習中,查閱了以下文章,一并分享給大家。祝大家玩得開心~

      來源:唐巧的技術博客 

       本文由用戶 MadWehner 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
       轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
       本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
  • sesese色