メモリ管理を理解する(後編)Cocoaの素、Objective-Cを知ろう(8)(1/2 ページ)

iPhone用アプリケーション開発で注目を集める言語「Objective-C」。C++とは異なるC言語の拡張を目指したこの言語の基本を理解しよう(編集部)

» 2009年08月05日 00時00分 公開
[竹下肯己株式会社 qnote]

メモリのretainやreleaseの定石

 前回「メモリ管理を理解する」では、オブジェクトの所有権の取得と解放の基本的な仕組みについて説明しました。

 ここで紹介したようなごくシンプルなプログラムであれば、どこでretainし、どこでreleaseするかについてあまり迷うことはないでしょう。

 しかし、一般的なアプリケーションの規模になると、多くのクラスやオブジェクトが複雑に関わってきますし、それらを多人数で分担して開発する場合もあります。

 そうなると、releaseを忘れて不要なオブジェクトがメモリ上に溜まっていき、メモリを食いつぶしてしまったり(これをメモリリークといいます)、逆に所有権を確保しなかったせいでオブジェクトにアクセスできなくなって異常終了したりといったことも起こり得ます。

 これを防ぐためには、ある程度の範囲でメモリ管理の責任を切り離して考え、それぞれの文脈のなかでつじつまが合うように実装していく必要があります。

メモリ管理の責任範囲

 オブジェクト指向でプログラムを作成する場合、処理単位はクラスとそのメソッドです。アプリケーション実行の一番外側はC言語のmain関数となりますが、アプリケーションの具体的なロジックをmain関数に直接記述することはほとんどありません。

 ですから、プログラムのメモリ管理の責任範囲は、クラスとメソッドの単位で切り離して考えればよいことになります。

 クラス単位で責任を持つべきオブジェクトは、そのメンバとなるインスタンス変数です。これは、前回のサンプルで示したセッターメソッドや、ほかにも例えばinitなどの初期化メソッドのなかでretainされ、所有権が確保されています。これらは、deallocメソッドでまとめてreleaseします。

 メソッド単位で責任を持つべきオブジェクトは、メソッドの内部で生成した(その時点でretainされた)オブジェクトです。メソッドのなかだけで使用済みとなった場合、これを解放する必要があります。

 一方、メソッド終了後もそのクラスで必要となるオブジェクトの場合、インスタンス変数にセットしておきます。その時点でクラス単位での責任範囲となりますので、最後にdeallocメソッドでreleaseします。

 また、同じくメソッドの内部で生成した(その時点でretainされた)オブジェクトをクラス側では保持せず(つまりインスタンス変数にセットせず)、メソッドの戻り値として返す場合があります。

 この場合も、生成時に発生した所有権についてはメソッドの責任範囲にありますが、メソッド内ですぐ解放してしまっては、戻り値を受け取った側で利用できなくなってしまいます。

 しかしながら、deallocで解放しようとしてもインスタンス変数で保持していないので、アクセスすることができません。このような場合には、後述するautoreleaseメソッドによって、のちのち自動解放されるように予約しておきます。

 メソッドが引数として受け取ったオブジェクトについては、受け取った時点ではメソッドの責任範囲にはありません。メソッド終了後も必要となる場合のみ、retainして独自に所有権を確保したうえで、インスタンス変数にセットしておきます。その時点でクラス単位の責任範囲となりますので、最後にdeallocメソッドでreleaseします。

 なお、オブジェクト生成時のalloc、明示的なretain のほかに、copyやmutableCopyでオブジェクトをコピーした場合にも所有権が発生(参照カウンタが1プラス)しますので注意してください。

半自動のメモリ管理(autorelease)

 releaseでメモリ領域を解放するということは理解いただけたと思います。しかし、実際にはロジックのなかで一時的に使うオブジェクトなど、いちいちreleaseのタイミングを考慮しながら実装するのは面倒ですし、メモリリークの原因にもなりがちです。

 このような場合を考慮して、Objective-Cにはオブジェクトを一定の範囲の最後でまとめて自動解放する仕組みが用意されています。この仕組みは、これまで連載で紹介したサンプルにもすでに何度か登場しています。

 自動解放の基本的な仕組みは、まずプログラムの特定の範囲(例えばmain関数の最初と最後や、アプリケーションのイベント開始から終了までなど)の開始部分で、一時オブジェクトの容れ物を用意します。次に、その範囲内で一時オブジェクトを作成した場合は、この容れ物に入れます。そして、その範囲の最後の部分で、一時オブジェクトの容れ物の中身をまとめて解放します。

 ごく簡単なサンプルを見てみましょう。

01 #import <Foundation/Foundation.h>
02 
03 @interface MyClass : NSObject {
04 }
05 - (NSObject *)createObj1;
06 - (NSObject *)createObj2;
07 @end
08 
09 @implementation MyClass
10 - (NSObject *)createObj1 {
11     NSObject *obj = [[NSObject alloc] init];                // (a)
12     return obj;
13 }
14 - (NSObject *)createObj2 {
15     NSObject *obj = [[[NSObject alloc] init] autorelease];  // (b)
16     return obj;
17 }
18 @end
19 
20 int main(void) {
21 
22     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // (c)
23 
24     MyClass *myClass = [[MyClass alloc] init];
25 
26     NSObject *testObj1 = [myClass createObj1];  // (d)
27     printf("%d\n", [testObj1 retainCount]);     //
28 
29     NSObject *testObj2 = [myClass createObj2];  // (e)
30     printf("%d\n", [testObj2 retainCount]);     //
31 
32     [pool drain];                               // (f)
33 
34     printf("%d\n", [testObj1 retainCount]);     // (g)
35     //printf("%d\n", [testObj2 retainCount]);   //
36 
37     return 0;
38 }
main.m

a. createObj1メソッドは、alloc→initで生成したオブジェクトをそのまま戻り値として返します。

b. 一方createObj2メソッドは、alloc→initで生成したオブジェクトから、さらにautoreleaseメソッドを実行したうえで戻り値としています。

 このautoreleaseメソッドは、その時点ですでに用意されているはずの一時オブジェクトの容れ物(後述のNSAutoreleasePool)に、オブジェクトを登録しておくためのメソッドです。autoreleaseが実行された回数だけ、あとでreleaseされることになります。

c. ここでは、main関数全体を自動解放の制御範囲とするため、まずはmain関数の最初の部分で、一時オブジェクトの容れ物であるNSAutoreleasePoolのインスタンスを生成しています。これ以降、autoreleaseが実行されたオブジェクトは、このNSAutoreleasePoolのインスタンスに登録されることになります。

d. NSObjectのオブジェクトを1 つ取得しています(testObj1)。このとき、MyClassのcreateObj1メソッド(autoreleaseされていないオブジェクトを返すメソッド)を利用しているため、取得されたオブジェクトは、今後解放される見込みがありません。

 しかし、main関数側としては、testObj1を自分でallocしたわけではありませんので、releaseする対象とはなりません(自分の責任範囲にありません)。これでは、testObj1は片付けることができなくなってしまいます。

e. 次に、NSObjectのオブジェクトをもう1つ取得しています(testObj2)。このとき、MyClassのcreateObj2メソッド(autoreleaseしてからオブジェクトを返すメソッド)を利用しているため、取得されたオブジェクトは、のちのち自動解放されることが約束されています。

f. ここで自動解放の制御範囲が終了となるため、一時オブジェクトの容れ物であるNSAutoreleasePoolを解放します。解放にはdrainメソッドを利用します。この時点で、容れ物に登録されたオブジェクト(この例ではtestObj2)もreleaseされます。

g. testObj1は誰も解放していないので、参照カウントは1のままです。こういったオブジェクトが蓄積していくと、メモリリークの原因となります。一方、testObj2はfですでに解放されています。上記のコメントを外すとエラーになります。

autorelease済みのオブジェクトを返すメソッド

 第4回「変数のデータ型や文字列の扱いを理解しよう」の文字列クラス(NSStringクラス)の説明のところで、オブジェクトの初期化メソッドinitWithCString:encoding:と、オブジェクトの生成メソッドstringWithCString:encoding:について述べました。

 initWithCString:encoding:は、allocしたあとに呼び出すことでオブジェクトを初期化します。この方法でオブジェクトを生成した場合、releaseのタイミングは自分で考慮する必要があります。

 一方、stringWithCString:encoding:は、alloc→init→autorelease まで手続きを済ませたオブジェクトを返してくれます。すでにNSAutoreleasePoolが用意されている文脈内でこのメソッドを利用してオブジェクトを生成すれば、のちのオブジェクト解放について意識する必要はなくなります。

 Objective-Cの多くのクラスでは、上記のように単に初期化を行うメソッドと、alloc→init→autorelease を一気に行ってオブジェクトを生成してくれるメソッドの2パターンが用意されています。

 これらはメソッドのネーミングルールが統一されており、初期化メソッドは「initWith〜」、インスタンス生成メソッドは「xxxWith〜(xxxには "string" や "array" など、オブジェクトの汎用的な名称が入ります)」のようになっていますので、慣れればすぐに使い分けられるようになるでしょう。

       1|2 次のページへ

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。