今さら聞けないObjective-Cのメモリ管理 弱い参照と強い参照って何?

442件のシェア(そこそこ話題の記事)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

iOS5からサポートされているARC(Automatic Reference Counting)は今まで使う機会がなかったので名前だけ知ってるみたいな状態でした。
今さらですがいろいろ調べたので調査結果をまとめたいと思います。調査した環境は以下になります。

  • Mac OS X 10.8 Mountain lion
  • Xcode 4.6.3

まずは参照カウンタ方式の復習

ARCは基本的には以前と同じ参照カウンタ方式を自動化したものなので、ARCをやる前にまずは手動でのメモリ管理を復習します。
まずは新しいプロジェクトを作成します。テンプレートはCommand Line Toolを使います。
今回は手動でメモリ管理するのでARCはチェックを外しておいて下さい。
プロジェクトが作成できたらCarクラスとEngineクラスを作成して下さい。テンプレートはObjective-C classです。

サンプルはCarクラスとEngineクラスのインスタンスを生成して、必要なくなったらリリースしているだけの簡単なものです。
コメント内でプロパティを使った場合のソースも一応書いています。どちらを使っても同じ結果になります。

main.m
#import <Foundation/Foundation.h>
#import "Car.h"
#import "Engine.h"

int main(int argc, const char * argv[])
{
    Car *car = [Car new];
    Engine *engine = [Engine new];
    /* setter を使う場合 */
    [car setEngine: engine];
    
    /* プロパティを使った場合
    car.engine = engine;
    */
    
    /* 何か処理をする */
    
    [engine release];
    [car release];
    return 0;
}


次にCar.hを実装します。

Car.h
#import <Foundation/Foundation.h>
#import "Engine.h"

@interface Car : NSObject
{
    Engine *engine;
}

/* setter を使う場合 */
- (void) setEngine: (Engine *) newEngine;

/* propertyを使う場合
@property(retain) Engine *engine;
*/
@end

Carクラスは内部でEngineクラスのインスタンスを包含しています。

続いてCar.mを実装しましょう。

Car.m
#import "Car.h"

@implementation Car

/* setter を使う場合 */
- (void) setEngine: (Engine *) newEngine
{
    [newEngine retain];
    [engine release];
    engine = newEngine;
}

/* propertyを使う場合
@synthesize engine;
*/

- (void) dealloc
{
    NSLog(@"dealloc Car");
    [engine release];
    [super dealloc];
}
@end

setter を使ってEngineクラスのインスタンスをセットした際に retain メソッドで参照回数を増やしています。
deallocメソッドをオーバーライドして包含している engine も一緒に開放しています。
その他にメモリを解放できているかどうかを確認したいのでログを追加して呼び出された事が分かるようにしています。

Engine.hは特に実装しなくていいです。Engine.m を以下のように実装します。

Engine.m
#import "Engine.h"

@implementation Engine

- (void) dealloc
{
    NSLog(@"dealloc Engine");
    [super dealloc];
}
@end

Car.m と同じでdealloc時にログを出力するようにしただけです。以上で実装は完了です。実行してみて下さい。
実行すると以下のようなログが出力されdeallocメソッドが呼び出されていることが分かると思います。

2013-09-09 21:55:52.304 BlogSample[593:303] dealloc Car
2013-09-09 21:55:52.307 BlogSample[593:303] dealloc Engine

オブジェクトを生成した時点で参照カウンタは1になっています。
その後、他のオブジェクトからも参照されretainした場合は参照カウンタを+1、不要になりreleaseされた場合は参照カウンタを-1していきます。
参照カウンタが0になるとどこからも参照されていない不要なデータと判断されdeallocメソッドが呼ばれメモリーが開放されます。

参照カウンタ方式の問題点

参照カウンタ方式は循環参照の場合にメモリーが解放されない、という問題点があります。
循環参照しているとはどのような状態なのかサンプルを修正して確認してみましょう。
まずは Engine.h にはCarクラスのインスタンス変数とそれに対するsetter を定義します。

Engine.h
#import <Foundation/Foundation.h>

@class Car;

@interface Engine : NSObject
{
    Car *car;
}

/* setter を使う場合*/
- (void) setCar: (Car *) newCar;

/* propertyを使う場合
@property(retain) Car *car;
*/
@end

Car.h を参照しているので #import "Car.h" と書きたいところですが、書くとコンパイルエラーになります。
これはCar.h に Engine.h がすでにインポートされているからです。
#import "Car.h" の代わりに @class Car; と書くとCarクラスを利用できるようになります。

続いてsetterを実装します。

Engine.m
/* setter を使う場合 */
- (void) setCar: (Car *) newCar
{
    [newCar retain];
    [car release];
    car = newCar;
}

/* propertyを使う場合
@synthesize car;
*/

- (void) dealloc
{
    NSLog(@"dealloc Engine");
    [car release];
    [super dealloc];
}

最後に main.m を以下のように修正して下さい。

    Car *car = [Car new];
    Engine *engine = [Engine new];
    /* setter を使う場合 */
    [car setEngine: engine];
    [engine setCar: car];  // 追加

修正が終わったので実行してみて下さい。実行してもログに何も表示されないと思います。
car と engine がお互いの参照を持っているため参照カウントが0にならずdeallocメソッドが呼び出されないためです。
この問題は参照カウント方式の問題なのでARCを使っても起こります。この問題の対策は以下になります。

setterを使っている場合
Car.m の setEngine または Engine.m の setCarの中でretainしている行を削除する。
プロパティを使っている場合
Car.h または Engine.h の@property(retain) を@property(assign) に書き換える。

何をしているかというと循環参照している2つのオブジェクトどちらかの参照カウンタを参照が増えても増やさないようにしています。
これによりmain.m でリリースしたタイミングで car と engine の参照カウンタが0になりdeallocメソッドが呼び出されます。

ARCを使った場合はどうなるのか

手動でのメモリ管理の復習は終わったので次はARCを使ってやってみたいと思います。ソースを循環参照の状態に戻してARCをオンにして下さい。
ARCはプロジェクトの設定から変更することができます。

objective-c-memory3

ARCをオンにした場合、retain、releaseの処理はコンパイル時に自動で入れてもらえるのでソースコード内にあるとコンパイルエラーになります。全部消しましょう。
サンプルではdeallocをオーバーライドしていますが、この中にある[super dealloc]もコンパイルエラーになります。
このサンプルではオーバーライドしたdeallocメソッドが呼ばれていることだけ確認できればいいのでこの行も消してしまいましょう。
retain と release を全部消すと循環参照の状態になるので実行してもログに何も表示されません。そこでEngine.hを以下のように修正します。

Engine.h
@interface Engine : NSObject
{
    __weak Car *car;
}

/* setter を使う場合*/
- (void) setCar: (Car *) owner;

/* propertyを使う場合
@property(weak) Car *car;
*/
@end

Engine の変数定義に __weak修飾子 が付いています。これが弱い参照にする、という修飾子になります。
何も入れなかった場合は__strong修飾子 が付いているのと同様になります。これは強い参照の修飾子です。
循環参照している2つのオブジェクトのどちらかを弱い参照にすることによってメモリーリークを防ぐことができます。
実行してみればログにdeallocメソッドが呼び出されていることが分かると思います。
propertyを使う場合はプロパティ属性をweakにします。尚、propertyを使う場合でも変数定義の __weak修飾子 は必要ですので消さないでください。

Car.h もpropertyを使う場合はプロパティ属性を strong に修正しておいて下さい。

Car.h
/* propertyを使う場合
@property(strong) Engine *engine;
*/

__weak修飾子はiOS4、Mac OS 10.6では使えません。それらのOSに対応したい場合は__unsafe_unretained修飾子を使います。
__weak修飾子と__unsafe_unretained修飾子はどこが違うのか簡単に説明すると、__weak修飾子が付いたインスタンスは破棄されるとゼロ化(Zeroing)されて自動的にnilになります。
__unsafe_unretainedはnilにならずエラーになります。

その他メモ

プロパティ属性の assign と weak
weak は以前からある assign とほぼ同じという感じですが、元のインスタンスが解放されるとnilが代入されるのでぶら下がりポインタになることがない、という特徴があります。ぶら下がりポインタとはオブジェクトの解放後、参照先のメモリアドレスが他のオブジェクトのデータに再割り当てされていて危険な参照になることです。
iPhoneアプリ開発でのweakの使いどころ①
iPhoneアプリ開発では画面のコントロールとViewControllerに定義した変数を紐づけたりしますが、Viewがコントロールの参照をstrong で持っているため IBOutletオブジェクトは weak にするそうです。
iPhoneアプリ開発でのweakの使いどころ②
Objective-CではDelegateパターンを使うことがよくありますが、処理を委譲するために渡したインスタンスを strong で保持すると循環参照になるため weakにします。

まとめ

今回はARCの強い参照、弱い参照に絞って調査してみました。
簡単に言うと強い参照は参照カウントをインクリメントする参照、弱い参照は参照カウントをインクリメントしない参照、という感じでしょうか?
今度は @autoreleasepool を調査してみたいと思います。