首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 移动开发 > 移动开发 >

Cocoaのメモリ治理(3)

2012-09-03 
Cocoaのメモリ管理(3)保持と解除という方法は、理屈は分かるし簡単そうに見えます。しかし、実際にやってみると意外と難しいことがわかります。そこでCocoaで

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は、プログラムの最後に(つまり、メインスレッドの末端で)解放されるので、基本的にこれに登録されたオブジェクトに割り当てられたメモリはプログラムの最後まで解放されない。
    * イベントループ内で毎回生成されるNSAutoreleasePoolは、ループが一回転するごとに解放されるので、基本的にこれに登録されたオブジェクトに割り当てられたメモリはイベント処理が終わると解放される。
    * autoreleaseされたオブジェクトが、基本的にそのオブジェクトが登録されたNSAutoreleasePoolがreleaseされるまで解放されないということは、逆に言えば、NSAutoreleasePoolが生きている間は、オブジェクトの生存が保障されるということを意味している。
    * ただし、releaseメソッドを送ることで、このようなオブジェクトでもプログラムの途中で解放することは可能である。(これは、推奨されない方法である。基本的に、同じオブジェクトに対してautoreleaseとreleaseを併用することは混乱の元になるので、止めるべきである。 deallocについてはいわずもがな。オブジェクトのプログラマブルな解放のためには、後述する方法を用いるべきである。)
    * 例外発生時には、NSAutoreleasePool(と、登録されたオブジェクト)は解放されるので、メモリリークは起こらない。(どっちにしてもプログラムが終了するので、メモリは解放される。)
    * デフォルトのNSAutoreleasePoolだけで動作するようなプログラム場合、参照が切れたオブジェクトは、基本的にメモリリークと同じ状況になる。(あえて言うなら、知った上でのメモリリーク。通常のプログラムではあまり気にする必要がない。)
    * 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オブジェクトにはretainを送ってはならない。
    * NSAutoreleasePoolオブジェクトにはautoreleaseを送ってはならない。
    * 無用なバグと混乱を避けるために、NSAutoreleasePoolオブジェクトの生成と解放は、同じ文脈上で行うべきである。(たとえば、前述の例のように、ループ内の処理の前後など)
    * retain回数 + 1 = autorelease回数がプログラム全体で成り立つことを検証する。

さて、スレッドの流れを把握しているなら、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の適用方針
以下に垣内さん、白山さん、高橋さんから教えていただいた適用方針をまとめたものを示します。

    * 基本としてオブジェクトのオーナシップを意識する。すなわち、以下の鉄則を厳守する。

      【鉄則1】自分で生成したオブジェクトは、自分で解放する
      【鉄則2】他人が生成したオブジェクトは、気に留めない
      【鉄則3】他人が生成したオブジェクトが必要なら、必ず保持(retain)して、必要にならなくなった時点で、必ず解除(release or autorelease)する

    * 可能な限り alloc-init系の生成メソッドは使わない。【鉄則2からの派生】

    * alloc、init…を使う場合は必ず autoreleaseを入れる。【鉄則1】

    aFoo = [[[Foo alloc] init] autorelease];

    * alloc-init系以外の生成用クラスメソッドの場合は何もしない。それらのクラスメソッドは内部でalloc-init系の生成メソッドを呼んでいるため、鉄則1が適用されていると考えるべきである。【鉄則2】

    aFoo = [Foo foo];

    * インスタンス変数?グローバル変数?スタティック変数に代入して、参照を残す場合は、retainをかける。【鉄則3】

    globalFoo = [aFoo retain];

    * インスタンス変数?グローバル変数?スタティック変数の値を消す場合には、かならずreleaseかautoreleaseを入れる。【鉄則3】

//この例ではinstanceFooは、このクラスのインスタンス変数と仮定する。
- (void )setFoo:newFoo
{
    [instanceFoo release];
    instanceFoo = [newFoo retain];
    return;
}

- (void )dealloc
{
    [instanceFoo release];
    [super dealloc];
    return;
}

    * 自分でクラスを作るときは、NSStringの+string... NSArrayの+array... のようなそのクラス専用の生成用クラスメソッドを作成して利用させる。【鉄則2の応用】

// 生成用の引数がない場合
+ foo 
{
    return [[[Foo alloc] init] autorelease];
}

// 生成用の引数が必要な場合
+ fooWithBar: bar
{
    return [[[Foo alloc] initWithBar:bar] autorelease];
}

    * 必ずループ内部でNSAutoreleasePoolを適用する。

    for (i = 0; i < 10; i++) {
        arp = [[NSAutoreleasePool alloc] init];
        temp = [[[Foo alloc] init] autorelease];
        [arp release];
    }

热点排行