Help us understand the problem. What is going on with this article?

ARCとMRCを混ぜながらプログラムを書くための方法とその調査報告

More than 5 years have passed since last update.

想定読者

ARCでの強/弱参照の考え方は知っていて、ARCとMRCと共存できるらしい事は知っているために、MRCのコード資産もあるし少しずつ移行したいものの、実際にどうやって共存するのかはっきりわからないし、情報も少ないしで踏み切れない方。

ARCがMRCの後から出たわけだから下位互換があるのだとしても、MRCで作っているプロジェクトにARCを追加していく中で、逆にMRC部分からARC部分を呼び出す事になっても正しく動作するのか、などと気になっている方。

参考資料と背景

現状、ARCに関する信頼出来るドキュメントは次の2つぐらいです。

そこにはこんな事が書いてあります。

How do I think about ARC? Where does it put the retains/releases?

Try to stop thinking about where the retain/release calls are put and think about your application algorithms instead. Think about “strong and weak” pointers in your objects, about object ownership, and about possible retain cycles.

しかし、ARCではretain/releaseについて考えるのをやめろと言われても、MRCとの連携も考慮する上では、背景の動作を把握しないことには__autoreleasing修飾子とかもあるので、明らかにautoreleaseプールは存在していると言えます。

じゃぁそれがどのように動いているのかがわからないと、やっぱり実際に使うには不安です。そこでいろいろと検証してみました。

結論から言うと

ARCで実装したメソッドの動作

  • alloc + init の動作はMRCの時と同様。
  • デフォルトでは 返り値がautoreleaseされている。
  • 返り値をretainするメソッドを作る場合は、ヘッダでの宣言にNS_RETURNS_RETAINEDを付ける。

ARC環境から、MRCで実装されたメソッドを使う作法

  • alloc + init は同様に呼び出せる。
  • autoreleaseされたオブジェクトを返すメソッドは、普通に使える。

    • MRCで普通にプログラミングしていれば、返り値を無視してもメモリリークが起こらないように、必然的にこのように作っているはず。
  • 返り値をretainして返すメソッドについては、ヘッダでの宣言にNS_RETURNS_RETAINEDがついていなければ メモリリークになってしまう (ただし、alloc+initはOK)

    • ARC環境ではreleaseやautoreleaseが無いため、retainされて返されたオブジェクトの参照カウンタを減らす手段が無い。

MRC環境から、ARCで実装されたメソッドを使う作法

  • alloc + init は同様に呼び出せる。
  • デフォルトでは返り値はautoreleaseされていると考えて呼び出す。

    • MRCで普段そうしているはず。
  • NS_RETURNS_RETAINEDがついているメソッドは、返り値がretainされているため、releaseかautoreleaseを呼び出す。

alloc+initについて

原則がautoreleaseで、もしNS_RETURNS_RETAINEDなどの属性がついていると、挙動が変わる、というのが基本原理です。さらにそこに、Cocoa Naming Conversion が該当する場合、自動でそれに適した属性が付く、というルールが働いています。このルールが有るために、alloc+initやcopyWithZone:は問題なく動作しています。

修飾子ごとのオブジェクトポインタの挙動

本題では無いですが、補足として。

  • ローカル変数の__strongはスコープを抜ける時にreleaseしてくれる。

    • alloc+initから即__strongに代入する場合は、 スコープを抜けてすぐ解放される。
    • メソッドの返り値は、例え__strongに代入していても、 autoreleaseされているためautoreleaseプールを抜けるまで解放されない。
  • __autoreleasingに代入する時にretain+autoreleaseが呼び出される。

    ARC環境なら何をやっても安全だと思っていると、下記のようなコードのケースではバグが発生します。

    -(BOOL)doSomethingWithError:(NSError *__autoreleasing *)error{
        for(int i=0;i<10000;i++){
            @autoreleasepool{
                if(![self doSmallTask]){
                    if(error) *error = [self makeError];
                    return NO;
                }
            }
        }
        return YES;
    }
    
    

実験解説

一つのARCプロジェクト内で、-fno-objc-arcオプションを使用することで、ARCでビルドされる.mファイルと、MRCでビルドされる.mファイルを用意します。その中でお互いに呼び出し合うコードを作って挙動を調べました。

deallocでログを吐くようにして、解放タイミングを観察できるようにします。

ARCテストクラス

@implementation AMMARCClass

-(id)initWithName:(NSString *)name{
    self = [super init];
    if(self){
        NSLog(@"ARC %@ init",name);
        _name = name;
    }
    return self;
}

- (void)dealloc{
    NSLog(@"ARC %@ dealloc",self.name);
}
@end

MRCテストクラス

@implementation AMMMRCClass

-(id)initWithName:(NSString *)name{
    self = [super init];
    if(self){
        NSLog(@"MRC %@ init",name);
        _name = [name retain];
    }
    return self;
}

- (void)dealloc
{
    NSLog(@"MRC %@ dealloc",self.name);
    [_name release];
    [super dealloc];
}

まず基本の挙動

ARC環境の基本の挙動を確認します。

テストコード

NSLog(@"--- 1");
@autoreleasepool {
    if(YES){
        AMMARCClass * __strong a = [[AMMARCClass alloc]initWithName:@"apple"];
        AMMARCClass * __autoreleasing b = [[AMMARCClass alloc]initWithName:@"banana"];
        (void)a,(void)b;
    }
    NSLog(@"--- 2");
}
NSLog(@"--- 3");

出力

--- 1
ARC apple init
ARC banana init
ARC apple dealloc
--- 2
ARC banana dealloc
--- 3

結論

__strongはスコープ脱出時にreleaseするため、2より前にappleがdeallocされています。
__autoreleasingはautoreleaseプールに追加するため、2と3の間でbananaがdeallocされています。

ARC環境から、MRCでビルドしたコードを呼び出す

MRCクラス側に、alloc+initしてretainCountが+1の状態のオブジェクトを返すメソッドを用意します。NS_RETURNS_RETAINEDを付けるものと付けないものの二通りを用意します。

@interface AMMMRCClass : NSObject
+(AMMMRCClass *)makeRetainedWithName:(NSString *)name;
+(AMMMRCClass *)makeRetainedAnnotatedWithName:(NSString *)name NS_RETURNS_RETAINED;
@end
@implementation AMMMRCClass
+(AMMMRCClass *)makeRetainedWithName:(NSString *)name{
    return [[AMMMRCClass alloc]initWithName:name];
}
+(AMMMRCClass *)makeRetainedAnnotatedWithName:(NSString *)name{
    return [[AMMMRCClass alloc]initWithName:name];
}
@end

MRCでいえば、返ってきたオブジェクトを必ずreleaseしなければならないパターンです。(普通は作らないと思いますが。)また、これをMRC環境でビルドするので、retainedなオブジェクトが返る事は間違いありません。これを下記のようなARCコードから呼び出してみます。

コード

NSLog(@"--- 3");
@autoreleasepool {
    if(YES){
        AMMMRCClass * __strong c = [[AMMMRCClass alloc]initWithName:@"cherry"];
        AMMMRCClass * __strong d = [AMMMRCClass makeRetainedWithName:@"doll"];
        AMMMRCClass * __strong e = [AMMMRCClass makeRetainedAnnotatedWithName:@"eye"];
        (void)c,(void)d,(void)e;
    }
    NSLog(@"--- 4");
}
NSLog(@"--- 5");

出力

--- 3
MRC cherry init
MRC doll init
MRC eye init
MRC eye dealloc
MRC cherry dealloc
--- 4
--- 5

結論

cherryとeyeについては、__strongがスコープを抜けるタイミングでdeallocされているため、ARCがうまいこと判断している事がわかります。

一方、dollについては解放されていません。また、プロファイラで調べるとリークしています。アノテーションを付けないとリーク→デフォルトはretainedではなくautoreleaseであり、retainedな値を返す特殊なパターンではそれを伝える必要があるというARCの原則が読み取れます。

ARCでの返り値もチェック

ARC側にも同様なalloc+initしてreturnするだけのメソッドを、アノテーション付き、無しで用意します。

@interface AMMARCClass : NSObject
+(AMMARCClass *)makeRetainedWithName:(NSString *)name;
+(AMMARCClass *)makeRetainedAnnotatedWithName:(NSString *)name NS_RETURNS_RETAINED;
@end
@implementation AMMARCClass
+(AMMMRCClass *)makeRetainedWithName:(NSString *)name{
    return [[AMMMRCClass alloc]initWithName:name];
}
+(AMMMRCClass *)makeRetainedAnnotatedWithName:(NSString *)name{
    return [[AMMMRCClass alloc]initWithName:name];
}
@end

これをARC環境から呼び出してみます。

コード

NSLog(@"--- 5");
@autoreleasepool {
    if(YES){
        AMMARCClass * __strong f = [AMMARCClass makeRetainedWithName:@"fish"];
        AMMARCClass * __strong g = [AMMARCClass makeRetainedAnnotatedWithName:@"girl"];
        (void)f,(void)g;
    }
    NSLog(@"--- 6");
}
NSLog(@"--- 7");

出力

--- 5
ARC fish init
ARC girl init
ARC girl dealloc
--- 6
ARC fish dealloc
--- 7

結論

girlは6よりも前に解放されているため、ポインタgの破棄によるreleaseのタイミングで、即座に解放されている事が確認できます。これは、alloc+initの時の動きや、MRCの時にアノテーションを付けた時の流れと同じであり、返ってくるgirlはautoreleaseプールには入っていないから即座に解放されているとわかります。つまり、NS_RETURNS_RETAINEDをつけると、retainedなオブジェクトが返される事がわかります。

重要なのはfishの方です。6と7の間で解放されているため、autoreleaseプールの解放によって解放された事がわかります。つまり、返ってくるオブジェクトがautoreleaseされていたことがわかります。内部のコードはalloc+initだけなので、ARCがreturnするオブジェクトをautoreleaseした事がわかります。これはアノテーションをつけていない方なので、ARCでの基本の返り値はautoreleasedだとわかりました。

MRC環境からARCでビルドしたコードを呼び出す

さて、逆にMRCからARCを使うことはできるのでしょうか。これまでにわかった事を前提にしてコードを書いて確認します。

NSLog(@"--- 8");
@autoreleasepool {
    if(YES){
        AMMARCClass * h = [[[AMMARCClass alloc]initWithName:@"house"]autorelease];
        AMMARCClass * i = [AMMARCClass makeRetainedWithName:@"ion"];
        AMMARCClass * j = [AMMARCClass makeRetainedAnnotatedWithName:@"joke"];
        (void)h,(void)i,(void)j;
        [j release];
    }
    NSLog(@"--- 9");
}
NSLog(@"--- 10");

出力

--- 8
ARC house init
ARC ion init
ARC joke init
ARC joke dealloc
--- 9
ARC ion dealloc
ARC house dealloc
--- 10

結論

houseでは、alloc+initが同様に使える前提で、autoreleaseしています。ionでは、ARCのデフォルトはautoreleasedだという前提で、そのまま代入しています。jokeでは、retainedだという前提で、すぐにreleaseしています。

jokeは9の前に解放されているので、releaseを呼び出した時に解放された事、およびその後にクラッシュ等していないので、retainedなオブジェクトであり、autoreleaseによる二重解放等は起こっていない事がわかります。

9と10の間でionとhouseが解放されていることから、これらはautoreleaseされたことがわかります。MRCの時の自然なプログラミング作法に一致する動作をしていることがわかります。

ARCでの自動解放挙動の確認

ARCでは、アノテーションによりオブジェクトの返し方が変わりつつも、__strongに代入すればどちらも問題なく動くことが確認できました。では、代入せずに返り値を放置した場合には何が起こるのか、という事を確認します。

コード

NSLog(@"--- 11");
@autoreleasepool {
    if(YES) {
        (void)[[AMMARCClass alloc]initWithName:@"knight"];
        [AMMARCClass makeRetainedWithName:@"lime"];
        [AMMARCClass makeRetainedAnnotatedWithName:@"money"];
        (void)[[AMMMRCClass alloc]initWithName:@"nuke"];
        [AMMMRCClass makeRetainedWithName:@"ocean"];
        [AMMMRCClass makeRetainedAnnotatedWithName:@"pine"];
    }
    NSLog(@"--- 12");
}
NSLog(@"--- 13");

出力

--- 11
ARC knight init
ARC knight dealloc
ARC lime init
ARC money init
ARC money dealloc
MRC nuke init
MRC nuke dealloc
MRC ocean init
MRC pine init
MRC pine dealloc
--- 12
ARC lime dealloc
--- 13

結論

knightがinitされたあと、limeがinitされる前にknightが解放されています。retainしてくれる変数がなかったため、返った直後に解放されている事がわかります。

limeについては、12と13の間で解放されているので、autoreleaseプールに登録されていた事がわかります。

money、nuke、pineについても、knightと同様の挙動です。moneyはアノテーションによりretainedな返り値になっているため、nukeはalloc+initのため、pineは実装がretainedな返り値で、それをアノテーションによりARCに伝えられているためです。

oceanについてはメモリリークしてしまいました。これはretainedな返り値が来ている事をARCが把握できていないためで、代入した時の実験とも一致する結果です。

実験ソース

実験したソースはgithubで公開しています。

感想

ARCはてっきり、retained_returnが原則で、autoreleaseプールを完全排除、本当に必要な箇所だけでretain/releaseを行うような仕組みなのではないかと思っていました。

でも一方で、それだとMRCとの共用で問題が出まくる気もしていました。結局のところ、自然にMRCを書いた時のautorelease運用と同様だったという事ですね。

期待していたほどはパフォーマンスが上がらないんじゃないかという気もしますが、MRCに下位互換しつつも、楽に、安全にする、という意味ではきちんと上位互換になっているのは間違い無いと思います。

どんどん使って行きたいので情報が増えると嬉しいです。

(cycle collector導入されないかな・・・)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away