不能不說的 AutoreleasePool

為什么需要 AutoreleasePool

1. 延長對象生命周期

我們都知道,系統內存是有限的,要想系統一直正常高效運行著,就需要我們合理地管理內存,不需要的內存就應該及時釋放。在 Objective-C 早期年代采用的是 MRC 來管理內存,需要我們自己在合適的位置申請和釋放內存。在 LLVM 3.0 開始,Objective-C 引入 ARC(自動引用計數),就無需要我們手動管理內存了,系統會自動管理內存:

- (void)run {
    id __strong obj = [[NSObject alloc] init]; //生成并持有對象,retainCount = 1
    //... 使用 obj
} //obj 超出作用域,強引用失效,retainCount = 0

正常情況下,使用 release 都能幫助我們及時回收內存,防止內存泄漏。我們接著看下一種情況:

- (NSObject *)getObj {
    id __strong obj = [[NSObject alloc] init]; //生成并持有對象,retainCount = 1
    return obj;
}

- (void)run {
    id __strong obj = [self getObj]; //持有對象, retainCount = 2
    /*
     使用 obj
     */
}//obj 超出作用域,強引用失效,retainCount = 1

我們發現到最后 obj 的 retainCount = 1 并沒有被銷毀,那么 release 能解決這個問題嗎?

- (NSObject *)getObj {
    NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
    [obj release]; // 此處加入 release,導致 obj 提前釋放
    return obj;
    [obj release]; // 這兒根本沒什么作用...
}

所以,我們需要擴大 obj 的生命周期,而 AutoreleasePool 正好可以解決這個問題:被加入 AutoreleasePool 的對象,不會立即釋放,而是在 AutoreleasePool 結束時調用 [obj release] 來保證對象在超出指定生存范圍時能夠自動并正確釋放。所以上述方法正確的實現應該是:

- (NSObject *)getObj {
    NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
    [obj autorelease]; //系統自動插入,先將 obj 放入最近的 AutoreleasePool,稍后釋放(retainCount - 1)
    return obj;
}

2. 降低內存峰值

先看一個常見的面試題:

for (int i = 0; i < 100000000; i++) {
    NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
    str = [str stringByAppendingString:@" - world"];
}

上述代碼能正常運行嗎?如不能,請解釋原因并給出優化意見。

熟悉內存管理的同學應該知道,stringWithFormat: 返回的是一個 Autorelease 對象,所以每次循環生成的 str 都不會立即釋放,而是放入最近的 AutoreleasePool,當前 AutoreleasePool 需要等待當前線程 RunLoop 來釋放,當前線程 RunLoop 又一直在處理 for 循環等事件一直處于活躍狀態并不會釋放 AutoreleasePool,這樣就會導致 AutoreleasePool 里面的對象越來越多,內存占用成直線上升:

內存使用情況

那怎么樣才能避免這種情況出現呢?
及時釋放內存。我們可以換個初始化方法,將 stringWithFormat: 換成了 alloc + initWithFormat: ,這樣對象就能立即釋放。另一個比較好的方案是加入局部 AutoreleasePool ,這樣也能避免在復雜情況下,去一個一個方法去確認是否返回 Autorelease 對象,手動加入的 AutoreleasePool,在作用域過后就立即清空池內所有對象。

for (int i = 0; i < 100000000; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
        str = [str stringByAppendingString:@" - world"];
    }
}
內存使用情況

alloc new copy mutableCopy 等方法會生成并持有對象,而其他類似 [NSMutableArray array] 的方法會生成對象但不持有,返回的是 Autorelease 對象。

AutoreleasePool 原理

前面我們知道了 AutoreleasePool 的實踐應用,那它究竟是怎樣工作的呢,首先我們出它的結構說起,通過使用 clang -rewrite-objc 命令將下面的 Objective-C 代碼重寫成 C++ 代碼我們可以得知:

@autoreleasepool {
    //...
}

實際上相當于

void * atautoreleasepoolobj = objc_autoreleasePoolPush();        
//...
objc_autoreleasePoolPop(atautoreleasepoolobj);
void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

所以 AutoreleasePool 實際上是以 AutoreleasePoolPage 的形式在工作。我們來看看 AutoreleasePoolPage 在 NSObject.mm 中的定義:

class AutoreleasePoolPage {
    magic_t const magic; // 完整性校驗
    id *next; // 指向下一個內存為空的地址
    pthread_t const thread; // 當前線程
    AutoreleasePoolPage * const parent; // 構造雙向鏈表的指針
    AutoreleasePoolPage *child; // 構造雙向鏈表的指針
    uint32_t const depth;
    uint32_t hiwat;
};

AutoreleasePool 并沒有單獨的結構,是由若干個 AutoreleasePoolPage 以雙向鏈表的形式組合而成的。

AutoreleasePool

AutoreleasePoolPage 每個實例對象都會開辟 4096 bytes 內存(一頁虛擬內存的大小),除開底部用于存儲 AutoreleasePoolPage 的成員變量的空間,其余都用來儲存加入到自動釋放池的對象。

加入 AutoreleasePool

當你對一個對象 obj 發送 autorelease 消息時,如果當前線程不存在 AutoreleasePool,則會先生成 AutoreleasePool 對象:void *atautoreleasepoolobj = objc_autoreleasePoolPush(); ,每當執行一次 objc_autoreleasePoolPush, runtime 就向當前的 AutoreleasePoolPage 中 add 進一個哨兵對象(POOL_SENTINEL), atautoreleasepoolobj 即為返回的哨兵對象(POOL_SENTINEL)。接著 obj 就會被放入 next 指針所指的地址,然后 next 指向下一個內存為空的地址,如果當前 AutoreleasePoolPage 被占滿,就會生成新的 AutoreleasePoolPage 來存放 obj,并連接鏈表。

釋放 AutoreleasePool

從最新加入的對象一直向前清理,給每個對象發送 release 消息,直到哨兵對象(POOL_SENTINEL)位置,并回移 next 指針到正確的位置。

AutoreleasePool 對象何時釋放

我們從給對象 obj 發送 autorelease 消息開始說起:

-(id) autorelease
{
    return _objc_rootAutorelease(self);
}

_objc_rootAutorelease(id obj)
{
    assert(obj);
    return obj->rootAutorelease();
}

可以看到這個方法里只是簡單的調了一下 _objc_rootAutorelease() ,繼續跟進:

objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}
static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}

可以看到:[obj autorelease] 實際上是調用了 autoreleaseFast(obj) ,在 autoreleaseFast() 里面會判斷當前是否存在 AutoreleasePoolPage ,如果不存在則調用 autoreleaseNoPage(obj) ,我們接著看這個方法:

id *autoreleaseNoPage(id obj)
{
    // "No page" could mean no pool has been pushed
    // or an empty placeholder pool has been pushed and has no contents yet
    assert(!hotPage());

    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) {
        // We are pushing a second pool over the empty placeholder pool
        // or pushing the first object into the empty placeholder pool.
        // Before doing that, push a pool boundary on behalf of the pool 
        // that is currently represented by the empty placeholder.
        pushExtraBoundary = true;
    }
    else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        // We are pushing an object with no pool in place, 
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug", 
                     pthread_self(), (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        // We are pushing a pool with no pool in place,
        // and alloc-per-pool debugging was not requested.
        // Install and return the empty pool placeholder.
        return setEmptyPoolPlaceholder();
    }

    // We are pushing an object or a non-placeholder'd pool.

    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    // Push a boundary on behalf of the previously-placeholder'd pool.
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }

    // Push the requested object or pool.
    return page->add(obj);
}

可以發現,經過一系列條件篩選,當不存在 AutoreleasePoolPage 時會生成新的 AutoreleasePoolPage 對象: AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);,這就表明:
當我們對一個對象發送 autorelease 消息時,都會被加入到最近的 AutoreleasePool ,不存在則先創建

  • 對于手動添加的 @autoreleasepool { } ,里面的對象會在 } 之后接收到 release 消息
  • 對于系統自動創建的 autoreleasepool, 里面的對象與當前線程的 RunLoop 有關
    • 當前線程的 RunLoop 處于未開啟狀態時,autoreleasepool 會在線程銷毀時一并清空
    • 當前線程的 RunLoop 處于開啟狀態時(主線程的 RunLoop 會自動開啟,其他需要手動),RunLoop 會在合適的時機管理(push,pop)autoreleasepool

對于主線程,系統會幫我們自動開啟 RunLoop ,并注冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler(),第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其 order 是 -2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 釋放舊的池并創建新池;Exit(即將退出Loop)時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之后。在主線程執行的代碼,通常是寫在諸如事件回調、Timer 回調內的。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了。

Swift 中的 AutoreleasePool

實踐上,Swift 中的 AutoreleasePool 是橋接于 Objective-C,我們在 Swift 中使用 autoreleasepool { } 其實就是 Objective-C 中的那個。在過去這段時間,Swift 對 ARC 做過很多優化,好像并沒有了 AutoreleasePool 存在的必要性。然而真的不需要了嗎?我們看下述代碼:

guard let file = Bundle.main.path(forResource: "bigImage", ofType: "png") else {
    return
}
for i in 0..<1000000 {
    let url = URL(fileURLWithPath: file)
    let imageData = try! Data(contentsOf: url)
}

還會引起內存的問題嗎?答案是:會。因為 Data(contentsOf: url) 實際上是橋接于 [NSData dataWithContentsOfURL] ,不幸的,還是會返回 autorelease 對象,同樣適用 autoreleasepool { } 能解決這個問題:

autoreleasepool {
    let url = URL(fileURLWithPath: file)
    let imageData = try! Data(contentsOf: url)
}

所以,AutoreleasePool 在 Swift 開發中仍然有用,因為在 UIKit 和 Foundation 中仍然存在遺留的 Objective-C 類 autorelease,但是由于 ARC 的優化,你在處理 Swift 類時可能不需要擔心它。

總結

  • Autorelease 是通過推遲對對象發送 release 消息來延長對象生命周期的
  • 每一個接收到 autorelease 消息的對象都會被加入最近的 AutoreleasePool(如果沒有就創建),然后會在當前線程銷毀時或者當前 Runloop 切換狀態(準備進入休眠和即將退出)時釋放,所以無需擔心 autorelease 對象的內存問題
  • AutoreleasePool 并沒有單獨的結構,是由若干個 AutoreleasePoolPage 以雙向鏈表的形式組合而成的
  • AutoreleasePool 是通過哨兵對象(POOL_SENTINEL)來完成清空的,嵌套的 AutoreleasePool 相當于多個哨兵對象(POOL_SENTINEL)

參考

深入理解RunLoop
自動釋放池的前世今生
黑幕背后的Autorelease
帶著問題看源碼----子線程AutoRelease對象何時釋放
各個線程 Autorelease 對象的內存管理
does NSThread create autoreleasepool automatically now?
@autoreleasepool uses in 2019 Swift

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容