Cocoaのメモリ管理(3)

保持と解除という方法は、理屈は分かるし簡単そうに見えます。しかし、実際にやってみると意外と難しいことがわかります。そこでCocoaでは少し楽をするための仕組みを導入しています。簡単に言えば、とりあえずなんでも入れておけるごみ袋を用意して、不要になった時点でごみ袋ごと捨てちゃうという方法です。このごみ袋にあたるのが、NSAutoreleasePoolというクラスです。

ApplicationKitにおけるNSAutoreleasePool

さて、Cocoaの重要なフレームワークの一つであるApplicationKitの話から始めたいと思います。ApplicationKitは、主にGUIを持つアプリケーションを作成するためのフレームワークです。このフレームワークを利用して作ったアプリケーション(つまり、ぶっちゃけた話、ProjectBuilderとInterfaceBuilderを使って作られたNSApplicationを利用するアプリケーション)には、デフォルトで一つNSAutoreleasePoolオブジェクトが生成されています。さらに、NSApplicationが提供するイベントループ内でも、NSAutoreleasePoolオブジェクトが生成され、ループが一回転するごとに解放され、新しいNSAutoreleasePoolオブジェクトを生成します。つまり、NSAutoreleasePoolそのものを知らなくてもとりあえず使える仕組みが提供されているわけです。このごみ袋にオブジェクトを入れる方法が、NSObjectに用意されているautoreleaseメソッドです。FoundationKitとApplicationKitが提供するクラスは、基本的にNSAutoreleasePoolがある環境での動作を想定しています。

-(id)autorelease

ApplicationKitに準拠したプログラムの場合、任意のオブジェクトにautoreleaseを送っておくと、このオブジェクトはデフォルトのNSAutoreleasePoolオブジェクトに登録されます。そして、このプログラムが終了する直前にデフォルトのNSAutoreleasePoolオブジェクトとともに、登録されたオブジェクトがすべて解放されます。つまりautoreleaseは、retain&releaseの仕組みを使わないで、楽々とメモリ管理を行う仕組みなのです。

-(void)methodA

{

idobj=[[Fooalloc]init];

//objとして参照されているオブジェクトをNSAutoreleasePoolに登録する。

[objautorelease];

//このメソッドを抜けると、変数objによる参照はなくなるが、

//割り当てられたメモリは、イベントループのNSAutoreleasePoolが

//解放される時に解放される。

}

NSAutoreleasePoolの特徴

ApplicationKitが提供するNSAutoreleasePoolを使いこなすにあたって、いくつかの注意すべき特徴があります。

*デフォルトのNSAutoreleasePoolは、プログラムの最後に(つまり、メインスレッドの末端で)解放されるので、基本的にこれに登録されたオブジェクトに割り当てられたメモリはプログラムの最後まで解放されない。

*イベントループ内で毎回生成されるNSAutoreleasePoolは、ループが一回転するごとに解放されるので、基本的にこれに登録されたオブジェクトに割り当てられたメモリはイベント処理が終わると解放される。

*autoreleaseされたオブジェクトが、基本的にそのオブジェクトが登録されたNSAutoreleasePoolがreleaseされるまで解放されないということは、逆に言えば、NSAutoreleasePoolが生きている間は、オブジェクトの生存が保障されるということを意味している。

*ただし、releaseメソッドを送ることで、このようなオブジェクトでもプログラムの途中で解放することは可能である。(これは、推奨されない方法である。基本的に、同じオブジェクトに対してautoreleaseとreleaseを併用することは混乱の元になるので、止めるべきである。deallocについてはいわずもがな。オブジェクトのプログラマブルな解放のためには、後述する方法を用いるべきである。)

*例外発生時には、NSAutoreleasePool(と、登録されたオブジェクト)は解放されるので、メモリリークは起こらない。(どっちにしてもプログラムが終了するので、メモリは解放される。)

*デフォルトのNSAutoreleasePoolだけで動作するようなプログラム場合、参照が切れたオブジェクトは、基本的にメモリリークと同じ状況になる。(あえて言うなら、知った上でのメモリリーク。通常のプログラムではあまり気にする必要がない。)

*NSAutoreleasePoolに登録されているオブジェクトへのアクセス手段はないので、参照の管理には注意が必要である。

自前のNSAutoreleasePoolの利用

さて、長々と引き伸ばしてきた話題に入ろうと思います。まず、ApplicationKitが用意しているNSAutoreleasePoolだけでは、一度のイベント処理で大量にオブジェクトを生成するような処理に対応するのが難しい場合があります。また、そもそもApplicationKitを利用しないプログラム(たとえば、コマンドラインで動作するツールなど)は、この仕組みが提供されていないわけです。そこで、自前のNSAutoreleasePoolを作成して利用する意味が出てくるのです。ここであれこれいうより、まずはサンプルコードで説明しましょう。以下のコード内のobj2を見てわかるように、実はautoreleaseメソッドを呼び出すと、スレッド内で一番最近作られたNSAutoreleasePoolにオブジェクトが登録されます。つまり、テンポラリなオブジェクトは、このように自前のNSAutoreleasePoolオブジェクトを適宜用意してやれば、任意の時点でまとめて解放できるのです。

-(void)methodA

{

idobj1;

idobj2;

idarp;

obj1=[[Fooalloc]init];

obj2=[[Fooalloc]init];

[obj1autorelease];//obj1は、デフォルトのNSAutoreleasePoolに登録される。

arp=[[NSAutoreleasePoolalloc]init];

[obj2autorelease];//obj2は、arpに登録される。

[arprelease];//arp解放。この時点でobj2は、解放される。

//obj1は、上位のNSAutoreleasePoolがreleaseされるまで解放されない。

}

理解を深めるためにもう一つ例を示しておきます。

-(void)methodA

{

idtemp;

idarp;

inti;

for(i=0;i<10;i++){

arp=[[NSAutoreleasePoolalloc]init];

temp=[[[Fooalloc]init]autorelease];

//tempで現在参照されているオブジェクトは、

//arpに登録される。

[arprelease];//arp解放。

//この時点でtempで参照されるオブジェクトは解放される。

}

//この時点で10個のオブジェクトが解放されています。

}

入れ子になったNSAutoreleasePoolの利用

もうお分かりかと思いますが、NSAutoreleasePoolはいくらでも入れ子することができるのです。スレッド上で一番最近作られたNSAutoreleasePoolオブジェクトがカレントのNSAutoreleasePoolオブジェクトとして機能するわけです。さて、まずは自前のNSAutoreleasePoolを活用する前に、いくつかの注意点を示しておきます。

*NSAutoreleasePoolオブジェクトにはretainを送ってはならない。

*NSAutoreleasePoolオブジェクトにはautoreleaseを送ってはならない。

*無用なバグと混乱を避けるために、NSAutoreleasePoolオブジェクトの生成と解放は、同じ文脈上で行うべきである。(たとえば、前述の例のように、ループ内の処理の前後など)

*retain回数+1=autorelease回数がプログラム全体で成り立つことを検証する。

さて、スレッドの流れを把握しているなら、NSAutoreleasePoolオブジェクトを複数のメソッドで使うことが可能です。ApplicationKit標準のNSAutoreleasePoolオブジェクトも基本的にこの方法の例に他なりません。まずは簡単なサンプルを示します。この例では、obj2がそれにあたります。

-(void)methodA

{

idobj1;

idarp1;

arp1=[[NSAutoreleasePoolalloc]init];

obj1=[[[Fooalloc]init]autorelease];//obj1は、arp1に登録される。

[selfmethodB];

[arp1release];//arp解放。この時点でobj1、obj2は、解放される。

}

-(void)methodB

{

idobj2;

idobj3;

idarp2;

obj2=[[Fooalloc]init];

obj3=[[Fooalloc]init];

[obj2autorelease];//obj2は、呼出し元のarp1に登録される。

arp2=[[NSAutoreleasePoolalloc]init];

[obj3autorelease];//obj3は、arp2に登録される。

[arp2release];//arp2解放。この時点でobj3は、解放される。

//obj2は、arp1解放まで解放されない。

}

いままでは単純な例でしたが、最後に少し複雑な例を示します。それは、メソッドに戻り値がある場合の扱いについてです。以下のサンプルを見て下さい。このコードには、重要な項目が隠されています。つまり、NSAutoreleasePoolオブジェクトは、登録されているオブジェクトに対する登録回数を管理していて、自身が解放される時に、その登録回数分のreleaseを各オブジェクトに送っているだけだということです。つまり登録回数がそのオブジェクトの保持数より少なければ、NSAutoreleasePoolオブジェクトが解放された後でも、そのオブジェクトは生き続けます。この例では、retにretainを送ることでretの寿命を引き伸ばしています。

-(void)methodA

{

idobj;

idarp1;

arp1=[[NSAutoreleasePoolalloc]init];

obj=[selfmethodB:1];

[arp1release];//arp1解放。この時点でobjすなわちretは、解放される。

}

-(id)methodB:(int)i

{

idret;

idarp2;

arp2=[[NSAutoreleasePoolalloc]init];

ret=[[Fooalloc]init];//retの保持数は1

[retautorelease];

if(i==1){

[retretain];//retの保持数は2になる。

//autoreleaseではなく、retainを呼ぶことで、

//arp2の寿命を超えて、arp1(もしくは、

//もっと外)の文脈まで、retで参照されるオブジェクトの

//寿命を延ばすことができる。

}else{

ret=nil;

}

[arp2release];//判定で真だったら、この時点でretの保持数は1になる。

//判定で偽だったら、この時点でretは解放される。

return[retautorelease];//retを生成したので、autoreleaseを送ってから返す。

}

autoreleaseか、retain/releaseか

autoreleaseか、retain/releaseかという問題ですが、今まで見てきたようにそんなに大差はありません。状況に応じて、選択、または併用するとよいでしょう。比較的小さなあまりオブジェクトを作らないプログラムでは、ApplicationKit標準のNSAutoreleasePoolオブジェクトを使うことで、autoreleaseだけでプログラミングするのもよいでしょう。しかし、自前のNSAutoreleasePoolオブジェクトを使うような局面なら、retain/releaseでも同じようなものでしょう。ただ、NSAutoreleasePoolオブジェクトを使うことでソースコードのカスタマイズが容易になるという利点はあるかも知れません。なぜなら、オブジェクトは、NSAutoreleasePoolオブジェクトが少なくとも解放されるまで有効なのですから。

retain/release/autoreleaseの適用方針

以下に垣内さん、白山さん、高橋さんから教えていただいた適用方針をまとめたものを示します。

*基本としてオブジェクトのオーナシップを意識する。すなわち、以下の鉄則を厳守する。

【鉄則1】自分で生成したオブジェクトは、自分で解放する

【鉄則2】他人が生成したオブジェクトは、気に留めない

【鉄則3】他人が生成したオブジェクトが必要なら、必ず保持(retain)して、必要にならなくなった時点で、必ず解除(releaseorautorelease)する

*可能な限りalloc-init系の生成メソッドは使わない。【鉄則2からの派生】

*alloc、init…を使う場合は必ずautoreleaseを入れる。【鉄則1】

aFoo=[[[Fooalloc]init]autorelease];

*alloc-init系以外の生成用クラスメソッドの場合は何もしない。それらのクラスメソッドは内部でalloc-init系の生成メソッドを呼んでいるため、鉄則1が適用されていると考えるべきである。【鉄則2】

aFoo=[Foofoo];

*インスタンス変数・グローバル変数・スタティック変数に代入して、参照を残す場合は、retainをかける。【鉄則3】

globalFoo=[aFooretain];

*インスタンス変数・グローバル変数・スタティック変数の値を消す場合には、かならずreleaseかautoreleaseを入れる。【鉄則3】

//この例ではinstanceFooは、このクラスのインスタンス変数と仮定する。

-(void)setFoo:newFoo

{

[instanceFoorelease];

instanceFoo=[newFooretain];

return;

}

-(void)dealloc

{

[instanceFoorelease];

[superdealloc];

return;

}

*自分でクラスを作るときは、NSStringの+string...NSArrayの+array...のようなそのクラス専用の生成用クラスメソッドを作成して利用させる。【鉄則2の応用】

//生成用の引数がない場合

+foo 

{

return[[[Fooalloc]init]autorelease];

}

//生成用の引数が必要な場合

+fooWithBar:bar

{

return[[[Fooalloc]initWithBar:bar]autorelease];

}

*必ずループ内部でNSAutoreleasePoolを適用する。

for(i=0;i<10;i++){

arp=[[NSAutoreleasePoolalloc]init];

temp=[[[Fooalloc]init]autorelease];

[arprelease];

}