Autoreleasepool自動釋放池-源碼

Autoreleasepool相關的內容是在面試中比較容易被問到的。以前呢,談到Autoreleasepool只能粗淺的瞭解到自動釋放池與內存的管理有關,具體是怎麼樣來管理和釋放對象,並無深刻的學習,本文是筆者在深刻學習Autoreleasepool以後的總結和心得,但願對你們有幫助。git

main函數

首先咱們從main函數開始,main函數是咱們應用的入口。github

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

能夠看出,咱們整個iOS應用都是包含在一個自動釋放池中。
並且在如今的ARC環境中,自動釋放池的用法就是這樣子 @autoreleasepool {}面試

@autoreleasepool

這個@autoreleasepool{}你們都會用,咱們的代碼直接寫在這個大括號內便可。咱們代碼中的對象是怎樣加到自動釋放池中的,最後又是怎麼樣被釋放的呢 ?less

咱們要先知道這個@autoreleasepool究竟是什麼。函數

從網上的一些博客中能夠學到的,在命令行使用clang -rewrite-objc main.m 讓編譯器從新改寫main函數所在的這個文件。固然了這一步我並無操做,直接「盜用」了你們的結果。oop

objc-autorelease-main-cpp

從上圖中能夠看出,@autoreleasepool被轉換爲了一個__AtAutoreleasePool結構體。而後經過在main.cpp中查找找到這個結構體的定義。學習

objc-autorelease-main-cpp-struct

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

這個結構體在初始化是調用objc_autoreleasePoolPush(),在析構時調用objc_autoreleasePoolPop()ui

通過整理,能夠把main函數中實際的代碼應該是相似這樣的。this

int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        // do whatever you want
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

下面就是正式的經過源碼來學習Autoreleasepool了spa

Autoreleasepool源碼

上面咱們提到了mian函數中的@autoreleasepool其實最終轉成了objc_autoreleasePoolPush()objc_autoreleasePoolPop()這兩個方法的調用,咱們去源碼中搜一下這兩個函數。

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

有源碼能夠看出,這兩個函數就是對AutoreleasePoolPage類的pish和pop方法的封裝,因此咱們來着重看AutoreleasePoolPage類。

AutoreleasePoolPage

簡化一下代碼,先看類的部分屬性

class AutoreleasePoolPage {

    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

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
};

熟悉鏈表的朋友,看到這個parent和child就差很少能猜出來了,每個自動釋放池實際上是一個雙向鏈表,鏈表的每個結點就是這個AutoreleasePoolPage,每一個AutoreleasePoolPage的大小爲4096字節

#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES
自動釋放池中的棧(轉載)

若是咱們的一個AutoreleasePoolPage 被初始化在內存的 0x100816000 ~ 0x100817000 中,它在內存中的結構以下:

objc-autorelease-page-in-memory

其中有 56 bit 用於存儲 AutoreleasePoolPage 的成員變量,剩下的 0x100816038 ~ 0x100817000 都是用來存儲加入到自動釋放池中的對象。

begin()end() 這兩個實例方法幫助咱們快速獲取 0x100816038 ~ 0x100817000 這一範圍的邊界地址。

next 指向了下一個爲空的內存地址,若是next指向的地址加入一個 object,它就會以下圖所示移動到下一個爲空的內存地址中:

objc-autorelease-after-insert-to-page

從圖片中咱們能夠看到在AutoreleasePoolPage的棧中出現了一個POOL_SENTINEL,咱們稱之爲哨兵對象。

#define POOL_SENTINEL nil

其實哨兵對象只是nil的別名,他有啥做用呢 ?

每一個自動釋放池初始化在調用objc_autoreleasePoolPush的時候,都會把一個POOL_SENTINELpush到自動釋放池的棧頂,而且返回這個POOL_SENTINEL的地址。

int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        // do whatever you want
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

上面這個atautoreleasepoolobj就是一個POOL_SENTINEL
能夠看到在調用objc_autoreleasePoolPop時,會傳進去這個地址:

  1. 根據傳入的哨兵對象地址找到哨兵對象所處的page
  2. 在當前page中,將晚於哨兵對象插入的全部autorelease對象都發送一次- release消息,並向回移動next指針到正確位置
  3. 補充2:從最新加入的對象一直向前清理,能夠向前跨越若干個page,直到哨兵所在的page

由於自動釋放池是一個雙向鏈表,並且每個page的空間有限,因此會存在當前page已滿的狀況,也就出現了一個自動釋放池跨越幾個page的狀況,因此在release的時候,也要順着鏈表所有清理掉。

objc_autoreleasePoolPush
static inline void *push() 
    {
        id *dest;
        if (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;
    }

經查閱,DebugPoolAllocation是來區別調試模式的,咱們主要看autoreleaseFast這個函數。

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);
        }
    }

hotPage( )咱們能夠理解爲獲取當前的AutoreleasePoolPage,獲取到當前page以後又根據page是否已滿來區別處理。

  • 有 hotPage 而且當前 page 不滿

    • 調用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中
  • 有 hotPage 而且當前 page 已滿

    • 調用 autoreleaseFullPage 初始化一個新的頁
    • 調用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中
  • 無 hotPage

    • 調用 autoreleaseNoPage 建立一個 hotPage
    • 調用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中
static __attribute__((noinline))
    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 {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }

上面代碼中的函數是在page已滿的時候調用,從源碼中能夠看出經過傳入的page遍歷鏈表,直到找到一個未滿的page,若是遍歷到最後一個結點也沒有未滿的,就新建一個new AutoreleasePoolPage(page);。而且要把找到的知足條件的這個page設置爲hotPage。

objc_autoreleasePoolPop

咱們看一下pop的源碼,裏面內容不少,咱們精簡了一下。

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

    page->releaseUntil(stop);
    
    if (page->child) {
    // 不清楚爲何要用下面這個if分類
        if (page->lessThanHalfFull()) {
            page->child->kill();
        } else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

經過token調用pageForPointer()方法獲取到當前的AutoreleasePoolPage,而後調用releaseUntil()釋放page中的對象,直到stop,child節點調用kill()方法。

void kill() {
   AutoreleasePoolPage *page = this;
   //經過循環先找到最後一個節點
   while (page->child) page = page->child;

   AutoreleasePoolPage *deathptr;
   //經過do-while循環,依次從後往前置爲nil
   do {
       deathptr = page;
       page = page->parent;
       if (page) {
           page->unprotect();
           page->child = nil;
           page->protect();
       }
       delete deathptr;
   } while (deathptr != this);
}

pageForPointer()主要是經過內存地址的操做,獲取當前指針所在頁的首地址, releaseUntil()也是經過一個循環來釋放全部的對象,具體的源碼你們能夠本身看一下。

Autorelease對象何時釋放

在沒有手動加AutoreleasePool的狀況下,Autorelease對象都是在當前的runloop迭代結束時釋放的,由於系統在每一個runloop迭代中都加入了自動釋放池Push和Pop。

這個問題又要跟runloop聯繫到一塊兒了,等咱們研究過runloop的源碼,對這個問題應該就有更深入的認識了。

參考文章

自動釋放池的前世此生
黑幕背後的Autorelease

相關文章
相關標籤/搜索