iOS內存管理二:自動釋放池autoreleasepool

在上一篇文章中,詳細分析了IOS內存管理的內存佈局、內存管理方案、引用計數等內容,本篇文章將繼續上篇文章的內容探索自動釋放池autoreleasepool的相關知識。html

一、autoreleasepool初探

熟悉OC開發的都知道,在main函數中就有@autoreleasepool這樣一個東西,其實這就是自動釋放池。那麼@autoreleasepool的底層實現是什麼樣的呢?咱們在命令行中使用 clang -rewrite-objc main.m -o main.cpp 讓編譯器從新改寫這個文件,講獲得一個main.cpp文件,打開該文件,找到其中的main函數。c#

int main(int argc, const char *argv[])
{
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    }
    return 0;
}
複製代碼

咱們能夠看到@autoreleasepool轉化成了__AtAutoreleasePool這樣一個結構體,那麼意味着@autoreleasepool的本質就是__AtAutoreleasePool結構體。bash

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

這個結構體會在初始化時調用objc_autoreleasePoolPush() 方法,會在析構時調用 objc_autoreleasePoolPop 方法。app

這就說明了main函數在實際工做的時候是這樣的:less

int main(int argc, const char *argv[])
{
    void * atautoreleasepoolobj = objc_autoreleasePoolPush();

    // do whatever you want

    objc_autoreleasePoolPop(atautoreleasepoolobj);
    return 0;
}
複製代碼

彷佛一切都是圍繞着objc_autoreleasePoolPush()objc_autoreleasePoolPop這兩個方法展開。那麼咱們來看下這兩個方法的源碼實現:ide

void *
objc_autoreleasePoolPush(void)
{
    // 調用了AutoreleasePoolPage中的push方法
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    // 調用了AutoreleasePoolPage中的pop方法
    AutoreleasePoolPage::pop(ctxt);
}
複製代碼

上面的兩個方法看上去是對AutoreleasePoolPage對應靜態方法pushpop的封裝。函數

二、AutoreleasePoolPage

在runtime中的源碼(objc4-756.2版本)中找到了一段註釋,這段註釋對咱們理解AutoreleasePoolPage的底層結構會有所幫助。oop

  • A thread's autorelease pool is a stack of pointers.
  • Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
  • A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
  • The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
  • Thread-local storage points to the hot page, where newly autoreleased objects are stored.

翻譯中文以下:佈局

  • 一個線程的自動釋放池是一個指針的堆棧結構。
  • 每一個指針表明一個須要釋放的對象或者POOL_BOUNDARY(自動釋放池邊界)
  • 一個 pool token 就是這個 pool 所對應的 POOL_BOUNDARY 的內存地址。當這個 pool 被 pop 的時候,全部內存地址在 pool token 以後的對象都會被 release。
  • 這個堆棧被劃分紅了一個以 page 爲結點的雙向鏈表。pages 會在必要的時候動態地增長或刪除。
  • Thread-local storage(線程局部存儲)指向 hot page ,即最新添加的 autoreleased 對象所在的那個 page 。

從上面這段註釋中咱們能夠知道自動釋放池是一種棧的結構,遵循先進後出的原則,每個自動釋放池都是由一系列的AutoreleasePoolPage組成的,而AutoreleasePoolPage是以雙向鏈表的形式鏈接起來。post

2.一、AutoreleasePoolPage結構

來看一下AutoreleasePoolPage的代碼定義(只列出了關鍵代碼,部分代碼省略)。

class AutoreleasePoolPage 
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    // AutoreleasePoolPage的大小,經過宏定義,能夠看到是4096字節
    static size_t const SIZE =
 #if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
 #else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
 #endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;//16字節
    id *next;//8字節
    pthread_t const thread;//8字節
    AutoreleasePoolPage * const parent;//8字節
    AutoreleasePoolPage *child;//8字節
    uint32_t const depth;//4字節
    uint32_t hiwat;//4字節
}
複製代碼
  • magic:用來校驗AutoreleasePoolPage的結構是否完整。
  • *next:next指向的是下一個AutoreleasePoolPage中下一個爲空的內存地址(新來的對象會存儲到next處),初始化時指向begin()
  • thread:保存了當前頁所在的線程(一個AutoreleasePoolPage屬於一個線程,一個線程中能夠有多個AutoreleasePoolPage)。
  • *parent:指向父節點,第一個parent節點爲nil
  • *child:指向子節點,最後一個child節點爲nil
  • depth:表明深度,從0開始,遞增+1。
  • hiwat:表明high water Mark最大入棧數。
  • SIZEAutoreleasePoolPage的大小,值爲PAGE_MAX_SIZE,4096個字節。
  • POOL_BOUNDARY:只是nil的別名。在每一個自動釋放池初始化調用 objc_autoreleasePoolPush 的時候,都會把一個POOL_SENTINEL push到自動釋放池的棧頂,而且返回這個POOL_SENTINEL自動釋放池邊界。而當方法 objc_autoreleasePoolPop調用時,就會向自動釋放池中的對象發送release消息,直到第一個 POOL_SENTINEL

AutoreleasePoolPage中的第一個對象是存儲在next後面,那麼就造成以下圖所示這樣一個結構。

其中的56個字節存儲的AutoreleasePoolPage的成員變量,其餘的區域存儲加載到自動釋放池的對象。 當next==begin()時表示AutoreleasePoolPage爲空,當next==end()的時表示AutoreleasePoolPage已滿。

2.二、AutoreleasePoolPage容量

在上一個小節的內容中咱們分析了AutoreleasePoolPage的結構,瞭解到每個AutoreleasePoolPage的大小是4096字節,其中56字節用於存儲成員變量,剩下的區域存儲加載到自動釋放池的對象,那麼彷佛答案呼之欲出,一個AutoreleasePoolPage能夠存儲(4096-56)/8=505個對象。可是有一個注意的點,第一個page存放的須要釋放的對象的容量應該是504個,由於在建立page的時候會在next的位置插入1POOL_SENTINEL

2.三、push方法

經過前面小節的分析,咱們知道objc_autoreleasePoolPush的本質就是調用push方法。咱們先來看下push方法的源碼。

static inline void *push() 
{
    id *dest;
    if (slowpath(DebugPoolAllocation)) {
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}
複製代碼

push方法中實際上調用的是autoreleaseFast方法,而且首先將一個POOL_BOUNDARY對象插入到棧頂。slowpath表示小几率發生。

2.3.一、autoreleaseFast方法

以下是autoreleaseFast方法的源碼

static inline id *autoreleaseFast(id obj)
{
    // hotPage就是當前正在使用的AutoreleasePoolPage
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // 有hotPage且hotPage不滿,將對象添加到hotPage中
        return page->add(obj);
    } else if (page) {
        // 有hotPage可是hotPage已滿
        // 使用autoreleaseFullPage初始化一個新頁,並將對象添加到新的AutoreleasePoolPage中
        return autoreleaseFullPage(obj, page);
    } else {
        // 無hotPage
        // 使用autoreleaseNoPage建立一個hotPage,並將對象添加到新建立的page中
        return autoreleaseNoPage(obj);
    }
}
複製代碼

autoreleaseFast方法的代碼很簡單,只要是三個判斷分支。

  1. 若是有hotPage且沒有滿,則調用add方法將對象添加到hotPage中。不然執行第2步。
  2. 若是有hotPage可是已經滿了,則調用autoreleaseFullPage方法初始化一個新頁,並將對象添加到新的AutoreleasePoolPage中。不然執行第3步。
  3. 若是沒有hotPage,則調用autoreleaseNoPage方法建立一個hotPage,並將對象添加到新建立的page

hotPage 能夠理解爲當前正在使用的 AutoreleasePoolPage。

2.3.二、add 添加對象

add方法將對象添加到AutoreleasePoolPage中。

id *add(id obj)
{
    ASSERT(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;//將obj存放在next處,並將next指向下一個位置
    protect();
    return ret;
}
複製代碼

這個方法其實就是一個壓棧操做,將對象添加到AutoreleasePoolPage中,而後移動棧頂指針。

2.3.三、autoreleaseFullPage

autoreleaseFullPage方法會從新開闢一個新的AutoreleasePoolPage頁,並將對象添加到其中。

id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // The hot page is full. 
    // Step to the next non-full page, adding a new page if necessary.
    // Then add the object to that page.
    assert(page == hotPage());
    assert(page->full()  ||  DebugPoolAllocation);

    do {
        // 若是page->child不爲空,那麼使用page->child
        if (page->child) page = page->child;
        // 不然的話,初始化一個新的AutoreleasePoolPage
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    // 將找到的合適的page設置成hotPage
    setHotPage(page);
    // 將對象添加到hotPage中
    return page->add(obj);
}
複製代碼

遍歷找到未滿的的page,若是沒有找到則初始化一個新的page,並將page設置爲hotPage,同時將對象添加到這個page中。

2.3.四、autoreleaseNoPage

若是當前內存中不存在hotPage,就會調用autoreleaseNoPage方法初始化一個AutoreleasePoolPage

id *autoreleaseNoPage(id obj) {
    // Install the first page.
    // 初始化一個AutoreleasePoolPage
    // 當前內存中不存在AutoreleasePoolPage,則從頭開始構建AutoreleasePoolPage,也就是其parent爲nil
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    // 將初始化的AutoreleasePoolPage設置成hotPage
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool.
    // 添加一個邊界對象(nil)
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    
    // Push the requested object or pool.
    // 將對象添加到AutoreleasePoolPage中
    return page->add(obj);
}
複製代碼

當前內存中不存在AutoreleasePoolPage,則從頭開始構建AutoreleasePoolPage,也就是其parentnil。初始化以後,將當前頁標記爲hotPage,而後會先向這個page中添加一個POOL_SENTINEL 對象,來確保在pop調用的時候,不會出現異常。最後,將對象添加到自動釋放池中。

2.四、pop方法

上面小節咱們探索了objc_autoreleasePoolPush,下面咱們看看objc_autoreleasePoolPopobjc_autoreleasePoolPop的本質就是調用pop方法。

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;

    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        // Popping the top-level placeholder pool.
        if (hotPage()) {
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            pop(coldPage()->begin());
        } else {
            // Pool was never used. Clear the placeholder.
            setHotPage(nil);
        }
        return;
    }

    page = pageForPointer(token);
    stop = (id *)token;
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
            // Start of coldest page may correctly not be POOL_BOUNDARY:
            // 1. top-level pool is popped, leaving the cold page in place
            // 2. an object is autoreleased with no pool
        } else {
            // Error. For bincompat purposes this is not 
            // fatal in executables built with old SDKs.
            return badPop(token);
        }
    }

    if (PrintPoolHiwat) printHiwat();

    page->releaseUntil(stop);

    // memory: delete empty children
    if (DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top) 
        // when debugging missing autorelease pools
        page->kill();
        setHotPage(nil);
    } 
    else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}
複製代碼

上面方法作了以下幾件事:

  1. 調用pageForPointer獲取當前token所在的page。
  2. 調用releaseUntil方法釋放棧中的對象,直到stop
  3. 調用childkill方法。

2.4.一、pageForPointer找到page

pageForPointer方法主要是經過經過內存地址的操做,獲取當前token所在頁的首地址。

static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE;

    ASSERT(offset >= sizeof(AutoreleasePoolPage));

    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();

    return result;
}
複製代碼

將指針與頁面的大小(4096)取模,能夠獲得當前指針的偏移量。而後將指針的地址減偏移量即可以獲得首地址。

2.4.二、releaseUntil釋放對象

void releaseUntil(id *stop) {
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    
    // 釋放AutoreleasePoolPage中的對象,直到next指向stop
    while (this->next != stop) {
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
        // hotPage能夠理解爲當前正在使用的page
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it
        // 若是page爲空的話,將page指向上一個page
        // 註釋寫到猜想這裏可使用if,我感受也可使用if
        // 由於根據AutoreleasePoolPage的結構,理論上不可能存在連續兩個page都爲空
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        // obj = page->next; page->next--;
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        // POOL_BOUNDARY爲nil,是哨兵對象
        if (obj != POOL_BOUNDARY) {
            // 釋放obj對象
            objc_release(obj);
        }
    }

    // 從新設置hotPage
    setHotPage(this);

#if DEBUG
    // we expect any children to be completely empty
    for (AutoreleasePoolPage *page = child; page; page = page->child) {
        assert(page->empty());
    }
#endif
}
複製代碼

由於AutoreleasePool實際上就是由AutoreleasePoolPage組成的雙向鏈表,所以,*stop可能不是在最新的AutoreleasePoolPage中,即hotPage,這時須要從hotPage開始,一直釋放,直到stop,中間所通過的全部AutoreleasePoolPage裏面的對象都要釋放。
對象的釋放objc_release方法請移步前面的文章iOS內存管理一:Tagged Pointer&引用計數

2.4.三、kill方法

kill方法刪除雙向鏈表中的每個page

void kill() {
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    AutoreleasePoolPage *page = this;
    // 找到鏈表最末尾的page
    while (page->child) page = page->child;

    AutoreleasePoolPage *deathptr;
    // 循環刪除每個page
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    } while (deathptr != this);
}
複製代碼

三、自動釋放池和線程

官方文檔Using Autorelease Pool Blocks中關於自動釋放池和線程的關係有以下一段描述。

Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.
If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.

翻譯成中文以下:

應用程序中的每一個線程都維護本身的自動釋放池塊堆棧。若是您正在編寫一個僅限基礎的程序,或者正在分離一個線程,那麼您須要建立本身的自動釋放池塊。
若是您的應用程序或線程是長生命週期的,而且可能會生成大量的自動釋放對象,那麼您應該使用自動釋放池塊(如在主線程上使用AppKit和UIKit);不然,自動釋放的對象會累積,內存佔用會增長。若是分離的線程不進行Cocoa調用,則不須要使用自動釋放池塊。

從上面這段秒速咱們能夠知道自動釋放池和線程是緊密相關的,每個自動釋放池只對應一個線程。

四、AutoreleasePool和RunLoop

通常不多會將自動釋放池和RunLoop聯繫起來,可是若是打印[NSRunLoop currentRunLoop]結果中會發現和自動釋放池相關的回調。

<CFRunLoopObserver 0x6000024246e0 [0x7fff8062ce20]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
<CFRunLoopObserver 0x600002424640 [0x7fff8062ce20]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
複製代碼

即App啓動後,蘋果會給RunLoop註冊不少個observers,其中有兩個是跟自動釋放池相關的,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()。\

  • 第一個observer監聽的是activities=0x1(kCFRunLoopEntry),也就是在即將進入loop時,其回調會調用_objc_autoreleasePoolPush() 建立自動釋放池;
  • 第二個observer監聽的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit), 即監聽的是準備進入睡眠和即將退出loop兩個事件。在準備進入睡眠以前,由於睡眠可能時間很長,因此爲了避免佔用資源先調用_objc_autoreleasePoolPop()釋放舊的釋放池,並調用_objc_autoreleasePoolPush() 建立新建一個新的,用來裝載被喚醒後要處理的事件對象;在最後即將退出loop時則會 _objc_autoreleasePoolPop()釋放池子。

五、總結

  1. 自動釋放池是由AutoreleasePoolPage以雙向鏈表的方式實現的。
  2. 當對象調用autorelease方法時,會將對象加入AutoreleasePoolPage的棧中。
  3. 調用AutoreleasePoolPage::pop方法會向棧中的對象發送release消息。
  4. 自動釋放池和線程是緊密相關的,每個自動釋放池只對應一個線程

參考資料

相關文章
相關標籤/搜索