HOME » Natsu note » iOS » [iOS5] ARC : 循環参照

[iOS5] ARC : 循環参照 2011/11/25/|iOS, ,

これまでの記事はこちら:

循環参照とは

今回は、強参照(Strong reference)を使うときに注意したい循環参照(Strong reference cycle)についてです。循環参照とは、その名の通り、2つ以上のオブジェクトが強参照し合うことにより、どちらのオブジェクトも破棄できない状態を言います。

ここで、循環参照が発生するのは、お互いに”強参照“しているときです。複数のオブジェクトが親子関係を持つ場合を考えてみます。

アドレス帳オブジェクトAddrBookと、そのエントリーEntryがあるとします。AddrBookはEntryオブジェクトのentryを、Entryは、自身がどのアドレス帳に含まれているかを示すAddrBookオブジェクトaddrBookを持ちます。

このとき、もし、entryもaddrBookも強参照だと循環参照が発生します。

ARC strong reference cycle


この場合、Entryオブジェクトは、AddrBookから強参照されているので、破棄されません。一方で、Entryオブジェクトが破棄されないと、AddrBookオブジェクトへの強参照もなくならないため、こちらも破棄されないことになります。

弱参照を使って解決

このように、複数オブジェクトが親子関係を持つ場合には、片方を弱参照にすることで循環参照を回避することができます。一般的には、親オブジェクトが子オブジェクトのオーナーとなり(強参照し)、子オブジェクトは、親オブジェクトを弱参照するのみとします。

ARC avoid strong reference cycle


これであれば、AddrBookオブジェクトを強参照している変数がなくなれば、AddrBookオブジェクトは破棄され、それと同時にEntryオブジェクトへの強参照もなくなります。

また、先にAddrBookオブジェクトが破棄された場合でも、EntryオブジェクトのaddrBook変数にはZeroingによりnilが代入されますので、破棄済みオブジェクトにアクセスしてクラッシュするような心配もありません。

Delegateパターンの場合

Cocoaではよく使われるdelegateパターンですが、ここでも循環参照を避けるために弱参照を使う必要があります。

ViewControllerから、DetailViewControllerをModalViewなどで開く場合、DetailViewControllerを閉じる動作を決めるために、delegateを設定することがよくあります。

ARC delegate pattern


ここで、ViewControllerがdetailViewControllerを強参照している場合、delegateを弱参照にしないと循環参照が発生します。そもそも、delegateは、親となるViewControllerが存在しなくなった時点で意味をなさなくなるので、弱参照が適しているはずです。

なお、自分で作成したクラスのdelegateにweakプロパティを使っている場合、Zeroingにより、参照先が破棄されたら自動的にnilがセットされますので、わざわざ自分でnilをセットしなくても問題ありません。

Blocksの場合

Blockも一つのオブジェクトだと考えられます。Blockによるキャプチャに注意しておかないと、ここでも循環参照が発生します。

Blockへのcopyプロパティを持つMyObjectを例に考えてみます。

typedef void(^MyBlock)(void);

@interface MyObject : NSObject
@property (nonatomic, copy) MyBlock block;
@property (nonatomic, strong) NSString *str;

- (void)performBlock;
@end
@implementation MyObject
@synthesize block, str;

- (void)performBlock {
    if (self.block) {
        self.block();
    }
}
@end

呼び出し側では以下のようにしたとします。

MyObject *object = [[MyObject alloc] init];
object.str = @"hoge";

object.block = ^{
    NSLog(@"block: str=%@", object.str);
};
[object performBlock];

ARC strong reference cycle blocks

Block構文の中でobjectを参照しています。したがって、object.blockは、objectをキャプチャし保持します。これにより、objectがblockを強参照し、blockがobjectを強参照することになります。これが、Blockによる循環参照です。


解決方法 その1: __block修飾子を使う

Blockによる循環参照を回避する方法はいくつかありますが、その一つ目は、Block内で、処理が終了したらobjectを解放する方法です。そのためには、__block修飾子を使用して、objectを読み書き可能にする必要があります。

なお、ARC以前では、__block変数はキャプチャされませんでしたが、ARCの場合挙動が違います。__block変数が持つ意味は、Block内で読み書き可能となるだけで、キャプチャに関しては通常の変数と変わりません。

__block MyObject *object = [[MyObject alloc] init];
object.str = @"hoge";

object.block = ^{
    NSLog(@"block: str=%@", object.str);
	object = nil;
};
[object performBlock];

これで、blockがobjectの強参照をやめるため、循環参照が解消されます。しかしながら、この方法には一つ欠点があります。objectの解放がBlock内で行われているため、Blockが実行されないと循環参照したままとなります(上記コードで、[object performBlock];をコメントアウトすると循環参照します)。

解決方法 その2: __weak修飾子を使う

もう一つの解決方法です。この方法では、Block内からの参照に弱参照を使います(キャプチャされるのはweak変数)。

MyObject *object = [[MyObject alloc] init];
object.str = @"hoge";
    
__weak MyObject *weakObject = object;
object.block = ^{
    NSLog(@"block: str=%@", weakObject.str);
};
[object performBlock];

これだと、blockが参照しているweakObjectは弱参照ですので、循環参照は発生しません。

さらに、この方法をもう少し安全にしたのが以下です。

MyObject *object = [[MyObject alloc] init];
object.str = @"hoge";
    
__weak MyObject *weakObject = object;
object.block = ^{
	MyObject strongObject = weakObject;
	if (strongObject) {
	    NSLog(@"block: str=%@", strongObject.str);
	}
};
[object performBlock];

ここで例にあげたようなシンプルな実装ではここまでする必要はありませんが、非同期でBlocksを使う場合など、weak変数であるweakObjectがnilになってしまう可能性があります。したがって、一度、strong変数として保持しておき、nilチェックを行うということです。

注意: キャプチャされるのは str ではなく object

今回、あえてBlock内で使う変数をobject.strとしてみました。ここで注意が必要なのは、キャプチャされるのは、strではなく、objectそのものだということです。

したがって、仮にMyObjectがNSInteger型のvalueというプロパティを持っていたとして、Block内でobject.valueを利用しても結果は同じです。

Blocksを利用する場合には、Block内で利用している変数とインスタンス変数との関係をよく考えて、循環参照が起きないように注意しましょう。

まとめ

個人的には、ARCの利用で一番怖いかなと思っている循環参照についてまとめました。循環参照していても気がつきにくいので、知らないうちにメモリリークして悲しい事態にならないよう、実装時には細心の注意を払いたいものです。

質問、間違いの指摘はツイッターでお願いします。@natsun_happy

参考資料

ARCの詳細を書きました↓

iOS5プログラミングブック
加藤 寛人 吉田 悠一 藤川 宏之 西方 夏子 関川 雄介 高丘 知央
インプレスジャパン
売り上げランキング: 243,952

iOS 6 UICollectionViewについて書きました↓

iPhoneアプリ開発エキスパートガイド iOS 6対応
加藤 寛人 藤川 宏之 高丘 知央 西方 夏子 吉田 悠一 関川 雄介
インプレスジャパン
売り上げランキング: 4,856