あるクラスが、インスタンス変数として別のクラスのインスタンスを保持する場合があります。このときにも、オブジェクトの所有権について考慮しておく必要があります。
インスタンス変数という「容れ物」にオブジェクトを保存するとき、これが勝手に消えてしまわないように所有権を確保しておくのです。
第3回「Objective-Cのクラス定義を理解しよう」で触れたように、クラスのインスタンス変数にはアクセサというメソッドを通してアクセスします。
インスタンス変数に何らかのオブジェクトをセットするアクセサはセッターメソッドと呼ばれます。オブジェクトの所有権は、このセッターメソッドで確保すればよいことになります。
次のサンプルを見てみましょう。
01 #import <Foundation/Foundation.h>
02
03 @interface MyClass : NSObject {
04 // インスタンス変数
05 NSObject *myObj1, *myObj2;
06 }
07 // アクセサの宣言
08 - (NSObject *)myObj1;
09 - (void)setMyObj1:(NSObject *)argMyObj1;
10 - (NSObject *)myObj2;
11 - (void)setMyObj2:(NSObject *)argMyObj2;
12
13 @end
14
15 @implementation MyClass
16
17 - (NSObject *)myObj1 {
18 return myObj1;
19 }
20 // セッターメソッドの実装1: retainする
21 - (void)setMyObj1:(NSObject *)argMyObj1 {
22 if (myObj1 != argMyObj1) {
23 [myObj1 release];
24 myObj1 = [argMyObj1 retain];
25 }
26 }
27
28 - (NSObject *)myObj2 {
29 return myObj2;
30 }
31 // セッターメソッドの実装2: retainしない
32 - (void)setMyObj2:(NSObject *)argMyObj2 {
33 myObj2 = argMyObj2;
34 }
35
36 @end
37
38
39 int main(void) {
40
41 MyClass *myCls = [[MyClass alloc] init];
42
43 NSObject *testObj1 = [[NSObject alloc] init]; // (a)
44 myCls.myObj1 = testObj1; // (b)
45 printf("%d\n", [myCls.myObj1 retainCount]); // (c)
46
47 [testObj1 release]; // (d)
48 printf("%d\n", [myCls.myObj1 retainCount]); // (e)
49
50 NSObject *testObj2 = [[NSObject alloc] init]; // (f)
51 myCls.myObj2 = testObj2; // (g)
52 printf("%d\n", [myCls.myObj2 retainCount]);
53
54 [testObj2 release]; // (h)
55 //printf("%d\n", [myCls.myObj2 retainCount]); // (i)
56
57 [myCls release];
58
59 return 0;
60 }
MyClassというクラスには2つのインスタンス変数(myObj1とmyObj2)があり、それぞれにアクセサが用意されています。myObj1のセッターメソッドは、受け取ったインスタンスをretainしたうえで、自クラスのインスタンス変数にセットします。一方、myObj2のセッターメソッドはretainしません。
実行プログラム(main関数)側で、オブジェクトを1つ生成しています(a)。この時点で生成したオブジェクトの参照カウンタは1です。
次にこのオブジェクトを、セッターメソッドを介してMyClassのmyObj1というインスタンス変数にセットします(b)。
すると、セッターメソッド(setMyObj1)側でもこのオブジェクトに対してretainを行うので、参照カウンタは2になります(c)。
main関数側にとって、testObj1はMyClassに渡すために生成したオブジェクトなので、セッターメソッドに渡した時点で不要になったと判断し、relesseします(d)。この時点でMyClassが独自に確保した所有権は残っていますので、オブジェクトの参照カウンタは1となり、まだ解放されずに残っています。
ここでmain関数側が責任を持つべきtestObj1の所有権は、最初にallocした分だけですので、それ以上にtestObj1をreleaseしてはいけません。クラスに渡したあとの所有権の管理はクラス側に委ねられます。
次に、別途生成したオブジェクトを、今度はMyClassのmyObj2というインスタンス変数のセッターメソッドに渡します(f、g)。生成した時点でオブジェクトの参照カウンタは1です。myObj2のセッターメソッドはretainをしていないので、gの時点でもやはり参照カウンタは1です。
先ほどと同じく、main関数側にとって、testObj2はMyClassに渡すために生成したオブジェクトなので、不要になったと判断しreleaseします(h)。
この時点でtestObj2の参照カウンタは0になってしまい、MyClassのインスタンス変数myObj2はアクセスできなくなってしまいます。iのコメントを外すとエラーになります。
このような理由から、インスタンス変数がオブジェクト型の場合(かつガベージコレクションを利用していない場合)には、クラス側でretainするようにセッターメソッドを実装するのが通例となっています。
これは、やはり第3回で解説した@propertyや@synthesizeによるアクセサメソッドの自動生成の場合にも同様です(自動生成の場合には、@propertyのあとにretainやcopyといったキーワードを指定することで、インスタンス変数の所有権を確保できます)。
なお、上記サンプル中のsetMyObj1 は、retainしたうえでインスタンス変数をセットするセッターメソッドの典型的な実装例となっています。基本的な処理内容は、すでにインスタンス変数に保存されているオブジェクトをまず解放し、新たに受け取ったオブジェクトをretainしたうえでインスタンス変数に保存しています。
ただし、そのインスタンス変数にもともと保存されていたオブジェクトと、あらたに受け取ったオブジェクトがまったく同一のインスタンス(実体)であった場合は何もしないようにという判定がなされている点に注意してください。この判定がないと、旧インスタンス変数を解放したつもりが、新たに受け取ったオブジェクトも解放してしまう可能性があります。
さきほどのサンプルで、main関数側では、オブジェクトを生成した時点で生じる所有権のみ、責任を持って解放(release)していました。これでmain関数側の文脈では、所有権の確保と解放のつじつまが合います。
一方、MyClassクラス側では、セッターメソッド(setMyObj1)で、インスタンス変数のセット時に所有権を確保しておくようにしていますが、これを最終的に解放する処理がどこにもありません。
クラスが所有権を持つオブジェクトに関しては、その所有権の解放に関してもクラス自身で責任を持つ必要があります。通常、クラスが持つインスタンス変数は、そのクラス自身が破棄される(メモリが解放される)時点で不要となりますので、クラスの破棄と同時に実行されるメソッドのなかで、まとめてreleaseすればよいことになります。
そこで利用されるのが、deallocメソッドです。名前が示すとおり、alloc(割り当て)の反対の意味を持つメソッドです。
deallocメソッドはNSObjectに定義されているメソッドなので、あらゆるクラスで実行できます。このメソッドは、参照カウンタが0になってクラスのインスタンスが解放される直前に実行されます。初期化メソッドやセッターメソッドで確保したオブジェクトの所有権は、ここでまとめて解放しましょう。
なお、deallocメソッドはインスタンス解放時に自動的に実行されるものであり、開発者がコード上で明示的に呼び出すものではありません。
以下にサンプルを示します。
01 #import <Foundation/Foundation.h>
02
03 @interface MyClass : NSObject {
04 NSObject *myObj;
05 }
06 - (NSObject *)myObj;
07 - (void)setMyObj:(NSObject *)argMyObj;
08 @end
09
10 @implementation MyClass
11 - (NSObject *)myObj {
12 return myObj;
13 }
14 - (void)setMyObj:(NSObject *)argMyObj {
15 if (myObj != argMyObj) {
16 [myObj release];
17 myObj = [argMyObj retain];
18 }
19 }
20 - (void)dealloc { // (a)
21 NSLog(@"dealloc called. release `myObj`."); // (b)
22 [myObj release]; // (c)
23 [super dealloc]; // (d)
24 }
25 @end
26
27
28 int main(void) {
29
30 NSObject *testObj = [[NSObject alloc] init]; // (e)
31 printf("%d\n", [testObj retainCount]); //
32
33 MyClass *myCls = [[MyClass alloc] init];
34 myCls.myObj = testObj; // (f)
35 printf("%d\n", [testObj retainCount]); //
36
37 [testObj release]; // (g)
38 printf("%d\n", [myCls.myObj retainCount]); //
39
40 [myCls release]; // (h)
41 //printf("%d\n", [testObj retainCount]); //
42
43 return 0;
44 }
a.自作クラスにdeallocメソッドを定義しています。
b.deallocメソッドが確かに実行されていることを確認するために、ログを出力してみます。
c.ここで、MyClassのインスタンス変数であるmyObjをreleaseするようにしておきます。ほかにも所有権を保持しているオブジェクトがあれば、ここですべてreleaseします。
d.継承の親クラスでオブジェクトを所有している場合もありますので、最後に必ず親クラスのdeallocメソッドを呼び出します。
e.main関数側でオブジェクト(testObj)を生成します。この時点でtestObjの参照カウンタは1です。
f.MyClassのインスタンス変数(myObj)に、testObjをセットします。myObjのセッターメソッドでretainが実行され、MyClassでも所有権を確保します。この時点で、testObjの参照カウンタは2になります。
g.main関数側では、MyClassに渡した時点でtestObjは不要となりますので、ここでreleaseしてしまいます。この時点で、testObjの参照カウンタは1になります。testObjの実体はまだ解放されませんが、main関数側の守備範囲はここで終わりです。
h.main関数側で、MyClassのオブジェクトをreleaseします。deallocは直接呼び出していませんが、ログの表示からこのタイミングでdeallocが実行されていることが分かります。これにより、MyClassが保持している分の所有権がreleaseされ、testObjは完全に解放されます。その次のretainCount取得の行のコメントを外すとエラーになります。
今回はメモリ管理に関するトピックの前半として、参照カウンタの基本的な仕組みについて解説しました。最終回となる次回は、メモリ管理の定石や、自動のメモリ管理について解説する予定です。
Copyright © ITmedia, Inc. All Rights Reserved.