オートリリースプールの使い方と基本 : Objective-C プログラミング
PROGRAM
オートリリースプールについて
Objective-C では、オートリリースプールという生成した Objective-C クラスインスタンスの自動解放を行う仕組みが用意されています。
ARC (Automatic Reference Counting) の登場で Objective-C クラスインスタンスの管理も原則自動化されましたけど、内部ではこれまで通り retain や autorelease 等を駆使してメモリ管理が行われています。
そんな ARC が搭載されて久しいですけど、久しぶりにオートリリースプールに注目する機会があったので詳細を詰めてみたところ、知らないことがいくつか出てきてしまいました。
そんな訳で、今回はオートリリースプールの基本も含めて、整理してみたいと思います。
オートリリースプールの使い方(変数)
Non-ARC 環境の場合
Non-ARC 環境の場合、autorelease メソッドを呼び出すことで、そのインスタンスをオートリリースプールに登録することができます。
NSString* string = [[[NSString alloc] init] autorelease];
新規作成のときだけでなく、受け取ったインスタンスを確保 (retain) してそれをオートリリースプールに入れることもできます。
[[string retain] autorelease];
このようにして、オートリリースプールに登録したインスタンスは、登録したオートリリースプールのルールに従って、自動的に release されるようになります。
ARC 環境の場合
ARC 環境では __autoreleasing オーナーシップが指定された変数へインスタンスを受けることで、オートリリースプールに登録されます。
__autoreleasing NSString* string = aString;
こちらも Non-ARC 環境のときと同様、登録したオートリリースプールのルールに従って、自動的に release されるようになります。
オートリリースプールの基本
Non-ARC 環境の場合
オートリリースプールは、Non-ARC 環境では NSAutoreleasePool クラスによって管理されています。
このインスタンスが生成されたところから、それが破棄されるまでの間に NSAutoreleasePool に追加されたインスタンスを、破棄されるときに一緒に解放するのが NSAutoreleasePool の役目です。
NSAutoreleasePool は入れ子にすることができ、何か Objective-C インスタンスを autorelease すると、最後に生成した NSAutoreleasePool にそのインスタンスが追加されます。
たとえば、非 ARC 環境の場合は、次のようにしてオートリリースプールを使用できます。
// NSAutoreleasePool のインスタンスを作成します。
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// 以下で autolerease したインスタンスは、上で作成したオートリリースプールに追加されます。
@try
{
NSString* string = [[[NSString alloc] init] autorelease];
NSDate* date = [[[NSDate alloc] init] autorelease];
}
@finally
{
// NSAutoreleasePool を解放した時点で、そこに追加されたインスタンスも全て release されます。
[pool release];
}
または、Xcode 4.2 以上(LLVM Compiler 3.0 以上?)であれば、NSAutoreleasePool の代わりに @autoreleasepool も使用できるようになりました。
こちらも、NSAutoreleasePool のような "インスタンスを作る" という動きはありませんけど、括られたスコープ内で autorelease されたインスタンスが、スコープを抜けるタイミングで release されます。
// オートリリースプールを作成します。
@autoreleasepool
{
// このスコープ内で autolerease したインスタンスは、抜けたときに解放されます。
NSString* string = [[NSString alloc] init] autorelease];
NSDate* date = [[NSDate alloc] init] autorelease];
}
こちらの方が見た目がすっきりしますね。
また、iOS 5.0 以上や OS X 10.7 以上だと、NSAutoreleasePool を使う方法よりも @autoreleasepool を使う方が高速らしいので、可能であればこちらを積極的に使って行くと良さそうです。
ちなみに iOS 4.3 でも @autoreleasepool はエラーなく動作します。
ARC 環境の場合
ARC 環境の場合、NSAutoreleasePool クラスは使用できないため、必ず @autoreleasepool を使ってオートリリースプールを管理する必要があります。
使い方や特徴は、上記の Non-ARC 環境のときと同じです。
オートリリースプールの使いどころ
長いループの中で使用する
オートリリースプールは通常、裏で自動的に生成・破棄されているので、基本的には意識しないで autorelease が利用できるようになっています。
ただし、無造作に独立して動いている訳ではないので、処理を長くキープし続けるような場面では、意図的にオートリリースプールを解放してあげないと、解放待ちのインスタンスでメモリが溢れてしまう危険性があります。
// 長いループの中でオートリリースプールを生成することで、解放待ちのインスタンスが溜まってしまうのを防ぎます。
while (!isCancelled)
{
@autoreleasepool
{
// この中で生成された autorelease なインスタンスは…
self.label.text = [NSString stringWithFormat:@"Step: %d", self.step];
}
// 次のループ処理に入る前に release されます。
}
そのような場合には、ループの中にオートリリースプールを作ることで、解放待ちで溜まってしまうようなことが防げます。
スレッドを自分で立ち上げた場合
スレッドを自分で立ち上げた場合も、オートリリースプールにも気を遣わなければいけない場合が出てきます。
基本的に、スレッドを自分で立ち上げた場合はオートリリースプールが何も存在していない状態になるらしいので、そのような状態で autorelease を使用すると、次のようなエラーがログに表示されてしまうことになります。
*** __NSAutoreleaseNoPool(): Object 0x469c of class NSCFString autoreleased with no pool in place - just leaking
これはつまり、インスタンスが retain & autorelease されたものの、それを溜めておくオートリリースプールがないため、解放されることがないということを意味します。
この状況を回避するためには、適切なタイミングでオートリリースプールを自分で作成してあげる必要があります。
適切なタイミングというのは、一般的には、別スレッドで処理が開始された最初と最後です。
最初にオートリリースプールを作成して、最後にオートリリースプールを解放してあげるようにすれば、そのスレッド内で使用した autorelease なインスタンスは、最後には必ず解放されるようになります。
もちろん、そのスレッドの中で長いループ処理を行う場合は、先ほど記したように、ループ内でもうひとつオートリリースプールを作成する必要が出てくるかもしれません。
または、そのスレッド内で autorelease を一切使用しないのであれば、そもそもオートリリースプールを作らなくても大丈夫です。
このあたりの判断は、状況に合わせてというところもありますが、この状況の違いには iOS のバージョンも少し絡んでくるようでした。
iOS 4.3 系の場合
別スレッドを作成した時のオートリリースプールの扱い方でよく言われる、別スレッドで立ち上げた処理の最初でオートリリースプールを作るという話ですか、試した感じ、これはどうも iOS 4 の頃のセオリーのようです。
確かに iOS 4.3 では、NSThread の detachNewThreadSelector:toTarget:withObject や NSObject の performSelectorInBackground:withObject: で立ち上げたスレッドの場合、オートリリースプールを作成しておかないと、上記の "just leaking" 警告メッセージが表示されてしまいました。
ちなみに NSOperation と NSOperationQueue によるスレッドの場合は、別スレッドでの処理を行う main メソッドの前後でオートリリースプールを管理してくれるのか、"just leaking" 警告メッセージは表示されませんでした。
また、後でも再度整理しますが、+load メソッド内で autorelease なインスタンスを使用した場合にも、"just leaking" 警告メッセージが表示されました。
実際に autorelease なインスタンスは解放されないので、どうやら +load の段階でも、オートリリースプールは存在していない様子です。
iOS 5 系の場合
それが iOS 5 になると、+load のときを除いて、上記の警告メッセージは表示されなくなります。
iOS 4 では警告になっていた NSThread の detachNewThreadSelector:toTarget:withObject や NSObject の performSelectorInBackground:withObject: は警告が出なくなりました。
確かに、インスタンスも実際に解放されるので、どうやらその前後でオートリリースプールが正しく機能している様子です。
+load では相変わらず "just leaking" 警告メッセージが表示されます。
インスタンスも実際に解放されないので、この時点ではまだオートリリースプールが存在していないものと思われます。
この +load ですけど、iOS 5 ではもう一つ注意したいところがあって、この中で NSLog を使用すると、それ以下で行われた autorelease に対しては "just leak" 警告メッセージが表示されない様子です。
それでも +load のスコープを抜けても、そこで作成したインスタンスは release されませんでした。そしてアプリが終了する時を含めて、最後まで release されることはありませんでした。
iOS 6 の場合
これが iOS 6 になると、+load でも上記の警告メッセージが表示されなくなります。
ここで作成した autorelease なインスタンスも直ちに release されるので、こちらでも適切な範囲でオートリリースプールが機能している様子でした。
iOS 6.0 の場合は少なくとも、今回試した NSThread の detachNewThreadSelector:toTarget:withObject と NSObject の performSelectorInBackground:withObject: と +load では、自前でオートリリースプールを作らなくても大丈夫そうです。
もっとも、スレッドの開始の前後だとか、初期化時の 1 回だけのことなので、わざわざ OS の違いでオートリリースプールを作り分けるほどの価値はない気もします。
とりあえず iOS のバージョンが上がるほど、オートリリースプールを作り忘れるミスでメモリーリークする可能性が減っていると思っておけば、それで十分なのかもしれません。
+load メソッドで使用する場合
先ほどのおさらいになりますけど、+load メソッドでのオートリリースプールの挙動について整理しておくことにします。
まず +load というのは、それを実装しているクラスが Objective-C ランタイムに認識された最初の 1 度だけに実行されるクラスメソッドです。高度なクラス操作を行う場合や、クラスで共通の静的変数を初期化する場面で、使う機会もあるかもしれません。
この +load でのオートリリースプールの準備ですけど、iOS 6.0 からは、このメソッドが呼ばれた時に、既にオートリリースプールが用意されているようでした。
iOS 6.0 であれば、このメソッド内でオートリリースプールを作らずに autorelease なインスタンスを使用しても、メソッドを抜けた頃合いでそのインスタンスが解放されます。
ただし、iOS 4.3 と iOS 5.0、iOS 5.1 では、+load メソッドの中でオートリリースプールを作らないと、autorelease なインスタンス最後まで解放されません。
オートリリースプールのないところで autorelease を使うと、通常はデバッグコンソールに "autoreleased with no pool in place - just leaking" という警告メッセージが表示されるようになっているのですけど、何故か iOS 5.0 や 5.1 では、+load メソッド内で NSLog を使ってしまうと、メソッド内のそれ以下のコードで autorelease を使っても、この警告メッセージが表示されなくなるので注意が必要です。
単に表示されないだけで、オートリリースプールが生成された訳でもないようで、そこで autorelease したインスタンスは最後まで解放されることはありませんでした。
オートリリースプールの使い方(メソッドの戻り値)
最後に補足として、メソッドの戻り値で autorelease を指定するルールについて見て行きます。
関数の戻り値は、原則として "alloc", "init", "copy", "mutableCopy", "new" で始まる名前のメソッド以外の場合は autorelease した状態のインスタンスを戻り値にする必要があります。
Non-ARC 環境の場合
Non-ARC 環境の場合、このルールをプログラマが自分自身で行います。
- (NSString*)string
{
}
ルールにプログラマーが従うことで、そのメソッドを呼び出した側が、受け取った値を自分で release しないといけないのか、それともオートリリースプールが自動解放してくれるのかが判るようになっています。
ARC 環境の場合
ARC 環境でもこの流れを汲んでいて、これらの "alloc", "init", "copy", "mutableCopy", "new" で始まるメソッド名以外の場合は、戻り値を自動的に autorelease してくれます。
例えば "getString" という名前のように上記のキーワードで始まっていないメソッドの場合は、次のように書くだけで、オートリリースプールに入れてくれます。
- (NSString*)getString
{
// ARC が autorelease と同じ働きを加えてくれます。
return _string;}
逆に copyString のように "copy" というキーワードから始まっているメソッドの場合は、先ほどと同じように書くだけで、retain されたインスタンスを返すようになります。
- (NSString*)copyString
{
// ARC が retain と同じ働きを加えてくれます。(名前は "copy" でも copy は呼ばない)
return _string;}
同じプログラムで異なる動作をするので、一見すると混乱しやすそうにも思えますけど、Objective-C の流儀である "alloc", "init", "copy" などのメソッド名の重みが体に染みつくにつれて、とても楽にプログラミングできるようになります。
このような自動化が不都合な場合には、メソッド名の後ろに属性を指定することで、ARC の挙動を変更することもできます。
- (NSString*)string NS_RETURNS_RETAINED;
上のように、メソッド定義の後ろに "NS_RETURNS_RETAINED" を加えることで、"alloc", "init", "copy", "mutableCopy", "new" などのときと同じように、戻り値を retain だけした状態で返すことができます。
- (NSString*)string __attribute__((ns_returns_autoreleased));
または、上のように "__attribute__((ns_returns_autoreleased))" 付けることで、メソッド名に関わらず autorelease された戻り値を返すことができます。
なお、"alloc", "init", "copy", "mutableCopy", "new" という名前は、ARC 環境では "メソッドファミリ" と呼ばれていて、それぞれによって配慮のされ方が僅かに違う場合があります。
例えば "init" ファミリでは、自身と同じかそこから派生したクラスを retain したまま返す必要に迫られます。
このような特徴を、例えば "init" で始まらない名前のメソッドを "init" と同様に扱いたい場合には、メソッドの定義のところで "__attribute__((objc_method_family(init)))" という指定をすることで、そのメソッドを指定したメソッドファミリとして扱うことが可能になります。
ただ、このような方法でメソッドの種類を管理してしまうと、どのメソッドがどんな状態のインスタンスを返すかが判らなくなって混乱します。
ARC 環境であればその辺りをコンパイラが自動で調整してくれるので問題は起こりにくいですけど、Non-ARC 環境と組み合わせて使用するような場面では破綻するのが目に浮かぶので、余程の理由があってもこれらの指定は行わないのが賢明なように思います。
[ もどる ]