2011/03/24

Objective-Cのココが苦手(メモリ管理編)

【ミッション: Objective-Cのメモリ管理を覚えよ】

前回のあらすじ: Objective-C の本を買った

せっかくだし、読んでてちょっと苦手だなぁと思ったところを潰していこうと思う。さっそくみんな大好きメモリ管理いってみよう。

一般的なオブジェクト指向言語では、おおむね、メモリのどこかにインスタンスの実体を格納しておくための領域を動的に確保し、そこにポインタ経由でアクセスする事になっている(おなじみ new 演算子はその典型である)。

Java でこの仕組みをある程度意識せずに済んでいるのは、ポインタを意識させないように構文が整備されている事に加え、使用済みのメモリ領域を自動的に解放するガベージコレクションをサポートしているためである。

しかし、iOS は現時点でガベージコレクションをサポートしておらず、iPhone や iPad 向けアプリを作る場合、メモリ管理は避けては通れない。

メモリ管理を疎かにする悪い癖がつかないうちに、この辺りをもうちょっと理解しておきたいと考えた。



【メモリ管理の基本は allocrelease

すべての基本は、alloc で確保したメモリ領域を release で解放。C 言語の mallocfree、C++ の newdelete に相当する。

基本的にはこんな感じで使うらしい。

#import <Foundation/Foundation.h>

main() {
    NSAutoreleasePool *pool 
        = [[NSAutoreleasePool alloc] init];
    
    NSDate *d1;
    
    // メモリ確保
    d1 = [[NSDate alloc] init];
    NSLog(@"メモリ確保だん。\n");
    
    [d1 release]; // 使い終わったら解放
    NSLog(@"メモリ解放だん。\n");
    
    [pool release];
}

うん。簡単だね。

これ、何が厄介かというと、一度解放した領域に知らず知らずのうちにアクセスしてしまう危険性を孕んでいる。つまり、以下のような事をやってどうしようもないバグを生みがちなのだ。

#import <Foundation/Foundation.h>

main() {
    NSAutoreleasePool *pool 
        = [[NSAutoreleasePool alloc] init];
    
    NSDate *d1, *d2;
    
    // メモリ確保
    d1 = [[NSDate alloc] init];
    
    // ポインタコピー
    // d1 と d2 は同じ領域を参照している
    d2 = d1;
    
    // メモリ解放
    [d1 release];
    
    // d2の参照先は既に解放されているのに…
    NSLog(@"%@\n", d2);
    
    [pool release];
}

上記のプログラムの d1d2 は同じ領域を参照しているのだけど、どれかひとつを解放した時点で参照先にはもう二度とアクセスできなくなる。17 行目で d1 の参照先(= d2 の参照先)を release し、その後 20 行目で d2 の参照先を表示しようとするとこんな事になる。


はい死んだ。いまプログラム死んだよ。



【メモリ管理をラクにする参照カウンタ】

上記の例はわざとらしいが、ぼくが『C 言語は苦手だ』という理由は、メモリ解放のタイミングの難しさにある。

『どこからも参照されなくなった事が確実に保証されるまで、その領域は解放してはならない』

プログラムが複雑化するにつれて、メモリ領域の共有状況をプログラマが把握するのは至難の業となる。

しかし、Objective-C には、確保したメモリ領域が何箇所から参照されているかを保持する『参照カウンタ』が用意されているという。

この仕組みは非常に単純で、
  1. 同じ領域を参照するポインタが増殖するごとに、参照カウンタをインクリメント。
  2. 逆に、ポインタ変数が死ぬときに、参照カウンタをデクリメント。
  3. 最後の参照元ポインタが死ぬとき、そのポインタが参照している領域を解放。
というものである。

参照カウンタを採用している言語の中には、カウンタの増減を自動で行うものもある(らしい)が、Objective-C の場合は retainrelease を用いて参照カウンタを手動で増減させる必要があるのだ。

以上を踏まえて、参照カウンタを試用してみた。

#import <Foundation/Foundation.h>

main() {
    NSAutoreleasePool *pool 
        = [[NSAutoreleasePool alloc] init];
    
    NSDate *d1, *d2;
    
    // メモリ確保
    d1 = [[NSDate alloc] init];
    NSLog(@"メモリ確保だん。%dヵ所から参照されています\n",
        [d1 retainCount]);
    
    d2 = d1;      // ポインタをコピー
    [d2 retain];  // 参照カウンタ++;
    NSLog(@"ポインタコピー。%dヵ所から参照されています\n",
        [d2 retainCount]);
    
    [d1 release]; // 参照カウンタ--;
    NSLog(@"メモリ解放だん。%dヵ所から参照されています\n",
        [d2 retainCount]);
        
    [d2 release]; // もひとつ解放
    NSLog(@"メモリ解放だん。\n");
    
    [pool release];
}
※註: 19 行目は、厳密には解放しているわけではないので、20 行目のメッセージは誤り。ごめん。

実行すると、確保したメモリ領域が何箇所から参照されているかを確認できる。


この事から、release はただちにメモリを解放するメソッドというわけではなく、参照カウンタを減じる働きをするらしいという事が判る。

そして、参照カウンタが 0 になった瞬間に、参照しているオブジェクトのデストラクタ(dealloc メソッド)が呼ばれて領域が解放されるというのがより正確な理解のようだ。



NSAutoreleasePool でもっと簡単に】

上記の例で、だいたい参照カウンタの仕組みは解ったが、それでもメモリの解放忘れが解決するとは限らない。

そんなときは、メモリの解放を自動的に行ってくれる NSAutoreleasePool (自動解放プール)を使うと幸せになるらしい。

とりあえず、alloc でインスタンスを生成するたびに、autorelease で自動解放プールに登録していく。

あとは、最後にプールを release するだけで、登録されたすべてのインスタンス(の実体が格納されているメモリ領域)を安全に解放してくれるスグレモノだという。

#import <Foundation/Foundation.h>

main() {
    // 自動解放プールの作成
    NSAutoreleasePool *pool 
        = [[NSAutoreleasePool alloc] init];
    
    NSDate *d1;
    
    // メモリ確保
    d1 = [[NSDate alloc] init];
    
    // 自動解放プールに登録
    d1 = [d1 autorelease];
    
    // なんと、d1 の release メソッドが要らない!
    
    [pool release];
}

うん、これで、インスタンスごとの release が必要なくなった……のかな?正直よく解らん。

プールを入れ子にしてちょっと実験してみよう。

#import <Foundation/Foundation.h>

main() {
    // 自動解放プール(1)の作成
    NSAutoreleasePool *outerPool 
        = [[NSAutoreleasePool alloc] init];
    
    // メモリ確保 & プール(1)に登録
    NSDate *d1 = [[[NSDate alloc] init] autorelease];
    
    
    // 自動解放プール(2)の作成
    NSAutoreleasePool *innerPool 
        = [[NSAutoreleasePool alloc] init];
    
    // メモリ確保 & プール(2)に登録
    NSDate *d2 = [[[NSDate alloc] init] autorelease];
    
    // d1 と d2 の表示
    NSLog(@"d1: %@\n",d1);
    NSLog(@"d2: %@\n",d2);
    
    // 自動解放プール(2)の解放
    // ここで d2 の参照先が解放される
    [innerPool release];
    
    // ふたたび d1 と d2 の表示
    NSLog(@"d1: %@\n",d1);
    NSLog(@"d2: %@\n",d2);  // ←ここで死ぬ
    
    // 自動解放プール(1)の解放
    // ここで d1 の参照先が解放される
    [outerPool release];
}

autorelease メソッドを実行すると、直近で作成されたプールに登録される。

9 行目の d1outerPool に登録され、17 行目の d2innerPool に登録される。

その証拠に、innerPoolrelease した後は、d2 にアクセスできなくなる。29 行目で強引にアクセスしようとしているが、そんな事をすれば segmentation fault で即死だ。


この仕組み、何かに似ていると思ったら、C# の using ステートメントのイメージに似ているのだ。

まだ誤解があるかも知れないが、『動的メモリ領域のスコープを決める』という表現がしっくりくる。



【結局どれがいいのかな】

ここまで色々見てきた中で、メモリ管理に関しては自動解放プールを活用するが一番ラクだなと感じた。

《動的に確保したメモリ領域がどこで解放されるか》が、コードを読むだけで判り、メモリを御しやすいためである。

参照カウンタを用いた場合だと、ソースコード上の release が単にカウンタを減らしているだけなのか、それともメモリ領域を解放しているのかが傍目には判りづらい。正直、ぼくには少し難しい。

さらに、最初に挙げた《参照カウンタすら用いない方法》に関しては、メリットが全く思い浮かばない。強いて言えば、せいぜいカウンタ操作に要するわずかなオーバヘッドを節約できる程度だろうが、メモリ周りの深刻かつ致命的なバグを生むリスクを侵してまでやるものではないと思っている。

……というわけで、自動解放プールという Objective-C 独自の仕組みに関して、少しずつ慣れていく必要がありそうだと感じた。

『ラクをするための努力』は、ムダにはならないと思っている。