BACK INDEX NEXT

Cocoaのメモリ管理(3)

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

Application KitにおけるNSAutoreleasePool

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

- (id)autorelease

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

- (void)methodA
{
    id obj = [[Foo alloc] init];

    // objとして参照されているオブジェクトをNSAutoreleasePoolに登録する。
    [obj autorelease];
    // このメソッドを抜けると、変数objによる参照はなくなるが、
    // 割り当てられたメモリは、イベントループのNSAutoreleasePoolが
    // 解放される時に解放される。
}

NSAutoreleasePoolの特徴

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

自前のNSAutoreleasePoolの利用

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

- (void)methodA
{
    id obj1;
    id obj2;
    id arp;

    obj1 = [[Foo alloc] init];
    obj2 = [[Foo alloc] init];

    [obj1 autorelease]; // obj1は、デフォルトのNSAutoreleasePoolに登録される。
    arp = [[NSAutoreleasePool alloc] init];
    [obj2 autorelease]; // obj2は、arpに登録される。
    [arp release]; //arp解放。この時点でobj2は、解放される。

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

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

- (void)methodA
{
    id temp;
    id arp;
    int i;

    for (i = 0; i < 10; i++) {
        arp = [[NSAutoreleasePool alloc] init];
        temp = [[[Foo alloc] init] autorelease];
                            // tempで現在参照されているオブジェクトは、
                            // arpに登録される。
        [arp release];      // arp解放。
                            // この時点でtempで参照されるオブジェクトは解放される。
    }
    // この時点で10個のオブジェクトが解放されています。
}

入れ子になったNSAutoreleasePoolの利用

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

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

- (void)methodA
{
    id obj1;
    id arp1;


    arp1 = [[NSAutoreleasePool alloc] init];
    obj1 = [[[Foo alloc] init] autorelease]; // obj1は、arp1に登録される。
    [self methodB];
    [arp1 release];     //arp解放。この時点でobj1、obj2は、解放される。
}

- (void)methodB
{
    id obj2;
    id obj3;
    id arp2;

    obj2 = [[Foo alloc] init];
    obj3 = [[Foo alloc] init];

    [obj2 autorelease]; // obj2は、呼出し元のarp1に登録される。
    arp2 = [[NSAutoreleasePool alloc] init];
    [obj3 autorelease]; // obj3は、arp2に登録される。
    [arp2 release];     //arp2解放。この時点でobj3は、解放される。

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

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

- (void)methodA
{
    id obj;
    id arp1;

    arp1 = [[NSAutoreleasePool alloc] init];
    obj = [self methodB:1];
    [arp1 release];    // arp1解放。この時点でobjすなわちretは、解放される。
}

- (id)methodB:(int)i
{
    id ret;
    id arp2;

    arp2 = [[NSAutoreleasePool alloc] init];
    ret = [[Foo alloc] init]; // retの保持数は1
    [ret autorelease];
    if (i == 1) {
        [ret retain]; // retの保持数は2になる。
                      // autoreleaseではなく、retainを呼ぶことで、
                      // arp2の寿命を超えて、arp1(もしくは、
                      // もっと外)の文脈まで、retで参照されるオブジェクトの
                      // 寿命を延ばすことができる。
    } else {
        ret = nil;
    }
    [arp2 release];   // 判定で真だったら、この時点でretの保持数は1になる。
                      // 判定で偽だったら、この時点でretは解放される。

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

autoreleaseか、retain/releaseか

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

retain/release/autoreleaseの適用方針

以下に垣内さん、白山さん、高橋さんから教えていただいた適用方針をまとめたものを示します。
    aFoo = [[[Foo alloc] init] autorelease];
    aFoo = [Foo foo];
    globalFoo = [aFoo retain];
//この例ではinstanceFooは、このクラスのインスタンス変数と仮定する。
- (void )setFoo:newFoo
{
    [instanceFoo release];
    instanceFoo = [newFoo retain];
    return;
}

- (void )dealloc
{
    [instanceFoo release];
    [super dealloc];
    return;
}
// 生成用の引数がない場合
+ foo 
{
    return [[[Foo alloc] init] autorelease];
}

// 生成用の引数が必要な場合
+ fooWithBar: bar
{
    return [[[Foo alloc] initWithBar:bar] autorelease];
}
    for (i = 0; i < 10; i++) {
        arp = [[NSAutoreleasePool alloc] init];
        temp = [[[Foo alloc] init] autorelease];
        [arp release];
    }
◆この稿の執筆にあたり、以下の方々に感謝いたします。ありがとうございます。
・遠藤茂行さん <ender@yo.rim.or.jp>
・伊藤勝良さん <kito@hana.or.jp>
・垣内正年さん <masato-k@is.aist-nara.ac.jp>
・白山貴之さん <psi@stellar.co.jp>
・高橋 究さん <kiwamu@zephyr.sato-hosp.or.jp>
・竹尾哲也さん <ttakeo@amtec.co.jp>
・水野隆一さん <mizuno@cyberlab.co.jp>
・井上@Appleさん <aki@apple.com>
BACK INDEX NEXT