わかっているようでわかっていなかったBlocksの仕組み
iOS 4から使えるようになったBlocksは、余計なことを気にせず気軽に使えるとてもよい拡張機能で、僕も便利にガンガン使わせてもらっています。
ただ、表向きは親切なのですが、裏側の実装は変態的なことになっており、それを理解するまで疑問に思うことや不安を感じることが多々ありました。
というわけで、自分の復習もかねて、Blocksに関して色々調べた結果をここにまとめておきます。随時追記予定&つっこみ歓迎です。
Blockの正体は構造体
下記のようにBlock変数 myblock を定義したあとでブレークさせると、XcodeのVariables Viewでその中身を見ることができます。
「myblock = (struct __block_literal_1 *) 0xbfffd490」ということなので、myblock は __block_literal_1 という型の構造体のポインタ変数で、その実体はスタックに確保されているらしいことがわかります。
注目は、構造体のメンバとして __FuncPtr があることです。名前から推測できるように、これは関数ポインタを格納するメンバ変数です。
実はこれ、
void (^myblock)() = ^{ NSLog(@"%d", i); };
というふうにBlock変数を定義すると、コンパイル時に
{ NSLog(@"%d", i); }
の部分が関数として自動的に定義されて、その関数へのポインタが __FuncPtr に格納される、という仕組みになっているのです。
「えー、じゃあこの関数で使っている i はどこで定義されるの?」という疑問がわきますが、さきほど見た __block_literal_1 型の構造体のメンバとして、一番下に i が定義されているのに気づいたでしょうか? 実はこの関数が定義されるときに、構造体のメンバとして持っている i が使われるようなコードに置き換わるのです。
ということは、この構造体の型 __block_literal_1 は、__FuncPtr が指す関数内で使っている変数によって、そのメンバ構成が変わらなくてはいけないことになるわけですが、まさにそのとおり。コード内でBlockを定義すると、その正体である構造体の型も必要に応じて自動的に定義されます。定義が増えるたびに、__block_literal_2、__block_literal_3 と型名の最後の数字が増えていくわけです。
そして、このBlockを実行するために myblock(); と書いた部分は、myblock が持つ FuncPtr で示された関数に myblock 自身を渡して実行されるコードに置き換わります。
こんな感じで、たった数行のコードから色んなものが自動に追加されたり置き換わったりすることで、Blocksの便利な機能が実現されているのです。
Blockはその定義によって配置されるメモリの位置が違う
「iOS4プログラミングブック」などのBlocksの解説を見ると、新しく定義したBlockが配置されるメモリの位置は、ちょっと特殊なルールで決まるそうです。
Blockの内容 | メモリの位置 |
---|---|
Block外のローカルな自動変数を使っているBlock | スタック |
Block外のローカルな自動変数を使っていないBlock | .dataセクション |
スタックにあるBlockをヒープにコピーする Block_copy() を使った場合はこうなります。
コピー元のBlockの内容 | コピー後のメモリの位置 |
---|---|
Block外のローカルな自動変数を使っているBlock | ヒープ |
それ以外のBlock | 元の場所と同じ(.dataセクション or ヒープ) |
本当にこうなるか実際に試してみます。
int stack_int = 1; static int global_int = 1; NSLog(@"stack_int:\t%d : %p", stack_int, &stack_int); NSLog(@"global_int:\t%d : %p", global_int, &global_int); void (^stack_block)() = ^{ NSLog(@"stack_int:\t%d : %p", stack_int, &stack_int); }; NSLog(@"stack_block:\t%@", stack_block); void (^global_block)() = ^{ NSLog(@"global_int:\t%d : %p", global_int, &global_int); }; NSLog(@"global_block:\t%@", global_block); void (^copy_stack_block)() = Block_copy(stack_block); NSLog(@"copy_stack_block:\t%@", copy_stack_block); void (^copy_global_block)() = Block_copy(global_block); NSLog(@"copy_global_block:\t%@", copy_global_block); void (^copy_copy_stack_block)() = Block_copy(copy_stack_block); NSLog(@"copy_copy_stack_block:\t%@", copy_copy_stack_block); void (^copy_copy_global_block)() = Block_copy(copy_global_block); NSLog(@"copy_copy_global_block:\t%@", copy_copy_global_block); Block_release(copy_stack_block); Block_release(copy_global_block); Block_release(copy_copy_stack_block); Block_release(copy_copy_global_block);
Blockはオブジェクトとして振る舞うので、NSLogで %@ とするとその中身を簡単に見ることができます。出力結果はこんな感じ。
2012-03-28 13:53:26.235 xxx[3781:207] stack_int: 1 : 0xbfffd4b0 2012-03-28 13:53:26.237 xxx[3781:207] global_int: 1 : 0x4660 2012-03-28 13:53:26.238 xxx[3781:207] stack_block: <__NSStackBlock__: 0xbfffd490> 2012-03-28 13:53:26.239 xxx[3781:207] global_block: <__NSGlobalBlock__: 0x4698> 2012-03-28 13:53:26.240 xxx[3781:207] copy_stack_block: <__NSMallocBlock__: 0x6804900> 2012-03-28 13:53:26.241 xxx[3781:207] copy_global_block: <__NSGlobalBlock__: 0x4698> 2012-03-28 13:53:26.242 xxx[3781:207] copy_copy_stack_block: <__NSMallocBlock__: 0x6804900> 2012-03-28 13:53:26.242 xxx[3781:207] copy_copy_global_block: <__NSGlobalBlock__: 0x4698>
まさに冒頭の表に書かれている結果になりました。
__NSStackBlock__ を Block_copy() すると __NSMallocBlock__ になり、どちらもそれっぽいアドレスに配置されています。また、__NSMallocBlock__ と __NSGlobalBlock__ は何回 Block_copy() しても、配置される領域だけでなくアドレスすら変わらないことがわかります。
実際には、__NSMallocBlock__ を再度 Block_copy() すると、新たにコピーされずに参照カウントが増える形になります。NSObject の retain のような動作をするのですが、どちらにしろ Block_copy() した数だけ Block_release() しないとメモリリークします。
一方、__NSGlobalBlock__ の場合は参照カウントすら増やしません。おそらく構造体のアドレスをコピーしているだけではないでしょうか。Block_release() も何にも処理をしていないっぽいです。なので、もし Block_copy() 後に Block_release() しなくても、なんとメモリリークしません!
でも両者をいちいち区別するのは面倒なので、確保しておきたいBlockは Block_copy() して、使わなくなったら Block_release() するのがよいでしょう。
キャプチャの正体は構造体メンバへのコピー
では、以下のようなコードを Block_release() の前に追加して、定義したBlock達を実行してみます。
stack_int++; stack_block(); global_int++; global_block(); stack_int++; copy_stack_block(); global_int++; copy_global_block(); stack_int++; copy_copy_stack_block(); global_int++; copy_copy_global_block();
実行結果。最初に記述した部分も含みます。
2012-03-28 13:59:10.212 xxx[3811:207] stack_int: 1 : 0xbfffd4b0 2012-03-28 13:59:10.214 xxx[3811:207] global_int: 1 : 0x4660 2012-03-28 13:59:10.215 xxx[3811:207] stack_block: <__NSStackBlock__: 0xbfffd490> 2012-03-28 13:59:10.216 xxx[3811:207] global_block: <__NSGlobalBlock__: 0x4698> 2012-03-28 13:59:10.217 xxx[3811:207] copy_stack_block: <__NSMallocBlock__: 0x68390e0> 2012-03-28 13:59:10.218 xxx[3811:207] copy_global_block: <__NSGlobalBlock__: 0x4698> 2012-03-28 13:59:10.265 xxx[3811:207] copy_copy_stack_block: <__NSMallocBlock__: 0x68390e0> 2012-03-28 13:59:10.265 xxx[3811:207] copy_copy_global_block: <__NSGlobalBlock__: 0x4698> 2012-03-28 13:59:10.266 xxx[3811:207] stack_int: 1 : 0xbfffd4a4 2012-03-28 13:59:10.267 xxx[3811:207] global_int: 2 : 0x4660 2012-03-28 13:59:10.272 xxx[3811:207] stack_int: 1 : 0x68390f4 2012-03-28 13:59:10.273 xxx[3811:207] global_int: 3 : 0x4660 2012-03-28 13:59:10.274 xxx[3811:207] stack_int: 1 : 0x68390f4 2012-03-28 13:59:10.275 xxx[3811:207] global_int: 4 : 0x4660
ローカルな自動変数である stack_int をいくらインクリメントしても、Blockで出力されるのはBlockを定義したときの値になっている、というのはBlocksの「キャプチャ」と呼ばれる機能の一例です。それも大事なのですが、ここでは変数のアドレスに注目します。
最初に定義した stack_int のアドレスと stack_block() で出力される stack_int のアドレスは、同じスタック内でも違う位置になっています。これは、前述のように stack_block が示す構造体のメンバのほうの stack_int が使われている、という事実を表しています。その値は stack_block を定義したときにローカル変数 stack_int からコピーされるため、その後いくらローカル変数を変更しても変化しない、というわけです。
また、stack_block を Block_copy() した copy_stack_block の実体はヒープ上に配置されますので、copy_stack_block() で出力される stack_int のアドレスもヒープ上になっています。そして、以後何回 Block_copy() してもそのアドレスは変わっていません。
一方、同じローカル変数でもstaticな変数である global_int はどこでもアドレスが変わらず、値の変化にも追従していることがわかります。Block内で使おうとしている変数がstaticである場合、プログラム実行中はメモリに確保され続けるため、構造体のメンバもポインタ変数になってそこに元の変数のアドレスがコピーされるようです。関数内のコードもポインタ変数の操作に置き換わります。なので、どこで値を変更しても反映されるんですね。
__block修飾子の正体も構造体
Blockでキャプチャしたローカル変数をBlock内で変更できるようにするには、__block修飾子を使います。__block修飾子がついた変数はBlock内外の変更に追従します。
__block int i = 1; void (^myblock)() = ^{ NSLog(@"%d : %p (myblock)", i, &i); }; i++; NSLog(@"%d : %p", i, &i); myblock(); void (^copy_myblock)() = Block_copy(myblock); i++; NSLog(@"%d : %p", i, &i); copy_myblock(); i++; NSLog(@"%d : %p", i, &i); myblock(); Block_release(copy_myblock);
このコードの結果はちょっと面白いですよ。
2012-03-29 01:00:33.235 xxx[5613:207] 2 : 0xbfffd560 2012-03-29 01:00:33.239 xxx[5613:207] 2 : 0xbfffd560 (myblock) 2012-03-29 01:00:33.257 xxx[5613:207] 3 : 0x6865000 2012-03-29 01:00:33.258 xxx[5613:207] 3 : 0x6865000 (myblock) 2012-03-29 01:00:33.260 xxx[5613:207] 4 : 0x6865000 2012-03-29 01:00:33.262 xxx[5613:207] 4 : 0x6865000 (myblock)
i がちゃんとインクリメントされているのはいいとして、なんと、ローカル変数 i と myblock の i が同じアドレスを指しています。さっきの説明と違いますね。
また、驚くべきことに、そのアドレスが途中からヒープ上になってしまっています! int i として定義したローカルな自動変数のアドレスが関数内で変わるなんて、普通ではちょっと考えられません。
さらに、スタック上にあるはずの myblock の最後の実行結果も、同じヒープ上のアドレスを表示しています。わけがわからない…。
実はこの現象は、__block修飾子をつけた変数の宣言部分が、Blockとはまた別の構造体の宣言に置き換わることによって発生するのです。__block修飾子がついた変数の正体も構造体なのです。
ということは、Block変数のときように、XcodeのVariables Viewでその実体と値の変化を見ることができそうです。アドレスが変化しているのは Block_copy() のせいっぽいので、Block_copy() の行でブレークしてみます。
んー、i は (int) のままですねぇ…。
ここで i のアドレスも監視したいので、Variables Viewの中で右クリックして「Add Expression...」を選択し、「&i」と入力します。
この時点では、アドレスはスタック上のままのようです。
では、この状態でStep Overして Block_copy() を実行させます。
おお、i は相変わらず (int) のままですが、&i の値がヒープ上になりました! なんだこれ!
どうやらXcode(というかgdb)は、__block修飾子がついた変数をデバッグしやすいように元の形のまま表示してくれるようで、こうやっても正体を見せてくれないんですね。うむー。
さて、このままだと__block修飾子の正体がわからないのですが、実は myblock や copy_myblock の構造体のメンバにその正体を見ることができます。
ローカル変数のキャプチャの仕組みと同様、Blockの構造体に i というメンバが追加されており、それが構造体のポインタ変数として定義されていることがわかります。この構造体が__block修飾子をつけられた変数の正体です。
この構造体の中にはさらに別の i が定義されており、どうやら僕らはその値やアドレスを見ていたようです。
また、myblock が持つ構造体 i のメンバ __forwarding の値が、copy_myblock のほうの i が指しているアドレスになっているのに気づきます。この仕組みをここで説明するのは大変なので省きますが、この __forwarding が指すアドレスが必要に応じて切り替わることで、i がメモリ上のどこにあっても気にせず使えるようになっているのです。
__block修飾子の詳しい話は、「iOS4プログラミングブック」でBlocksやGCDの章を執筆された坂本一樹 (@splhack) 氏のブログに書かれていますので、ぜひ一度ご覧下さい。今回書いていない「clang -rewrite-objc」での解析などの話もあって非常にためになります。
Blockを含むBlockをBlock_copy()するとどうなるか
あとで書く