Objective-C ARCによるメモリ管理

Saturday, December 31st, 2011

Objective-Cを勉強していて一番驚いたのがメモリ管理の仕組みです。ちょっと前までは、手動メモリ管理(MRC: Manual Reference Counting)、その後、GCがMac OS Xのみに入ったけど、最近になって新たにARC(Automatic Reference Counting)が導入されたとのこと。これからはARCが主流となるとのことで、少し調べてみました。

Appleの出している資料は、おそらくこのTransitioning to ARC Release Notesのみだと思われます。でも、どのような仕組みでARCが動作しているかの情報もなく、少しわかりにくい。

おそらく一番詳しいのはClangのAutomatic Reference Countingだと思います。これを読むと、内部でどのような動作をしているのかはわかります。ただ、じゃあ実際のコーディングはどうしたらいいのかという点は、わかりにくいです。

その点では、この記事が一番わかりやすかったです。なので、以下の内容は、これらの内容を参考にしながら書いたものです。

ARCによるメモリ管理

基本的な考え方

ARCになっても、管理の基本的な仕組みは、手動メモリ管理のときと同じです。つまり、各オブジェクトが参照カウンタ(retain counter)を持っていて、そのカウンタが0になるとオブジェクトが破棄されます。また、戻り値や一時的なオブジェクトのためにauto release poolが存在します。
ARCでは、新たに「強い参照」と「弱い参照」の概念が導入されます。強い参照は今までの参照と同じく、強い参照がある間はそのオブジェクトは破棄されません。対して、弱い参照は、弱い参照があっても、強い参照がなくなればそのオブジェクトは破棄されます。強い参照と弱い参照のどちらの参照なのかをコンパイラに教えるために、新たに__strong、__weakという修飾子が用意されます。
強い参照と弱い参照の二種類を持つ理由は「循環参照」を避けるためです。循環参照についてはこちらを参照してください。

ARCで用いる修飾子

変数のためのARC修飾子

強い参照

以下のように__strongを指定することで、その参照は強い参照と認識されるようになります。

__strong MyClass *obj = [[MyClass alloc]init];

デフォルトは強い参照となるので、通常は指定する必要はありません。このため、alloc/initで生成されたオブジェクトは、現在のスコープのライフタイムの間、維持されることとなります。「現在のスコープ」の意味するところは、多くの場合、変数が宣言された中括弧内を指します。

弱い参照

以下のように__weakを指定することで、その参照は弱い参照と認識されるようになります。

__weak MyClass *wobj = obj;

__weakが指定されたオブジェクトは、いつでも削除される可能性があります。弱い参照を用いる場合は、普通は、他の場所で強い参照が持たれているはずです(でないと、そのオブジェクトは破棄されてしまうので)。このオブジェクトが破棄された場合、この弱い参照にはnilが設定されます。これにより、破棄された後でも、nilに対するメソッド呼び出しは無視されるため、アプリケーションのクラッシュにはつながりません。

残念ながら、__weakをサポートしているのは、OS X 10.7とiOS 5からです。このため、OS X 10.6やiOS 4では使えません。代わりに__unsafe_unretainedを使うことができます。動作は、オブジェクトが破棄された場合にnilにならない以外は同じです。ただ、破棄された際にnilとならないので、破棄後のメソッド呼び出しはアプリケーションのクラッシュとなります。

プロパティのためのARC修飾子

強い参照

ARCがない頃は、以下のように記述していました。

@property(retain) NSObject *obj;

ARCでは、これを以下のように記述します。

@property(strong) NSObject *obj;

弱い参照

ARCがない頃は、以下のように記述していました。

@property(assign) NSObject *obj;

ARCでは、これを以下のように記述します。

@property(weak) NSObject *obj;

*) 一番わかりやすいのは、どこか一カ所で強い参照でそのオブジェクトを所有し、他の場所では、その弱い参照を持ち、そのオブジェクトにアクセスするという方法です。

ARCを使う場合のルール

1.alloc/init

オブジェクトを生成した後、retain/release/autorelease/retainCountを呼び出してはなりません。さらに、@selector(retain)や@selector(release)を用いたセレクタによる呼び出しもしてはなりません。

2.dealloc

deallocメソッドは自動的に生成されます。それと、直接deallocを呼び出してはなりません。ただ、インスタンス変数以外のリソースをリリースするためにカスタムdeallocを作成することは問題ありません。しかし、この際、[super dealloc]は呼び出してはなりません。これはコンパイラによって行われます。

3.プロパティの宣言

ARCがない頃は、@property宣言にassign/retain/copyパラメータを指定することで、メモリ管理の仕方をコンパイラに指示していました。しかし、ARCではこれらのパラメータは不要です。代わりに、weak/strongパラメータを指定し、コンパイラに指示します。

4.NSAutoReleasePoolの代わりに@autoreleasepoolを使う

5.C構造体内のオブジェクトポインタ

これは使ってはなりません。構造体の代わりにクラスを用いるべきです。

6.idとvoid*の間のキャスト

これはCore FoundationのCライブラリとFoundation KitのObjective-Cライブラリメソッドの間で発生します。ARCを用いる場合、CFオブジェクトがメモリ管理下から出たり、入ったりした場合に、ヒントをコンパイラに与えなければなりません。このヒントとしては、__bridgeや__bridge_retain、__bridge_transferなどです。さらに、Core FoundationオブジェクトのメモリマネージメントのためにはCFRetainとCFReleaseが必要となります。

7.その他

ゾーン(NSZone)によるメモリ管理はなくなったので、NSAllocateObjectやNSDeallocateObjectは使えない。

*) Objective-Cに閉じた世界で、あまり凝ったことをしていなければ1〜4を覚えておけばよいと思います。

関数の引数や戻り値

関数の引数

関数やインスタンスメソッドの引数として、オブジェクトを渡す場合は、普通は、呼び出し元側で強い参照を持っているので大きな問題となりません。
もし、特別な理由で、参照カウンタを+1してから関数やメソッドにオブジェクトを渡したい場合は、以下のように記述することができます。

void foo(__attribute((ns_consumed)) id x);
- (void) foo: (id) __attribute((ns_consumed)) x;

ただし、普通は、特に何もする必要はありません。(つまり、呼び出し元で、引数として渡すオブジェクトの強い参照を持ち、呼び出せばよい)

関数の戻り値

MRCの時代にautoreleaseという仕組みを用意したように、関数の戻り値をどのように返すかには工夫が必要となります。例えば、以下のようにインスタンス変数を返す場合は、参照カウンタがすでに+1されているので問題はありません。

@interface MyClass : NSObject
{
    NSString *name;
}
-(NSString*)getName;
@end

@implementation MyClass
-(NSString*)getName
{
    return name;
}
@end

でも、以下のように関数内で生成したオブジェクトを返す場合は、どう考えればよいのでしょうか。関数を抜けた際、参照カウンタが-1されてしまうと、参照カウンタが0になるのでオブジェクトが破棄されてしまいます。MRCでは参照カウンタを-1するタイミングをずらすためにautoreleaseを用いていました。ARCではどのようになるのでしょうか。

NSString* generateName()
{
    NSString *name = [NSString stringWithString:@"James"];
    return name;
}

ただ、この点について、どのように実装されているのかあまりはっきり書かれていません。Clangの資料では3.2.3 Unretained return valuesが該当箇所と思われます。以下は、該当部分の(だいたいの)訳です。

“メソッドや関数で、Objective-Cオブジェクト(つまり、参照カウンタを持つオブジェクト)を返すとなっているが、実際には、すでに参照カウンタが+1されている値を返すのではない場合は、オブジェクトが戻り側の境界において有効であることを保証しなければならない。
このような関数やメソッドから戻る場合、ARCはreturnステートメントを評価する時点でカウンタを+1する。そして、すべてのローカルスコープにおいてその状態を維持し、呼び出し境界において値が存在することを保証する。最悪のケースでは、autoreleaseが関与するかもしれない。しかし、呼び出し側は値が実際にautoreleaseプールにあるかどうかを仮定してはならない。
ARCは、呼び出し側ではこれ以上の作業は行わない。しかしながら、もしかしたら戻り値の生存期間を短くするために何かするかもしれない。”

結局はっきりしないので、以下のようなコードで実際にどこでオブジェクトが破棄されるのかを確認しました。

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
-(void)dealloc;
@end

@implementation MyClass
-(void)dealloc {
    NSLog(@"dealloc");
}
@end

MyClass* test()
{
    NSLog(@"enter test() scope");
    MyClass *obj1 = [[MyClass alloc]init];
    NSLog(@"leave test() scope");
    return obj1;
}

int main (int argc, const char * argv[])
{
    NSLog(@"enter main scope");
    @autoreleasepool {
        NSLog(@"enter autorelease scope");
        {
            NSLog(@"enter inner scope");
            MyClass *obj2 = test();
            NSLog(@"leave inner scope");
        }
        NSLog(@"leave autorelease scope");
    }
    NSLog(@"leave main scope");
    return 0;
}

考えられるのは、obj2がスコープから外れるタイミング、つまり「leave inner scope」と「leave autorelease scope」の間です。しかしながら、実際に実行してみると以下のような結果となります。

objc_unretained_return[1083:707] enter main scope
objc_unretained_return[1083:707] enter autorelease scope
objc_unretained_return[1083:707] enter inner scope
objc_unretained_return[1083:707] enter test() scope
objc_unretained_return[1083:707] leave test() scope
objc_unretained_return[1083:707] leave inner scope
objc_unretained_return[1083:707] leave autorelease scope
objc_unretained_return[1083:707] dealloc
objc_unretained_return[1083:707] leave main scope
Program ended with exit code: 0

つまり、autoreleaseの後となります。ここから推測されるのは、戻り値はautoreleaseプールに入るということになります。つまり、Clangの資料の後半の実装になっているようです。

*) ですので、@autoreleasepool {}は必ず記述する必要あります。

ブリッジキャスト

C/C++のオブジェクトとObjective-Cのオブジェクトの間の変換を行うのが、ブリッジキャストです。Objective-Cしか使わないという方は読飛ばしてかまいません。
Objective-Cのオブジェクトは、すべて参照カウンタを持っています。対して、C/C++のオブジェクトは持っていません。なので、単純にキャストすることはできません。

(__bridge T) op

(__bridge T) opは、opをT型へキャストします。もしTがObjective-Cオブジェクトポインタ型ならば、opはC/C++オブジェクトポインタ型でなければなりません。もし、TがC/C++オブジェクトポインタ型ならば、opはObjective-Cオブジェクトポインタ型でなければなりません。所有者の移動はないので、ARCは参照カウンタを+1しません。

T *obj2 = (__bridge T)obj1;

(__bridge_retained T) op

(__bridge_retained T) opは、Objective-Cオブジェクトポインタ型であるopを、C/C++オブジェクトポインタ型 Tへ変換します。ARCはopの参照カウンタを+1します。

(__bridge_transfer T) op

(__bridge_transfer T) opは、C/C++オブジェクトポインタ型であるopを、Objective-Cオブジェクトポインタ型 Tへ変換します。ARCはopの参照カウンタを-1します。

通常、(__bridge_retained T) opと(__bridge_transfer T) opは対で使います。__bridge_retainedで、C/C++へポインタを渡し、__bridge_transferで結果のポインタを戻すことになります。

補足: C++プログラマにとってのARC

C++ 11で正式に採択されたstd::shared_ptr<>が、Objective-Cでの強い参照(__strong)となり、std::weak_ptr<>がObjective-Cでの弱い参照(__weak)となります。関数の戻り値については、呼び出し元で受け取った際に+1されるので、ややこしい仕組みは持っていない(はず)です。Clangの資料の前半に近い実装なのだと思います。

補足: WindowsプログラマにとってのARC

MRCがCOM(AddRef/Release)、ARCがATL(CComPtr)と思ってください。