Objective-C Blockの実現について


投稿:http://blog.devtang.com/blog/2013/07/28/a-look-inside-blocks/
前言
ここに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のコンパイラにおける実現方式を紹介する.主な内容は次のとおりです.
blockの内部実装データ構造紹介blockの3種類とそれに関連するメモリ管理方式blockはcapture変数によってアクセス関数外の変数に達する方法
インプリメンテーションモード
データ構造の定義
blockのデータ構造は以下のように定義されています(画像はここから).
谈Objective-C Block的实现_第1张图片
対応する構造体の定義は次のとおりです.
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. */ }; 

この図により、1つのblockインスタンスは、実際には6つの部分から構成されていることが分かる.
isaポインタは、すべてのオブジェクトにこのポインタがあり、オブジェクト関連の機能を実現します.flagsは、bitビットでいくつかのblockの追加情報を表すために使用され、本明細書では、block copyの実装コードについて後述し、この変数の使用を見ることができる.reserved、変数を保持します.invoke、関数ポインタ、特定のblock実装の関数呼び出しアドレスを指す.descriptorは、blockの追加記述情報、主にsizeサイズ、copyおよびdispose関数のポインタを表す.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があります.
_NSConcreteGlobalBlockグローバルの静的blockは、外部変数にはアクセスしません._NSConcreteStackBlockはスタック内のblockに保存され、関数が戻ると破棄されます._NSConcreteMallocBlockはスタックに保存されたblockであり、参照カウントが0の場合に破棄される.それぞれの実装方法の違いを以下でそれぞれ確認します.
研究ツール:clang
コンパイラがblockをどのように実現するかを研究するためにclangを使用する必要があります.clangは、Objetive-Cのソースコードをc言語に書き換えるコマンドを提供し、blockの具体的なソースコード実装方式を検討することができる.このコマンドは
1
clang -rewrite-objc block.c 

NSConcreteGlobalBlockタイプのblockの実装
まずblock 1という名前を追加します.cのソースファイル:
1
2
3
4
5
6
7
#include <stdio.h>  int main() {  ^{ printf("Hello, World!
"); } ();
return 0; }

次に、コマンドラインにclang -rewrite-objc block1.cを入力と、clangがblock 1という名前で出力されているのがディレクトリに表示される.cppのファイル.このファイルはblockがc言語で実現され、私はblock 1を作成します.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!
");
} 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の実現であり、そこから見ることができます.
blockは実際にはオブジェクトであり、主にisaとimplとdescriptorから構成されています.この例ではisaは_を指すNSConcreteGlobalBlockは,主にオブジェクトのすべての特性を実現するためであり,ここでは議論を展開しない.clang書き換えの具体的な実装方式はLLVMとは異なるため,ここではARCをオンにしていない.ここでisaが指しているのは_NSConcreteStackBlockですしかし、LLVMの実装では、ARCがオンの場合、blockは_NSConcreteGlobalBlockタイプは、具体的には『objective-c-blocks-quiz』第2題の解釈を見ることができます.implは実際の関数ポインタであり、この例では__を指すmain_block_func_0.ここでのimplは,前述したinvoke変数に相当するが,clangコンパイラによる変数の命名が異なるだけである.descriptorは、構造体のサイズ、captureおよびdisposeを必要とする変数リストなど、現在のblockの追加情報を記述するために使用される.構造体のサイズを保存する必要があるのは、各blockがいくつかの変数をcaptureするため、これらの変数は__に加算されるからです.main_block_impl_0この構造体では、体積を大きくします.この例ではcaptureに関連するコードはまだ見られず、後述する.NSConcreteStackBlockタイプのblockの実装
block 2という名前を追加しました.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
", 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
", 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; }

この例では、
この例ではisaは_を指すNSConcreteStackBlockは、スタックに割り当てられたインスタンスであることを示します.main_block_impl_0に変数aが追加され、blockで参照される変数aは実際にblockを宣言するとmain_にコピーされるblock_impl_0構造体の変数a.これにより、block内部で変数aの内容を修正しても、外部の実際の変数aに影響を与えないことが理解できるからである.main_block_impl_0では変数aが1つ増加したため、構造体の大きさが大きくなり、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
", 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
", (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; }

コードから次のことがわかります.
ソースコードに__という名前を追加Block_byref_i_0の構造体は、captureを保存し、変更する変数iを保存するために使用されます.main_block_impl_0ではBlock_を参照byref_i_0の構造体ポインタは,外部変数を修正する役割を果たす.__Block_byref_i_0構造体にはisaがあり、オブジェクトであることを示します.Blockを担当する必要がありますbyref_i_0構造体に関するメモリ管理なのでmain_block_desc_0にcopyとdispose関数ポインタが追加され、呼び出し前後に対応する変数を変更する参照カウントが追加されます.NSConcreteMallocBlockタイプのblockの実装
NSConcreteMallocBlockタイプのblockは、通常、ソースコードに直接表示されません.デフォルトでは、blockがcopyされたときにスタックにコピーされるためです.次に、ブロックがcopyされた場合のサンプルコード(ここから)を示します.8ステップ目では、ターゲットのブロックタイプが_に変更されていることがわかります.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のデフォルトでは、次の図に示すように、そのデータ構造にコピーしてアクセスします(画像はここから).
谈Objective-C Block的实现_第2张图片
使用についてblock修飾の外部変数参照、blockはその参照アドレスをコピーしてアクセスを実現し、下図のように(画像はここから):
谈Objective-C Block的实现_第3张图片
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
", i);
}; block1(); NSLog(@"%@", block1); } return 0; }

個人的には、ARCはオブジェクトのライフサイクルの管理をうまく処理しているので、すべてのオブジェクトをスタック上に配置して管理することができ、コンパイラの実現に便利だと思います.
リファレンスリンク
本文がblockに対する理解を深めることを望んでいます.私は勉強の中で、以下の文章を調べて、みんなに共有しました.みんな楽しんでね~
A look inside blocks: Episode 1 [A look inside blocks: Episode 2]([A look inside blocks: Episode 3](Objective-CにおけるBlockの追跡LLVMにおけるblock実装ソースコードobjective-c-blocks-quiz Blocks Posted by唐巧Jul 28 th,2013 iOS
オリジナル文章,版権声明:自由転載-非商用-非派生-保持署名|Creative Commons BY-NC-ND 3.0