Swift 與 Objective-C

@autoreleasepool 完整整理(解決迴圈中的記憶體暴增)

「都已經是 ARC 時代了,為什麼還要使用 @autoreleasepool?」

閱讀 4 分鐘
@autoreleasepool 完整整理(解決迴圈中的記憶體暴增) 封面圖

「都已經是 ARC 時代了,為什麼還要使用 @autoreleasepool?」

這是我常被後輩問到的問題。明明是在某處看過的關鍵字,但要立即回答何時以及為什麼使用,意外地不容易。

今天會從@autoreleasepool是什麼開始,一路說明實務上何時該使用它。

先說結論。

@autoreleasepool是一個區塊,能在你指定的時間,提前釋放那些「預約稍後釋放的物件」。在迴圈中大量產生暫存物件時,防止記憶體暴增是它最典型的用途。

我們逐一來看。


什麼是自動釋放池?

先從背景概念開始。

Objective-C很早以前就有autorelease這項機制。它會把物件預約成「現在先不要,過一會兒再release」。

這些已預約物件排隊等待的清單,就是 autorelease pool。

當 pool 被 drain 的瞬間,清單中的物件會一次收到 release。

那麼,這個 pool 什麼時候會被清空呢?

在 iOS App 中,主執行迴圈會自動管理。每次完成一個週期——處理觸控事件再更新畫面——就會將 pool 徹底清空。

所以平常不需要特別操心。


問題會在迴圈中爆發

不過,「每次週期結束時」這個條件,在某些情況下反而會造成問題。

也就是在一次執行迴圈運轉期間,累積了大量暫存物件。

例如,假設在一個迴圈中處理數千張照片。

for (int i = 0; i < 5000; i++) {
    // 每次都會暫時建立大型影像物件
    UIImage *image = [self loadAndResizeImage:i];
    [self saveThumbnail:image];
}

迴圈執行期間,執行迴圈根本沒有機會清空 pool。

因此,只設定等待釋放的暫存物件,會完整累積相當於 5,000 張照片的記憶體。

記憶體圖表像山一樣急升,嚴重時系統甚至會強制終止 App。

迴圈執行期間記憶體圖表像山一樣急升的瞬間
迴圈執行期間記憶體圖表像山一樣急升的瞬間

用 @autoreleasepool 剷平這座山

解法很簡單:在迴圈內建立專用的 pool,每跑一圈就自行清空。

for (int i = 0; i < 5000; i++) {
    @autoreleasepool {
        UIImage *image = [self loadAndResizeImage:i];
        [self saveThumbnail:image];
    } // 區塊結束的瞬間,暫存物件就會立即釋放
}

@autoreleasepool區塊關閉的瞬間,區塊內建立的暫存物件就會立即整理。

不再是累積相當於 5,000 張照片的記憶體後一次釋放,而是每次累積一張、釋放一張。

原本山形的記憶體圖表會變成平緩的鋸齒形。


在 Swift 中這樣使用

你可能會說:「我只使用 Swift 耶?」Swift 也有完全相同的工具。

就是autoreleasepool函式。

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

有一點需要注意。

純 Swift 物件大多不會經過 autorelease pool,作用域結束時就會立即釋放。

這項工具真正發揮作用的時機,是重複呼叫UIImage、Data(contentsOf:)、NSData等以 Objective-C 為基礎的 Foundation・UIKit API。

因為現在仍有許多 API 會在內部建立並回傳 autorelease 物件。

即使在 Swift 中,只要一個 autoreleasepool 區塊就能讓圖表變得平穩
即使在 Swift 中,只要一個 autoreleasepool 區塊就能讓圖表變得平穩

這些情況就該使用

總結來說,需要使用@autoreleasepool的時機包括以下情況。

  • 在迴圈中大量建立影像、檔案、字串等大型暫存物件時
  • 在背景執行緒中,沒有執行迴圈卻長時間執行工作時
  • 在命令列工具等沒有執行迴圈的環境中使用 Objective-C 物件時

反過來說,平常的 UI 程式碼和一般商業邏輯不需要特別使用。因為主執行迴圈已經管理得很好。

也不建議沒有量測就習慣性地包起來。應該先在 Instruments 或 Xcode 記憶體圖表中確認實際出現尖峰,再決定使用。


常見問題(Q&A)

Q. ARC 會自動處理,為什麼還要由我管理 pool?

ARC(Automatic Reference Counting,自動參考計數)只會代替你插入 retain・release,並不會改變 autorelease pool 何時清空。提前清空 pool 的「時機」,仍然是開發者的責任。

Q. main 函式中的 @autoreleasepool 是什麼?

開啟 Objective-C 專案的 main.m,會看到整個 App 都包在@autoreleasepool中,這就是 App 的最上層 pool。執行迴圈會在其中運作,並在每個週期清空子 pool。

Q. 可以巢狀使用區塊嗎?

可以。pool 會像堆疊一樣逐層堆疊。內層區塊關閉時,只會清空內層 pool,外層 pool 會維持原狀。

pool 像堆疊一樣累積,並從內層開始清空
pool 像堆疊一樣累積,並從內層開始清空

簡單總結,@autoreleasepool就是一個能在指定時機清空「已預約釋放的暫存物件」的區塊。

如果你用 Instruments 確認迴圈中的記憶體像山一樣暴增,請試著用這個區塊包住迴圈內部。你會清楚看到圖表變得平穩。

如果你對從 MRC(Manual Reference Counting,手動參考計數)轉向 ARC 的歷史感興趣,也推薦閱讀之前整理的「Objective-C 記憶體管理歷史」。祝你今天也享受寫程式!


參考資料

延伸閱讀