Cocoaでいこう! Macらしく 第10回
Yoshiki(DreamField)
この記事は、MOSAが発行するデベロッパ向けのデジタルマガジンMOSADeN 第69号(2003年6月10日発行)に掲載された記事です。2〜3ヶ月遅れで、ここに掲載して行きます。

前回で、TinyViewは第一段階の完成をみました。ですが、今までCやC++でプログラムを組まれて来た方は、メモリの管理について疑問を感じたのではないでしょうか。ファイルから読み込んだデータが(NSData *)dataとして渡されて来るけど、これを解放しているのは誰だろう?imageを勝手に解放してしまっているけど、imageViewはこれで困らないのだろうか?

Objective-Cでは、この様な制御をリファレンスカウンタ方式で行っています。

※既にリファレンスカウンタについてご存じの方は、「リファレンスカウンタの仕組み」は読み飛ばしてもかまいません。ですが「リファレンスカウンタの目的」は、是非お読み下さい。

リファレンスカウンタの仕組み

Cocoaでは、全てのオブジェクトはリファレンスカウンタを持っています。オブジェクトを生成した時、このカウンタは1にセットされます。そして、このカウンタの値が0になった時、そのオブジェクトは解放されます。

この文章だけではイメージを掴みにくいでしょうから、図で説明しましょう。例えば、オブジェクトaがオブジェクトbを生成したとします。この時点でオブジェクトbのカウンタは1にセットされます(fig.01)。

オブジェクトの生成
[fig.01] オブジェクトの生成

次にオブジェクトaが、このオブジェクトbを解放しようとしたとします。オブジェクトを解放するには、そのオブジェクトに対してreleaseというメッセージを送ります。releaseを受け取ったオブジェクトは、カウンタの値から1を引きます。この結果、カウンタの値は0になります。そしてカウンタの値が0になると、オブジェクトbは解放されます(fig.02)。

オブジェクトの解放
[fig.02] オブジェクトの解放

オブジェクトがこの二つしかなければ、こんな仕組みは必要ありません。カウンタが無くても、releaseを送った時点で解放してしまえば、それで済む話です。では、どんな時に必要になるかと言うと、例えば次のような場合です。オブジェクトaがオブジェクトbを生成したとします。そして、このオブジェクトbをオブジェクトcにも渡したとします。そして、その後オブジェクトaではオブジェクトbが必要なくなり、解放したとしましょう。この時、まだオブジェクトcがオブジェクトbを使っていたとしたら。まだ使っているのにオブジェクトbは勝手に解放され、消えてしまったことになります。これでは、オブジェクトcは誤動作してしまいます(fig.03)。

オブジェクトを勝手に解放されてしまった例
[fig.03] オブジェクトを勝手に解放されてしまった例

そこで、オブジェクトcは勝手に解放されないように、オブジェクトbを渡された時点で、オブジェクトbにretainメッセージを送っておきます。retainメッセージを送られると、オブジェクトbはカウンタに1を加えます。この時点でカウンターの値は2になっていますから、オブジェクトaがオブジェクトbにreleaseを送って、カウンタから1を引いても結果は0になりません。従いまして、この時点ではオブジェクトbは解放されないのです。オブジェクトbが解放されるのは、さらにオブジェクトcがオブジェクトbを必要としなくなり、releaseを送り、カウンタの値が0になった時ということになります(fig.04)。この様にして、オブジェクトを勝手に解放されるのを防ぐことができるというわけです。

オブジェクトにretainを送って勝手に解放されないようにした例
[fig.04] オブジェクトにretainを送って勝手に解放されないようにした例

さて、以上の説明だけ読むと、何でこんな複雑なことをやらなければいけないんだと思うかもしれません。でも、実はリファレンスカウンタは、プログラムを単純にするための仕組みなのです。これについては「リファレンスカウンタの目的」で説明します。

リファレンスカウンタの目的

リファレンスカウンタの仕組みについては理解できたと思います。では、リファレンスカウンタの目的とは何でしょうか。それは、明確にオブジェクトを所有することです。自分が必要であれば所有する、そして他人がどうするかは知ったことでは無い。自分のことしか考えなくて良いのですから、非常に単純です。

これを実現するために、Cocoaではリファレンスカウンタに関して、明確な規約が決められています。

  1. 最初から所有している(リファレンスカウンタが1になっている。)オブジェクトは、allocで生成した時と、copyで複製した時だけです。
  2. 1.以外で得たオブジェクトは、継続的に必要であるなら、retainメッセージを送って明確に所有しなければなりません。
  3. 所有したオブジェクトは、必要が無くなったら、自分でreleaseメッセージを送って解放しなければなりません。

以上です。では、前回までに作成したプログラムに照らし合わせて説明しましょう。

- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
{
    image = [ [ NSImage alloc] initWithData:data];    
    return ( image != nil)?( YES):( NO);
}

まず、loadDataRepresentation:ofType:で受け取ったdataですが、これは自分で生成したものではありませんから解放する必要はありません。生成した人が勝手に解放するはずです。裏を返すと、このメソッドから返った後に解放されてしまう可能性がありますので、継続的に使用するならretainを送って所有しておく必要があります。今回は、

image = [ [ NSImage alloc] initWithData:data]; 

という具合に、imageの初期化に使っています。もし、imageがdataを継続的に必要とするのであれば、image自身が勝手にretainするなりcopyするなりしているはずです。ですから、こちらで気にする必要はありませんし、imageが実際にどうしているのかを知る必要もありません。自分自身はdataをこれ以上保持しておく必要は無いので、そのままほっておいて良いことになります。そうすれば、生成した人が勝手に解放してくれるはずです。もちろん、aTypeに関しても同様のことが言えます。では、imageに関してはどうでしょうか?imageは自分でallocして生成したものです。自分自身が所有していますから、必要なくなった時点で自分で解放する必要があります。次にimageを使っているのはwindowControllerDidLoadNib:ですから、こちらを見てみましょう。

- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
    [super windowControllerDidLoadNib:aController];
    [ imageView setImage:image];
    [ image release];
}

ここでは、imageViewにimageを渡し、その後imageにreleaseを送って解放しています。imageViewに渡した直後にimageを解放してしまっても良いのでしょうか。ここまで説明を読んで来た方にとっては、聞くまでも無いことですね。そうです。もしimageViewがimageを必要としているのであれば、勝手にretainするなりcopyするなりしているはずです。こちらでそれを知る必要はありません。自分自身は、imageViewにimageを渡してしまったら、もうimageは必要ありませんから、直後にreleaseしてしまってかまわないのです。と言うより、この時点でreleaseしておかないと、メモリリークになります。

どうでしょうか。リファレンスカウンタのおかげでプログラムが実に単純になるということが分かっていただけたのではないかと思います。でも、実はこの仕組みだけでは足りないのです。何が足りないのか、これに関しては次回に説明したいと思います。

前頁目次次頁