最近はJavaをまずプログラム言語として始める人が多いだろうから、Objective-Cを使いだした際メモリー管理でつまずくことが多いだろう。わたし自身もすっかりJavaな人になっていたので同様に最初のころハマった。一応メモリー管理についてまとめておこう。
Objective-Cでは10.5からは今時の言語っぽくGabage Collectionが導入されので、Java同様あまりメモリー管理を気にする必要はなくなった。だが、パフォーマンスの問題からなのかXCodeのデフォルトの設定ではオフになっていたりするので、一応レガシィなメモリー管理のお手前も知っておこう。
リファレンスカウンタ、retain/release
さすがに大昔のC++のようにnew/deleteで自身で管理ということはなくて、リファレンスカウンタという割と一般的な手法でメモリー管理はなされている。これはある変数について参照が増えるたびにカウントアップ、参照がなくなるごとにカウントダウンしていき、参照カウンタがゼロになった時点でオブジェクトが削除されるという管理方法。Objective-Cの場合、この仕組みが言語でなくCocoaのライブラリレベルで実現されている。
リファレンスカウンタは生成時点で1に、その後 retain
メッセージを送るごとに一つ増え、release
するごとに減る。オブジェクトにretainCount
というメッセージを送ると現在の参照カウンタを返してくれる。以下のようなコードでテストすると、
ObjectA* objA = [[ObjectA alloc] init];
ObjectA* obj_copy;
NSMutableArray* array1 = [NSMutableArray array];
printf("After initializing : %i\n,[objA retainCount]);
obj_copy = objA;
printf("After assignment : %i\n,[objA retainCount]);
[objA retain]; printf("After retain : %i\n,[objA retainCount]);
[array1 addObject:objA];
printf("After adding to array : %i\n,[objA retainCount]);
[array1 removeAllObjects];
printf("After removing from array : %i\n,[objA retainCount]);
[objA release];
printf("After release : %i\n,[objA retainCount]);
結果は、
After initializing : 1
After assignment : 1
After retain : 2
After adding to array : 3
After removing from array : 2
After release : 1
となる。
生成した際にリファレンスカウンタは1になる。別の変数に代入した際にリファレンスカウンタを増やしたければ代入するだけではだめで明示的にretain
をかけてやる必要がある。NSMutableArray
に加えたときリファレンスカウンタが増えるのはオブジェクトを集合に追加する際retain
をかけてくれているから。Cocoaで提供されているコレクションフレームワークは内部でretain
をかけており、removeする際release
をかけているので注意が必要だ。
AutoreleasePool
しかし、こうやってすべてのオブジェクトを管理するのは面倒だというので、AutoRelasePoolという仕組みが用意されている。このプールにオブジェクトを登録しておけば、リファレンスカウンタに関係なくプールが存在する間はオブジェクトのインスタンスも保証され、Poolの削除とともにオブジェクトも破棄されるという仕組み。Cocoaのクラスはこの仕組みが前提になっているケースが多いので、Foundationのクラスを使うターミナルのプログラムでも以下のようなコードがメイン関数に追加されている。
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; // User code ObjectA* objA = [[[ObjectA alloc] init] autorelease]; ..... [pool drain];
このように プールを作成しオブジェクトにautorelease
メッセージを投げることでプールに登録することができる。オブジェクトはプログラムでrelease
せずとも[pool drain]
で破棄される。仕組み的には[pool drain]
で登録されたオブジェクトに一斉にrelease
を投げているだけのようだ。逆に言うとプールに登録したオブジェクトといえど、リファレンスカウンタで制御されていてretain/releaseの回数のつじつまはあっていなければならない。「リファレンスカウンタに関係なく」と書いたが、実際にはプールに登録したオブジェクトに関しては「プログラム上でリファレンスカウンタに関係してはならない」が正解か。
また、NSAutoreleasePool
は複数生成することができる。
NSAutoreleasePool* pool1 = [[NSAutoreleasePool alloc] init];
printf("Pool1 is created\n");
ObjectA objA = [[ObjectA alloc] initWithString:@"objA"];
[objA autorelease]; printf("Pool2 is created\n");
NSAutoreleasePool* pool2 = [[NSAutoreleasePool alloc] init];
ObjectA objB = [[ObjectA alloc] initWithString:@"objB"];
[objB autorelease]; printf("Pool2 is drained\n");
[pool2 drain]; printf("Pool1 is drained\n");
[pool1 drain];
結果は
Pool1 is created. Pool2 is created. Pool2 is drained. objB is released. Pool1 is drained. objA is released.
つまりオブジェクトは直近に生成されたプールにより制御される。一つのプールに依存すると、プログラムが終了するまで多量のインスタンスを抱えることになりかねない。多くのメモリーを消費するクラスや多量のインスタンスを生成する処理では、対象のインスタンスを使用するスコープでNSAutoreleasePool
を生成し不要となったら破棄するのがベター。
Objecitve-Cにおけるretain/release/autoreleaseの方針
実は、このAutoreleasePoolがあるので最初にObjective-Cを書こうと思ったときこのメモリー管理で訳が分からなくなる。上記は簡単な例で説明したが、通常はオブジェクトはインスタンス変数に別のオブジェクトの参照を持っておりそれぞれのライフサイクルが異なっているのは普通で、しかもそれぞれ自身でretain/release
をかけていたり、AutoreleasePoolで管理されていたりと管理方法もバラバラになってしまう。で、結局retain/release
のバランスが崩れて、二重解放してしまったり解放されないオブジェクトができたりする。
いくつかの実験とWebサイトなどの情報をまとめると、以下のような方針がよいようだ。
- 自身が生成したオブジェクトは自分で解放する。つまり、
alloc-init
でオブジェクトを生成したメソッド、またはクラスは自身の責任はrelease
をかけるか、生成時にautorelease
を投げておく。 - CocoaのFrameworkでは、クラス名が先頭についた生成メソッドは
autorelease
がかかっている。例えば、NSArray
における+arrayWithObjects
や、NSString
における+stringWithCString
など。これらについては、積極的にこれらの生成メソッドを使用しalloc - init***
で生成しない。 - 同様に自作のクラスについては、CocoaのFrameworkに習いなるべく+クラス名の生成メソッドを用意しその中でautoreleaseをかけるなどして、自身のオブジェクトのライフサイクルに関しては自身で責任を取る。
- インスタンス変数については、そのオブジェクトの由来に関わらずretainをかけ、deallocの中でreleaseを入れる。こうすれば、例えばAutoreleasePoolに登録されているオブジェクトでも辻褄は合う。
- 局所的に多量のインスタンスを生成する場合(特にループ内など)は、全体の
NSAutoreleasePool
の制御に任せるとメモリーをどんどん消費していくので、大量の一時的インスタンスを生成するようなメソッドやループ内ではローカルのNSAutoreleaseaPoolを適用する。
コメント
コメントを投稿