Objective-C ARC下での強参照、弱参照、循環参照について学ぼう

  • 194
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

objcにARCが導入されてから久しいですが、開発者がメモリ管理から解放されたかというと、そうでもないです。strongとweakの導入によって、少し抽象化がなされましたが、オブジェクトのallocate/releaseは意識してプログラムする必要があります。この記事は、ARC環境下での強参照/弱参照/循環参照についてイマイチ理解が及ばないという人のための記事であると同時に、私の個人的なまとめでもあります。

循環参照とは?

循環参照という現象は、私も最初はなかなか理解できませんでした。まず、文字的な定義から言うと、循環参照(retain cycle)とは 「オブジェクト同士がお互いに強い参照を持っているからどちらも解放されない」という現象のことです。

iOSにはObjective-C 2.0のランタイムが実装されていますが、ガベージコレクションは存在しません。理由はよく知らないですがシステム的な余裕がないのかもしれません。(というか、OSX10.8からもXcodeはGCを公式でDeprecatedにしており、ARCがスタンダードになっていくのは間違いありません)

強参照/弱参照、メモリリーク

私はちょうどARCが導入された頃(二年くらい前?)にobjcを書き始め、とくにメモリ管理に気を使わずに長いことプログラミングを楽しんできました。いまでも実際の場面でほとんどメモリ管理を気にすることはありません。(非同期Blocksの中にstrong selfをキャプチャしないようにするのに注意するくらい)

強参照/弱参照の分かりにくい点は、その名前にひとつの原因があると思っています。

そもそも、 オブジェクトへの参照(reference)とは、ヒープ領域に動的にallocateされたobjcオブジェクトへのポインティングのことを差し ているのですが、強参照/弱参照については、より具体的に言うと 「オブジェクトのオーナーシップ(所有権)」 のことを意味しています。

オブジェクトがヒープ領域に存在するとき、それはそのオブジェクトがどこからか参照されている(インスタンス変数、静的変数、局所変数、...)ことを意味しますが、これを言い換えると、 そのオブジェクトは何者かによって所有されている ということを意味します。所有権は、必ず親子関係が必要になり、親が子を所有し(強参照し)、そのメモリ開放のタイミングの責任を持つようになります。


@interface SomeObject ()

@property NSString *hoge;

@end

@implementation

- (void)someMethod
{
    NSString *s = [[NSString alloc] initWithString:@"hogehoge"];
    self.hoge = s; 
}

@end

上記のコードは、あるオブジェクトが自身のインスタンスメソッドの中で、インスタンス変数へ文字列を代入した部分です。プロパティhogeは、省略されていますが、デフォルトでstrong,copyの属性を持っているので、sの所有者はself、つまりSomeObjectのインスタンスになります。所有されたオブジェクトは、所有者(SomeObject)が消えた(メモリが解放された)ときに、芋づる式に開放されるという特性を持ちます。


@interface SomeObject ()

@property NSString *hoge;
@property NSMutableArray *array;
@property NSMutableDictionary *dictionary;

@end

@implementation

- (void)someMethod
{
    NSString *s = [[NSString alloc] initWithString:@"hogehoge"];
    NSString *key = @"fugafuga";
    self.hoge = hoge;
    [self.array addObject:s];
    [self.dictionary setObject:s forKey:key];
}

@end

上記のようなコードも同様です。が、ちょっと考え方を変える必要があります。それは、コレクションクラスの参照の特徴です。Cocoaには、代表的なコレクションクラスとして、NSArray, NSDictionary, NSSet, NSOrderedSetが存在しますが、これらのクラスは、型を制限せず、C プリミティブ型以外のすべてのobjcオブジェクトを格納することができます。 通常、コレクションクラスはその内部のオブジェクトに対して強参照を持つ のですが、これはコレクションクラスが存在しているときにコンテンツに対する参照をロストすることを防ぐためです。配列に放り込んだはずがいつの間にかオブジェクトが解放されていて"解放済み領域の不正なアクセス"になったら困りますよね。

ちなみにNSSet、NSDictionaryは、valueだけではなく、keyに対しても強参照を持っています。NSDictionaryはNSCopyingプロトコルに準拠したオブジェクトをkeyに設定できますが、それはそのコピーをkeyとして用いているからです。上記のコードでは、このオブジェクトが開放されるまで、文字列@"hogehoge"と@"fugafuga"は解放されません。

1つのオブジェクト、複数のオーナー

さっき私は オブジェクトの所有権には親子関係がある と言いました。ですが、前述の例では親子関係は単一のものしかありませんでいたが、この例では文字列sに対して、少なくともself, self.array, self.dictionaryの三つの異なるオブジェクトが強参照を持っています。

これでこのオブジェクトが解放されたとき、きちんとsは消えてくれるのでしょうか?

それを現したのが下のgif画像です。

untitle.gif

これから分かるように、sに対する所有者は複数存在しますが、そのすべてがリニアな親子関係にあなので、最上位の所有者であるSomeObjectが解放されない限り、子供たちは解放されませんし、、SomeObjectが解放された場合、上位から順に所有者が消えた強参照が消滅して結果的に最下位のsもきちんと消えてくれます。これがARC下でのオブジェクトのオーナーシップの基本です。

では弱参照とはなんでしょうか。循環参照と合わせて説明します。

弱参照と循環参照


@interface ParentObject ()

@property SharedObject *shared;
@property SomeObject *some;
@property OtherObject *other;

@end

@interface SomeObject ()

@property SharedObject *shared;
@property ParentObject *parent;

@end

@interface OtherObject ()

@property SharedObject *shared;
@property ParentObject *parent;

@end

ParentObject.m
@implementation

- (id)init
{
    if (self = [super init] ) {
        self.shared = [SharedObject alloc] init];
        self.some = [SomeObject alloc] init];
        self.other = [OtherObject alloc] init];
        self.some.shared = self.shared;
        self.other.shared = self.shared;
        self.shared.some = self.some;
        self.shared.other = self.other;
        self.shared.parent = self;
    }
    return self ? self : nil;
}

@end

なんだか色々と間違っている気がするこんな三つのオブジェクトがあったとします。 これらは親子関係があるにもかかわらず、親子の間で頻繁にデータをやり取りしたりメソッドを呼び出したりする必要があるようです。そこで横着してこれら3つをすべて双方向に参照するようなプロパティを作ってしまいました。するとどうなるか。

2untitle.gif

こんなことが起きます。実装者としては、ParentObjectが消えるタイミングでSome,Other,Sahredの三つのオブジェクトも消えてほしいのですが、実際はParentが消えることはありません。なぜなら、Parentに対してSome,Other,Sharedが所有権を主張しているからです。

あるオブジェクトが別のオブジェクトに対して強参照を持つとき、強参照されている(所有されている)オブジェクトは、所有者が消えるまで、解放されません。これがオーナーシップの基本です。通常は、先の例で上げたように所有者と所有物の間に単方向の強参照があり、芋づる式に消えて行くのですが、もしその参照が双方向だった場合どうなるのでしょう?

答えは、お互いがお互いの所有権を主張しており、Aが本来消えるタイミングではBが所有権を主張して解放させず、Bが本来消えるタイミングではAが所有権を主張して解放されませんA,Bへの他のあらゆるオブジェクトからの参照が消えたとしても、AとBはお互いがお互いを所有しあっているので消えません。 これが循環参照です。

循環参照を防ぎつつ、双方向の参照を残しておきたいような場合に使う属性が、弱参照(weak reference)

強参照が所有権だとすれば、弱参照は使用権です。所有権を持つオブジェクトは、その所有物に対して自由に参照が可能で、かつ、その開放権を持っているといえ、使用権を持っているオブジェクトは、対象のオブジェクトを好きなときに参照し、使用することができるものの、所有者がそのオブジェクトを開放してしまったら、使用することができません。

なぜなら、オブジェクトの所有権を持っていないからです(ちなみに、オブジェクトの参照が消えた場合、weak propertyには自動的にnilが代入されるので解放済み領域への不正なアクセスにはなりません)。

所有者をコピー屋さん、所有物をコピー機、使用者を自分と考えると分かりやすいかもしれません。自分は好きなときにコピー屋にいってコピーができますが、コピー屋が潰れたらコピー機は処分されて、もう使うことはできない。イメージとしてはそういうことです。

先の循環参照のモデル図を改良すると以下のようになります。強参照が単方向になり、開放が正しく行われていることが分かります。

1untitle.gif

これから分かることは、 強参照は単方向でさえあれば、いくつ所有者があっても構わない ということです。

まとめ

ARCに慣れ親しんだプログラマにとっては少し複雑なstrong/weakリファレンスですが、きちんと理解しないとdelegateやnotification,KVOなどで思わぬバグを産むのできちんと理解しておいたほうが良さそうです。

※2014/03/02 9:59追記NSStringのpropertyの属性値について

@tomohisaota さんからご指摘いただいたNSStringのpropertyのデフォルトの属性値ですが、調査の結果copyはないことが判明しました。strongだけのようです。