NSAutoreleasePool はどこまでやってくれるのか

iphone開発ではガベージコレクションは使えないため、alloc/releaseを使って自分でメモリ管理を行う必要があるのはどの入門書にもあります。また、それを便利にするための NSAutoreleasePool というのがあり、それもよく使います。しかし、この NSAutoreleasePool はどこまで面倒を見てくれるイマイチよく分からなかったため調べてみることにしました。

はじめに NSAutoreleasePoolのおさらい。NSAutoreleasePoolのインスタンスを alloc 、init で作成して使いますが、iphoneの場合、プールが最初に作られるので明示的にプールを生成しなくても使えます。プールにオブジェクトを登録するには NSObjectの autorelease メソッドを使います。

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  // プールを明示的に作成
  NSObject *obj = [[[NSObject alloc] init] autorelease];  // autorelease でプールにオブジェクトを登録
  ・・・
  [pool release];  // プールをリリースすると、登録されたオブジェクトもリリースされる

ここまではよく解説書にあります。プールをリリースしたときにどういったことが行われるのでしょうか。プールに登録されたオブジェクトに単に1度だけ release を送っているのか、または完全に解放してしまうのか。

NSAutoreleasePool を release するとどうなるか

実際にやってみましょう。まずは一般的な動きから。オブジェクトの参照カウントを retainCount で取得して表示しています。

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSObject *obj = [[[NSObject alloc] init] autorelease];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [pool release];
  NSLog(@"pool released.");
  NSLog(@"obj retainCount: %u", [obj retainCount]);

出力結果

obj retainCount: 1
pool released.
obj retainCount: 1
obj description: 

いきなり予想外!2回目の [obj retainCount] のところでは既に解放されているため、メモリ違反で落ちるはずが、きっちり動いている。しかも retainCount 減ってない!!
実は [pool release] に問題がありそう。リファレンスを読むと、release メソッドは リファレンスカウンタ環境では動くが、GC環境では何もしないため、通常は drain メソッドを使え、とあった。iOSGC未サポートだから release で問題ないと思うのだが、シミュレータは違うのだろうか・・・ なんにせよ、drain メソッドに置き換えると期待通りに動く。

■修正版

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSObject *obj = [[[NSObject alloc] init] autorelease];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [pool drain];
  NSLog(@"pool released.");
  NSLog(@"obj retainCount: %u", [obj retainCount]);

出力結果

obj retainCount: 1
pool released.
プログラムはシグナルを受信しました:“EXC_BAD_ACCESS”。

プールを開放した後で、オブジェクトのretainCountメソッドを呼ぶとメモリ違反が起きてます。ちゃんと解放されてますね。

NSAutoreleasePool を drain するとどうなるか

ということで、気を取り直して本題。明示的に retain した場合、プールを開放したときにそれらのオブジェクトはどうなるかが知りたかった。そこで retain して参照カウントを増やしてからプールを開放(drain)してみる。

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSObject *obj = [[[NSObject alloc] init] autorelease];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [obj retain];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [pool drain];
  NSLog(@"pool released.");
  NSLog(@"obj retainCount: %u", [obj retainCount]);

出力結果

obj retainCount: 1
obj retainCount: 2
pool released.
obj retainCount: 1

プールを開放しても、オブジェクトは解放されてない。どうやら、autorelease したオブジェクトに対して release を一度だけ送っているようだ。まあそうだろう。そうでないと、他のオブジェクトのプロパティなどにセットしたものも全てreleaseされてしまい、あちこちでメモリ違反が起きそうだ。

今度は autorelease を2度呼ぶとどうなるかを試してみる。自分で alloc してインスタンス化したオブジェクトはいいが、そうでない方法でオブジェクトを取得したとき、そのオブジェクトが autorelease されているかどうか分からないことがある。そういう場合、もう一度 autorelease を呼んで問題があるか知りたい。先のコードの autorelease を二つにしてみる。

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  NSObject *obj = [[[[NSObject alloc] init] autorelease] autorelease];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [obj retain];
  NSLog(@"obj retainCount: %u", [obj retainCount]);
  [pool drain];
  NSLog(@"pool released.");
  NSLog(@"obj retainCount: %u", [obj retainCount]);

出力結果

obj retainCount: 1
obj retainCount: 2
pool released.
プログラムはシグナルを受信しました:“EXC_BAD_ACCESS”。

先程はプールを開放しても retainCount が 1 残っていたのに、今度はメモリ違反になった。メモリが解放されているようだ。どうやら autorelease を呼んだ回数だけ、解放時に release が送られるようである。ということで、autorelease されてるか分からないからもう一回よべばいいや、というのはとんでもない結果をもたらしてしまうことが分かった。

まとめ

  • NSAutoreleasePool の解放は drain を使うこと。
  • autorelease を呼んだ回数だけ、後で release が送られる。
    • retain したものは残るため、自分で retain したものは自分で release すること!
      • プロパティなどにセットしてretainした場合は、そのオブジェクトの dealloc でしっかりreleaseする!
    • alloc以外で取得したオブジェクトを勝手に autorelease しない!
      • ちゃんと autorelease されていることを信じるしかない。
      • autorelease されているかどうか判定する方法はない??

おまけ