Swift & Objective-C

@autoreleasepool 完全解説(ループ内のメモリ急増を防ぐ方法)

「ARCの時代なのに、なぜ今も@autoreleasepoolを使うの?」

読了 5 分
@autoreleasepool 完全解説(ループ内のメモリ急増を防ぐ方法)のカバー画像

「ARCの時代なのに、なぜ今も@autoreleasepoolを使うの?」

後輩からよく聞かれる質問です。どこかで見たキーワードなのは確かですが、いつ、なぜ使うのかをすぐ説明するのは意外と難しいものです。

今日は@autoreleasepoolとは何か、実際にはいつ使うのかまで解説します。

まず結論からです。

@autoreleasepoolは、「あとで解放するよう予約されたオブジェクト」を、任意のタイミングで先に解放するブロックです。ループ内で一時オブジェクトが大量に発生するとき、メモリの急増を防ぐのが代表的な用途です。

順に見ていきましょう。


オートリリースプールとは?

まずは背景となる概念を押さえます。

Objective-Cには以前からautoreleaseという仕組みがありました。オブジェクトに「今ではなく、少しあとでreleaseして」と予約する方式です。

このように予約されたオブジェクトが順番を待つリストが、autorelease poolです。

そしてプールがdrainされると、リスト内のオブジェクトが一斉にreleaseされます。

では、このプールはいつ空になるのでしょうか?

iOSアプリではメインランループが自動的に管理します。タッチイベントの処理から画面更新までの1サイクルが終わるたびに、プールをすべて空にします。

そのため、普段は私たちが気にする必要はありません。


問題はループで起きる

ただし、「サイクルが終わるたびに」という条件が問題になる状況があります。

1回のランループが回る間に、一時オブジェクトが大量に蓄積するケースです。

たとえば、1つのループで数千枚の写真を処理するとします。

for (int i = 0; i < 5000; i++) {
    // 毎回、大きな画像オブジェクトが一時的に生成されます
    UIImage *image = [self loadAndResizeImage:i];
    [self saveThumbnail:image];
}

ループが実行されている間、ランループにはプールを空にする隙がありません。

そのため、解放予約だけされた一時オブジェクトが、5,000枚分そのままメモリに蓄積します。

メモリグラフは山のように跳ね上がり、ひどい場合はシステムがアプリを強制終了します。

ループ中にメモリグラフが山のように跳ね上がる瞬間
ループ中にメモリグラフが山のように跳ね上がる瞬間

@autoreleasepoolで山を削る方法

解決策は簡単です。ループ内に専用のプールを作り、1回ごとに自分で空にします。

for (int i = 0; i < 5000; i++) {
    @autoreleasepool {
        UIImage *image = [self loadAndResizeImage:i];
        [self saveThumbnail:image];
    } // ブロックが終了した瞬間、一時オブジェクトが解放されます
}

@autoreleasepoolブロックが閉じると、ブロック内で作られた一時オブジェクトがすぐに解放されます。

5,000枚分のメモリがたまって一度に解放される代わりに、1枚分ずつ増えては減る動きになります。

山型だったメモリグラフが、穏やかなノコギリ波に変わります。


Swiftではこう使います

「Swiftしか使わないのですが?」と思うかもしれません。Swiftにも同じツールがあります。

autoreleasepool関数です。

for i in 0..<5000 {
    autoreleasepool {
        let image = loadAndResizeImage(i)
        saveThumbnail(image)
    }
}

注意点が1つあります。

純粋なSwiftオブジェクトの多くはautorelease poolを経由せず、スコープが終わるとすぐに解放されます。

このツールが本当に効果を発揮するのは、UIImage、Data(contentsOf:)、NSDataのようなObjective-CベースのFoundation・UIKit APIを繰り返し呼び出すときです。

内部でautoreleaseオブジェクトを作って返すAPIは、今も多く存在します。

Swiftでもautoreleasepoolブロック1つでグラフが穏やかになります
Swiftでもautoreleasepoolブロック1つでグラフが穏やかになります

こんなときに使いましょう

まとめると、@autoreleasepoolが必要なのは次のような場合です。

  • ループ内で画像・ファイル・文字列などの大きな一時オブジェクトを大量に生成するとき
  • バックグラウンドスレッドで、ランループなしに長時間処理するとき
  • コマンドラインツールのようにランループ自体がない環境でObjective-Cオブジェクトを使うとき

逆に、通常のUIコードや一般的なビジネスロジックでは、無理に使う必要はありません。メインランループがすでに適切に管理しているからです。

計測せずに習慣でコードを囲むこともおすすめしません。InstrumentsやXcodeのメモリグラフで実際にスパイクを確認してから使うのが基本です。


よくある質問(Q&A)

Q. ARCが自動で処理するのに、なぜ自分でプールを管理するのですか?

ARC(Automatic Reference Counting)はretain・releaseの挿入を肩代わりするだけで、autorelease poolがいつ空になるかまでは変えません。プールを空にする「タイミング」を早めるのは、今も開発者の役割です。

Q. main関数にある@autoreleasepoolとは何ですか?

Objective-Cプロジェクトのmain.mを開くと、アプリ全体が@autoreleasepoolで囲まれています。これがアプリの最上位プールです。その中でランループが動き、サイクルごとに子プールを空にします。

Q. ブロックをネストしてもよいですか?

はい。プールはスタックのように積み重なります。内側のブロックを閉じると内側のプールだけが空になり、外側のプールはそのまま維持されます。

プールはスタックのように積み重なり、内側から空になります
プールはスタックのように積み重なり、内側から空になります

簡単にまとめると、@autoreleasepoolは「解放予約された一時オブジェクトを、指定したタイミングで空にするブロック」です。

ループでメモリが山のように増えることをInstrumentsで確認したら、ループの内側をこのブロックで囲んでみてください。グラフが穏やかになる様子を確認できるはずです。

MRC(Manual Reference Counting)からARCへ移行した歴史に興味がある方は、以前まとめた「Objective-Cメモリ管理の歴史」もあわせてお読みください。今日も楽しいコーディングを!


参考資料

あわせて読みたい