iOS 深入探究 AutoreleasePool

swordjoy發表於2018-11-19

AutoreleasePool 是什麼

AutoreleasePool (下面稱為快取池)是 iOS 開發中的一種記憶體管理的機制,物件呼叫 autorelease 方法後會被放到快取池中延遲釋放,當快取池需要清除時,會向這些 Autoreleased 物件傳送 release 訊息。

新建一個 Xcode 專案,將專案調整成 MRC:

iOS 深入探究 AutoreleasePool
MRC 中,需要使用 retain/release/autorelease 手動管理記憶體,如下程式碼:

int main(int argc, const char * argv[]) {
    NSLog(@"-A-");
    Coder *coder = [[Coder alloc] init];
    [coder release];
    NSLog(@"-B-");
    return 0;
}

// log
-A-
Coder dealloc
-B-
複製程式碼

這裡用 alloc 建立了 coder 物件,讓它的引用計數增加,然後呼叫 release 方法完成釋放。如果使用 autorelease,就需要用到自動快取池了,程式碼如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"-A-");
        Coder *coder = [[[Coder alloc] init] autorelease];
        NSLog(@"-B-");
    }
    NSLog(@"-C-");
    return 0;
}

// log
-A-
-B-
Coder dealloc
-C-
複製程式碼

這裡的 coder 物件在出了自動快取池的作用域後被自動釋放。

不是所有情況都是出了作用域後自動釋放,後面詳解。

@autoreleasepool 幹了什麼

通過 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 命令將 main.m 轉成 C++ 程式碼。

會發現 @autoreleasepool 被轉成一個成員變數:

__AtAutoreleasePool __autoreleasepool; 
複製程式碼

__AtAutoreleasePool 結構體的實現:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};
複製程式碼

這裡有一個 C++ 的語法,__AtAutoreleasePool() 是建構函式,建立結構體時呼叫,~__AtAutoreleasePool() 是解構函式,在結構體銷燬時呼叫,所以上面的程式碼就可以理解為:

int main(int argc, const char * argv[]) {
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    // @autoreleasepool 括號裡面的程式碼
    objc_autoreleasePoolPop(atautoreleasepoolobj);
    return 0;
}
複製程式碼

這也解釋了上面說的為什麼並不是所有情況都是出了 @autoreleasepool 作用域後自動釋放,因為這只是一個語法糖,本質是呼叫了上面的 Push&PoP 方法。

AutoreleasePoolPage

runtime原始碼地址,這裡使用的 objc4-723

在原始碼中查詢上面的 Push&Pop 函式:

void *objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}
複製程式碼

這裡呼叫了 AutoreleasePoolPage 這個類的 Push&Pop 函式,關於 Push&Pop 這裡先打住。先來看看 AutoreleasePoolPage 是怎樣的結構,這裡只有成員變數:

class AutoreleasePoolPage 
{
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
    // ...
}
複製程式碼

這裡的 next 指標指向的是最新被新增進來的 autorelease 物件的下一個位置。 單看這個是不好理解的,所以這裡直接先說 AutoreleasePoolPage

自動釋放池實際上是封裝的 AutoreleasePoolPage 這個 C++ 類,以雙向連結串列的形式構成。每個 AutoreleasePoolPage 物件會開闢 4096 位元組記憶體(也就是虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來以棧的方式儲存 autorelease 物件。AutoreleasePoolPage 空間被佔滿時,會以連結串列的形式新建連結一個 AutoreleasePoolPage 物件,然後將 autorelease 物件的地址存在裡面。如圖所示:

iOS 深入探究 AutoreleasePool

原始碼分析

回到最初的 C++ 程式碼:

void *atautoreleasepoolobj = objc_autoreleasePoolPush();
objc_autoreleasePoolPop(atautoreleasepoolobj);
複製程式碼

呼叫 Push 函式後,會獲得一個返回值,這個返回值作為 Pop 函式的引數被傳入了,下面來看看裡面具體的原理是什麼。直接來看 Push 函式的原始碼:

// 簡化後
static inline void *push() 
{
    id *dest;
    dest = autoreleaseFast(POOL_BOUNDARY);
    return dest;
}
複製程式碼

這裡有個 POOL_BOUNDARY 值得我們注意,不過檢視它的定義會發現它其實是等價 nil 的巨集定義:

#   define POOL_BOUNDARY nil
複製程式碼

也就是說,POOL_BOUNDARY 僅僅只是一個哨兵值。進入 autoreleaseFast(...) 函式:

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // 1.
        return page->add(obj);
    } else if (page) {
        // 2.
        return autoreleaseFullPage(obj, page);
    } else {
        // 3.
        return autoreleaseNoPage(obj);
    }
}
複製程式碼

hotPage 指當前使用的 AutoreleasePoolPage 節點,coldPage 指已經被裝滿的連結串列節點。

這裡的判斷邏輯完全符合前面關於 AutoreleasePoolPage 的說明:

  • 1.當前 page 存在且沒有滿時,直接將物件新增到當前 page 中。
  • 2.當前 page 存在且已滿時,建立一個新的 page ,並將物件新增到新建立的 page 中,然後將這兩個連結串列節點連結。
  • 3.當前 page 不存在時,建立第一個 page ,並將物件新增到新建立的 page 中。

iOS 深入探究 AutoreleasePool
每次 Push 後,都會先新增一個 POOL_BOUNDARY 來佔位,是為了對應一次 Pop 的釋放,例如圖中的 page 就需要兩次 Pop 然後完全的釋放。也就是程式碼中巢狀的情況:

@autoreleasepool {
    @autoreleasepool {

    }
}
複製程式碼

這裡還需要強調的是,這裡使用的是雙連結串列來實現,只有在當前 page 空間使用完後,才會建立新的 page,並不是每個 @autoreleasepool 對應一個 AutoreleasePoolPage 物件。

接下來看 Pop 的原始碼:

// 簡化後
static inline void pop(void *token) 
{   
    AutoreleasePoolPage *page;
    id *stop;
    page = pageForPointer(token);
    stop = (id *)token;
    // 1.根據 token,也就是上文的佔位 POOL_BOUNDARY 釋放 `autoreleased` 物件
    page->releaseUntil(stop);

// hysteresis: keep one empty child if page is more than half full
    // 2.釋放 `Autoreleased` 物件後,銷燬多餘的 page。
    if (page->lessThanHalfFull()) {
        page->child->kill();
    }
    else if (page->child->child) {
        page->child->child->kill();
    }
}
複製程式碼

這裡沒什麼說的,來到 releaseUntil(...) 內部:

// 簡化後
void releaseUntil(id *stop) 
{
    // 1.
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();
        // 2.
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }
        // 3.
        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }
    // 4.
    setHotPage(this);
}
複製程式碼
  • 1.外部迴圈挨個遍歷 autoreleased 物件,直到遍歷到 stop 這個 POOL_BOUNDARY
  • 2.如果當前 hatPage 沒有 POOL_BOUNDARY,將 hatPage 設定為父節點。
  • 3.給當前 autoreleased 物件傳送 release 訊息。
  • 4.再次配置 hatPage

再來看看 autorelease 的實現,這裡直接定位到 page 裡面的 autorelease:

// 簡化後
static inline id autorelease(id obj)
{
    id *dest __unused = autoreleaseFast(obj);
    return obj;
}
複製程式碼

和上面的 push 操作中呼叫的同一函式 autoreleaseFast,沒什麼說的。

這裡從原始碼層面上就瞭解了自動快取池道理是怎麼一回事。

AutoreleasePool 和 runloop

這裡需要 runloop 的知識,可以看我前面的文章 iOS 淺談 Runloop

App 啟動後,蘋果在主執行緒 RunLoop 裡註冊了兩個 Observer,回撥都是 _wrapRunLoopWithAutoreleasePoolHandler ,用來處理自動快取池。

列印主執行緒的 runloop 進行確認。

print(RunLoop.main)
複製程式碼

iOS 深入探究 AutoreleasePool
注意觀察圖中 Observer 觀察的狀態,上面的是 activities = 0x1,下面的是 activities = 0xa0。這裡需要一點 runloop 的知識,下面就是 runloop 可以被監聽的狀態列舉。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),              // 1
    kCFRunLoopBeforeTimers = (1UL << 1),       // 2
    kCFRunLoopBeforeSources = (1UL << 2),      // 4
    kCFRunLoopBeforeWaiting = (1UL << 5),      // 32
    kCFRunLoopAfterWaiting = (1UL << 6),       // 64
    kCFRunLoopExit = (1UL << 7),               // 128
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製程式碼

0x1 (等於1)對應的是 kCFRunLoopEntry ,第一個 Observer 監視的即將進入Loop時,,其回撥內會呼叫 _objc_autoreleasePoolPush() 建立一個自動釋放池。其 order-2147483647,優先順序最高,保證建立快取池發生在其他所有回撥之前。

0xa0(16進位制等於160,等於32+128) 對應的是 kCFRunLoopBeforeWaiting&kCFRunLoopExit,第二個 Observer 監視了兩個事件: 準備進入休眠時呼叫 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 釋放舊的池並建立新池;即將退出Loop時呼叫 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observerorder2147483647,優先順序最低,保證其釋放快取池發生在其他所有回撥之後。

所以對於我們應用來說,autoreleased 物件更多的是在 runloop 的休眠時進行釋放的。

參考

關於 AutoreleasePool 還有一些實際使用中的技巧,例如解決迴圈中 autoreleased 物件的記憶體問題等等。

黑幕背後的Autorelease

Objective-C Autorelease Pool 的實現原理

相關文章