iOS 深刻探究 AutoreleasePool

AutoreleasePool 是什麼

AutoreleasePool (下面稱爲緩存池)是 iOS 開發中的一種內存管理的機制,對象調用 autorelease 方法後會被放到緩存池中延遲釋放,當緩存池須要清除時,會向這些 Autoreleased 對象發送 release 消息。c++

新建一個 Xcode 項目,將項目調整成 MRC:objective-c

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 對象在出了自動緩存池的做用域後被自動釋放。bash

不是全部狀況都是出了做用域後自動釋放,後面詳解。app

@autoreleasepool 幹了什麼

經過 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 命令將 main.m 轉成 C++ 代碼。less

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

__AtAutoreleasePool __autoreleasepool; 
複製代碼

__AtAutoreleasePool 結構體的實現:函數

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

這裏有一個 C++ 的語法,__AtAutoreleasePool() 是構造函數,建立結構體時調用,~__AtAutoreleasePool() 是析構函數,在結構體銷燬時調用,因此上面的代碼就能夠理解爲:oop

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 對象的地址存在裏面。如圖所示:

源碼分析

回到最初的 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 中。

每次 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)
複製代碼

注意觀察圖中 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 的實現原理

相關文章
相關標籤/搜索