Swift & Objective-C

Objective-Cメモリ管理の歴史:MRCからARCまで総まとめ

iOS開発をしていると、一度は気になることがあります。

読了 7 分
Objective-Cメモリ管理の歴史:MRCからARCまで総まとめのカバー画像

iOS開発をしていると、一度は気になることがあります。

「昔のObjective-Cコードには、なぜretainreleaseが大量に書かれているの?」

Swiftから入った方なら、古いコードを開いて戸惑った経験があるかもしれません。

今回は、Objective-Cのメモリ管理がMRCからARCへどう移り変わったのか、その歴史を整理します。

まず結論から説明します。

Objective-Cのメモリ管理は、開発者が手動で数えていた時代(MRC)から、コンパイラが代わりに数える時代(ARC)へ移行しました。

途中でガベージコレクションも試されましたが、最終的に2011年にARCが登場し、方向性が定まりました。

ここから時代ごとに見ていきましょう。


参照カウントとは? メモリ管理の基本概念

歴史に入る前に、まず1つだけ概念を押さえます。

それが参照カウント(reference count)です。

各オブジェクトに「自分を保持している人が何人いるか」を数える数字が付いていると考えてください。

この数字が1以上なら生存し、0になるとメモリから解放されます。

つまり、オブジェクトを保持するときは数字を増やし(retain)、手放すときは減らします(release)。

この単純なルールがObjective-Cのメモリ管理の基礎です。

1つの数字で生死が決まる参照カウントの流れ
1つの数字で生死が決まる参照カウントの流れ

MRC時代:開発者が数字を手動で数えていた頃

ARC以前、MRC(Manual Retain Count)の時代には、この数字を人が手作業で管理していました。

retainとreleaseだらけの昔のコード。初めて見ると戸惑いますよね
retainとreleaseだらけの昔のコード。初めて見ると戸惑いますよね

ルールは明確でした。よく「NARC」という言葉で覚えました。

  • New、Alloc、Retain、Copyで作ったオブジェクトは自分が責任を持つ
  • 責任を持つオブジェクトは使い終わったら必ずreleaseする

実際のコードはこのようなものでした。

// オブジェクトを作成すると、参照カウントが 1になります
NSObject *obj = [[NSObject alloc] init];
[obj retain];   // カウント 2
[obj release];  // カウント 1
[obj release];  // カウント 0 → メモリを解放

問題はここで起きました。

releaseを忘れるとメモリが漏れ、早く解放しすぎると、消えたオブジェクトに触れてアプリがクラッシュしました。

いわゆる「ゾンビオブジェクト」クラッシュです。

保持しているオブジェクトのカウントがいくつか、常に頭の中で数え続ける必要があり、本当に気を使う作業でした。


autorelease:「今ではなく、少し後で手放します」

MRC時代には、もう1つ厄介な状況がありました。

メソッドが新しいオブジェクトを作り、外部へ返す場合です。

- (NSString *)greeting {
    NSString *msg = [[NSString alloc] initWithString:@"こんにちは"];
    return msg; // 自分が alloc 作ったので release するべきだけど…いつ?
}

ルール上、allocした側がreleaseする必要があります。

しかしreturn前にreleaseすると、受け取る側が使う前にオブジェクトが消えます。かといってreleaseしなければメモリリークです。

このジレンマを解決するために登場したのがautoreleaseです。

「今すぐではなく、少し後でreleaseして」と予約しておく仕組みです。

autoreleaseされたオブジェクトは「オートリリースプール(autorelease pool)」という待機リストに入り、プールが空になる時点(通常はランループが1周終わった時点)でまとめてreleaseされます。

そのため、受け取る側はオブジェクトを安全に受け取り、使うことができました。

[NSString stringWithFormat:]のようにallocなしで直接受け取れる便利な生成メソッドは、すべてこの方式でオブジェクトを返していました。

このオートリリースプールはARC時代にも@autoreleasepoolブロックという名前で残っています。ループでメモリが急増したときに使う実践的なツールなので、詳しくは別の記事にまとめました。


ガベージコレクションはなぜ失敗したのか?

Appleもこの不便さを把握していました。

そこで2007年、Mac OS X 10.5 Leopardで、Javaのようにメモリを自動で掃除する「ガベージコレクション(GC)」を導入しました。

開発者はretainreleaseを書かなくてよく、便利そうに見えました。

しかし、結果は芳しくありませんでした。

GCがバックグラウンドで動作するため、予測できないタイミングでアプリが一時的に停止する問題がありました。特に性能とバッテリーに敏感なiPhoneには大きな負担でした。

そのため、iOSには導入されませんでした。

結局、Mac向けGCも2012年のOS X 10.8からdeprecatedとなりました。


ARCの登場:コンパイラが代わりに数える

そして2011年、Appleが解決策として発表したのがARC(Automatic Reference Counting)です。

Xcode 4.2、iOS 5、OS X 10.7 Lionとともに公開されました。

ARCの発想は巧妙でした。

参照カウント方式は維持しつつ、retainreleaseを開発者ではなくコンパイラが自動で挿入するのです。

GCのように実行時に別の掃除処理を動かすのではなく、コンパイル時に必要な場所へ解放コードを自動で入れる方式です。

その結果、実行時の性能負担をほとんど増やさずにメモリ管理を自動化できました。

2つの時代を表で比較すると、次のようになります。

区分 MRC ARC
登場時期 初期~2011年 2011年(iOS 5)
retain/release 手動で記述 コンパイラが自動挿入
autorelease 直接呼び出し コンパイラが管理
メモリーリークのリスク 高い 大幅に低い
性能負担 なし ほぼなし

ただし、ARCも万能ではありません。

互いに保持し合う「循環参照」は自動で解決できないため、開発者がweakunownedのような弱参照で切断する必要があります。

この概念は、現在使っているSwiftにもそのまま受け継がれています。


よくある質問(Q&A)

Q. 今でもMRCを学ぶ必要はありますか?

実務で新たにMRCでコードを書くことはほとんどありません。ただし、古いライブラリや面接で概念を聞かれることがあるため、原理は知っておくとよいでしょう。

Q. autoreleaseはARCでも使われますか?

直接呼び出すことはありませんが、仕組み自体はARCの下でも動作しています。そのため、@autoreleasepoolブロックは今でも有効なツールです。

Q. SwiftもARCを使いますか?

はい、SwiftもARCを基盤としています。そのため、循環参照とweakの概念はSwiftでも同じように重要です。


簡単にまとめると、Objective-Cのメモリ管理は「手動で数えるMRC → 短期間のGC実験 → コンパイラが数えるARC」へ発展してきました。

今では当然の自動メモリ管理も、数多くのクラッシュと試行錯誤を経て磨かれた結果です。

iOS開発を学ぶ方にとって、この流れが小さな地図になれば幸いです。今日も楽しいコーディングを!


参考資料

あわせて読みたい