Objective-Cとガーベッジコレクション

iPhone購入してapp開発に興味が出、仕事でも使う可能性が高まりだした今日この頃、

iPhone開発でおすすめの本 - Seasons.NET

なる良エントリを見つけ、以下の本を購入しObjective-Cを勉強し始めました。久々のコンパイル言語なだけに多少慣れるまで時間がかかったのもありますが、ガーベッジコレクションが結構くせ者だったので備忘録。スクリプト言語によく見られる一般的なGCは2.0から導入されたんですけど、iPhone app開発ではGC使えません。ってことでリファレンスカウンタ方式というメモリ管理法を学びました。理屈はとても簡単です。

詳解 Objective-C 2.0

詳解 Objective-C 2.0

動的にインスタンス生成する方法は典型的には以下のように。

id obj;
obj = [[MyClass alloc] init];
[obj msg];

id型の変数を宣言して*1、MyClassをalloc(確保)してinit(初期化)する。オブジェクトのメソッドを呼び出すのは、obj->methodとかじゃなくて[obj msg]っていう形。"メッセージ"ですよ。

retainとrelease

生成されたインスタンスはretain counter(保持カウンタ)っていうのを持っている。値は[obj retainCount]でint型(NSInteger型)で返ってくるよ。インスタンスに割り当てられたメモリはこの値が0になったとき、解放(dealloc)される。アプリケーション側からdeallocメソッドを直接呼び出してはダメ。保持カウンタの増減にはretainとreleaseを使う。

[obj retain];
printf("%d", [obj reCount]); // 2
[obj release];
printf("%d", [obj reCount]); // 1
[obj release];
// 0になったのでobjは解放された。以下はエラー
// printf("%d", [obj reCount]);

なんで一度retainしただけでカウンタが2になっているかというと、この一つ前で[[MyClass alloc] init]したから。allocとinit〜のメソッド(イニシャライザ)を使うと、勝手にカウンタを1にしてくれる。init〜から始まらないイニシャライザで初期化すると、インクリメントされない(後述)。

retainとreleaseのルール

オブジェクトを生成・保持するオブジェクトがretainとreleaseで責任持ってカウンタを管理する。

autorelease

ClassAのオブジェクトが保持していたオブジェクト(インスタンス変数やメソッドの返値)をClassBのオブジェクトに渡したとき、上述のretain、releaseはどうするの?ってお話。渡したらClassAの方はオブジェクトをreleaseしたい。でもreleaseしたらClassBの方に渡せない。こんなことが起こってしまう。そんなときに役立つのがautoreleaseというメソッド。NSAutoreleasePool型のオブジェクト(pool)に、「後でまとめてreleaseしたいオブジェクトを登録する」ことが可能。登録されたオブジェクトを後で一気にreleaseするにはこのpoolオブジェクトをreleaseするか、イベントループの始めに自動でreleaseされる。poolオブジェクト自身をautorelease登録するのはダメ。

// ClassAのメソッド
- (id)giveMyObjectTo:(ClassB *)b
{
  [b setObject:myobj_];
  [myobj_ autorelease];
  return myobj_ = [[NewObj alloc] init];
}
// ClassBのセッタメソッド
- (void)setObject:(id)obj
{
  if (obj != myobj_) {                      // 既に持ってるmyobj_と渡されたobjが同一だったら、myobj_を解放できないので
    [myobj_ release];
    myobj_ = [obj retain];
  }
}
...
// main関数内で
id pool = [[NSAutoreleasePool alloc] init]; // autoreleaseされると直近のこのpoolに登録される
id a = [[ClassA alloc] initWith: obj];     // ClassAからobjというオブジェクトを持ったインスタンスとして初期化したとする
id b = [[ClassB alloc] init];

[a giveMyObjectTo: b];                     // bに渡された。だけどこの時点ではaのmyobj_はreleaseされていない
[pool release];                            // 明示的にpoolをrelease

一時的なインスタンス

先程、「init〜から始まらないイニシャライザで初期化」と書いた。例えば

id array = [NSArray array];
/*
id array = [[[NSArray alloc] init] autorelease];
とするのと同じらしい
*/

これもインスタンスを生成して初期化しているには間違いないが、alloc&init〜とは違って、既にautorelease登録されている。一時的に使うインスタンスを生成するには打って付け。一般的に小文字クラス名から始まるこのメソッドは、コンビニエンスコンストラクタと呼ばれる。

循環参照

本書では「保持の循環」と言われてる。先の例でaがbを持ち、bがaを持ってたらおかしくなっちゃうよ〜っていう問題。純粋なツリー構造ではなく、循環ネットワーク系だとこの問題が起こる。そういうときはオブジェクトを保持せず、オブジェクトへのポインタを参照するインスタンス変数を設定しましょう。


こんな感じかな〜。ところどころ間違っている気はするけど。Googleのスタイルガイドなんかも勉強になりますね。とりあえず書いててわかったことは、Objective-Cはてな記法はごちゃごちゃになるよ![[]]

*1:この場合だとMyClass型でもOK