iOS底層學習 - 內存管理之Autoreleasepool

有關內存管理的相關優化方案和引用計數的相關原理,咱們已經瞭解,本章來說解在內存管理中的另外一個方案Autoreleasepoolbash

傳送門☞iOS底層學習 - 內存管理以內存管理方案app

傳送門☞iOS底層學習 - 內存管理之weak原理探究框架

初探Autoreleasepool

Autoreleasepool做用

經過以前章節的學習,咱們知道在ARC下,LLVM編譯器會自動幫咱們生產retainreleaseautorelease等代碼,減小了在MRC下的工做量。調用autorelease會將該對象添加進自動釋放池中,它會在一個恰當的時刻自動給對象調用release,因此autorelease至關於延遲了對象的釋放。less

可是在ARC下,autorelease方法已被禁用,咱們可使用__autoreleasing修飾符修飾對象將對象註冊到自動釋放池中。ide

Autoreleasepool建立

  • MRC下,可使用NSAutoreleasePool或者@autoreleasepool。建議使用@autoreleasepool,蘋果說它比NSAutoreleasePool快大約六倍。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release];
複製代碼
  • 而在ARC下,已經禁止使用NSAutoreleasePool類建立自動釋放池,只能使用@autoreleasepool。
@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}

複製代碼

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create 「local」 autorelease pools to help to minimize the peak memory footprint.函數

以上是蘋果對自動釋放池的一段介紹,其意思爲:AppKitUIKit 框架在事件循環(RunLoop)的每次循環開始時,在主線程建立一個自動釋放池,並在每次循環結束時銷燬它,在銷燬時釋放自動釋放池中的全部autorelease對象。一般狀況下咱們不須要手動建立自動釋放池,可是若是咱們在循環中建立了不少臨時的autorelease對象,則手動建立自動釋放池來管理這些對象能夠很大程度地減小內存峯值。工具

Autoreleasepool原理探究

Autoreleasepool底層結構

咱們知道在main函數中,會建立一個@autoreleasepool {}對象,那麼其底層的結構是怎樣的呢?oop

int main(int argc, const char * argv[]) {
    @autoreleasepool {}
    return 0;
}
複製代碼

咱們仍是使用clang -rewrite-objc main.m命令,轉換爲C++代碼查看。經過如下代碼,咱們能夠發現轉換後@autoreleasepool主要作了如下幾點:post

  • @autoreleasepool底層是建立了一個__AtAutoreleasePool結構體對象;
  • 在建立__AtAutoreleasePool結構體時會在構造函數中調用objc_autoreleasePoolPush()函數,並返回一個atautoreleasepoolobj(POOL_BOUNDARY存放的內存地址,下面會講到);
  • 在釋放__AtAutoreleasePool結構體時會在析構函數中調用objc_autoreleasePoolPop()函數,並將atautoreleasepoolobj傳入。
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; }
    return 0;
}
複製代碼

AutoreleasePoolPage底層結構

首先來看AutoreleasePoolPage的相關源碼,其幾個成員變量的含義以下:學習

  • magic:用來校驗AutoreleasePoolPage的結構是否完整。
  • *next:next指向的是下一個AutoreleasePoolPage中下一個爲空的內存地址(新來的對象會存儲到next處),初始化時指向begin()
  • thread:保存了當前頁所在的線程(一個AutoreleasePoolPage屬於一個線程,一個線程中能夠有多個AutoreleasePoolPage)。
  • *parent:指向父節點,第一個parent節點爲nil。
  • *child:指向子節點,最後一個child節點爲nil。
  • depth:表明深度,從0開始,遞增+1。
  • hiwat:表明high water Mark最大入棧數
  • SIZE:AutoreleasePoolPage的大小,值爲PAGE_MAX_SIZE,4096個字節,其中56個字節用來存儲本身的變量,剩下的4040個字節用來存儲要釋放的對象,也就是最多505個對象。
  • POOL_BOUNDARY
    • 只是nil的別名。前世叫作POOL_SENTINEL,稱爲哨兵對象或者邊界對象
    • POOL_BOUNDARY用來區分不一樣的自動釋放池,以解決自動釋放池嵌套的問題
    • 每當建立一個自動釋放池,就會調用push()方法將一個POOL_BOUNDARY入棧,並返回其存放的內存地址;
    • 當往自動釋放池中添加autorelease對象時,將autorelease對象的內存地址入棧,它們前面至少有一個POOL_BOUNDARY
    • 當銷燬一個自動釋放池時,會調用pop()方法並傳入一個POOL_BOUNDARY,會從自動釋放池中最後一個對象開始,依次給它們發送release消息,直到遇到這個POOL_BOUNDARY
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
	magic_t const magic;
	__unsafe_unretained id *next;
	pthread_t const thread;
	AutoreleasePoolPage * const parent;
	AutoreleasePoolPage *child;
	uint32_t const depth;
	uint32_t hiwat;

	AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
		: magic(), next(_next), thread(_thread),
		  parent(_parent), child(nil),
		  depth(_depth), hiwat(_hiwat)
	{
	}
};

----------------------------------------------------------------------------------

class AutoreleasePoolPage : private AutoreleasePoolPageData
{
	friend struct thread_data_t;

public:
	static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
		PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
		PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif
    
private:
	static pthread_key_t const key = AUTORELEASE_POOL_KEY;
	static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
	static size_t const COUNT = SIZE / sizeof(id);

    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)

# define POOL_BOUNDARY nil

    // SIZE-sizeof(*this) bytes of contents follow
    ......
}
複製代碼

/***********************************************************************

Autorelease pool implementation

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底層結構以下:

  1. AutoreleasePoolPage是以爲結點經過雙向鏈表的形式組合而成;遵循先進後出規則,整個自動釋放池由一系列的AutoreleasePoolPage組成的,而AutoreleasePoolPage是以雙向鏈表的形式鏈接起來。
  2. 自動釋放池與線程一一對應;
  3. 每一個AutoreleasePoolPage對象佔用4096字節內存,其中56個字節用來存放它內部的成員變量,剩下的空間(4040個字節)用來存放autorelease對象的地址。要注意的是第一頁只有504個對象,由於在建立page的時候會在next的位置插入1個POOL_SENTINEL
  4. POOL_BOUNDARY爲哨兵對象,入棧時插入,出棧時釋放對象到此傳入的哨兵對象

該圖表示AutoreleasePoolPage的雙向列表結構

該圖表示 AutoreleasePoolPage的雙向列表和棧結構

AutoreleasePoolPage::push()原理

首先咱們看objc_autoreleasePoolPush的源碼,發現其內部就是調用了AutoreleasePoolPagepush()方法。

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
複製代碼

來到AutoreleasePoolPage內部的push()方法,其中slowpath表示不多會走到,是底部的容錯處理,因此最終會走到autoreleaseFast方法中

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;
    }
複製代碼

查看autoreleaseFast源碼,先是調用了hotPage(),hotPage()方法就是用來得到新建立的未滿的Page。其內部主要是判斷邏輯:

  • 若是當前 Page 存在且未滿,走page->add(obj)將 autorelease 對象入棧,即添加到當前 Page 中
  • 若是當前 Page 存在但已滿,走autoreleaseFullPage,建立一個新的 Page,並將 autorelease 對象添加進去
  • 若是當前 Page 不存在,即還沒建立過 Page,建立第一個 Page,並將 autorelease 對象添加進去
static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage(); // 雙向鏈表中的最後一個 Page
        if (page && !page->full()) {// 若是當前 Page 存在且未滿
            return page->add(obj);      // 將 autorelease 對象入棧,即添加到當前 Page 中;
        } else if (page) { // 若是當前 Page 存在但已滿
            return autoreleaseFullPage(obj, page); // 建立一個新的 Page,並將 autorelease 對象添加進去
        } else {// 若是當前 Page 不存在,即還沒建立過 Page
            return autoreleaseNoPage(obj);      // 建立第一個 Page,並將 autorelease 對象添加進去
        }
    }

複製代碼

page->full()

首先咱們來看一下,如何判斷當前page是不是滿狀態的。

  • begin的地址爲:Page本身的地址+Page對象的大小56個字節;
  • end的地址爲:Page本身的地址+4096個字節;
  • empty:判斷Page是否爲空的條件是next地址是否是等於begin;
  • full:判斷Page是否已滿的條件是next地址是否是等於end(棧頂)。

咱們知道next指向的是下一個AutoreleasePoolPage中下一個爲空的內存地址,新對象會存在next,若是此時next指向end則表明當前AutoreleasePoolPage已滿。

id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

    id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    }

    bool empty() {
        return next == begin();
    }

    bool full() { 
        return next == end();
    }
複製代碼

page->add(obj)

當page沒有存滿時,會調用此方法,內部的原理很是簡單,就是一個壓棧的操做,並將next指針指向這個對象的下一個位置,而後將該對象的位置返回。

id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }
複製代碼

autoreleaseFullPage(obj, page)

若是當前 Page 存在但已滿,會調用此方法。其內部實現的主要方法就是一個do..while循環,主要實現了一下的邏輯

  • 因爲page是鏈表結構,因此經過循環查找page->child
  • 一級級判斷是否page->full()
  • 若是到最後一個page都是滿的,那麼就新new一個AutoreleasePoolPage
  • 若是有不滿的,或者新建立的,調用setHotPage(page)將當前頁設置爲活躍
  • 最後將對象經過page->add壓棧
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);
    }
複製代碼

autoreleaseNoPage(obj)

當沒有page時,會走到此方法,其主要邏輯以下:

  • 先會判斷是否有空的自動釋放池存在,若是沒有會經過setEmptyPoolPlaceholder()生成一個佔位符,表示一個空的自動釋放池
  • 建立第一個Page,設置它爲hotPage
  • 將一個POOL_BOUNDARY添加進Page中,並返回POOL_BOUNDARY的下一個位置。
  • 插入第一個對象
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", 
                         objc_thread_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::pop(ctxt)原理

看完對象入棧的實現,咱們再來看一下出棧的實現。

首先pop的入參token即爲POOL_BOUNDARY對應在Page中的地址。當銷燬自動釋放池時,會從從自動釋放池的中的最後一個入棧的autorelease對象開始,依次給它們發送一條release消息,直到遇到這個POOL_BOUNDARY,具體的步驟以下:

  • 判斷token是否是EMPTY_POOL_PLACEHOLDER,是的話就清空這個自動釋放池
  • 若是不是的話,就經過pageForPointer(token)拿到token所在的Page
  • 經過page->releaseUntil(stop)將自動釋放池中的autorelease對象所有釋放,傳參stop即爲POOL_BOUNDARY的地址
  • 判斷當前Page是否有子Page,有的話就銷燬
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();
            }
        }
    }

複製代碼

pageForPointer(token)

該方法,主要是經過token來拿到當前所在的page。主要實現原理是將指針token與頁面的大小(4096)取模,能夠獲得當前指針的偏移量。而後將指針的地址減偏移量即可以獲得首地址。即該page的地址

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

複製代碼

page->releaseUntil(stop)

pop()方法中釋放autorelease對象的過程在releaseUntil()方法中,下面來看一下這個方法的實現:

  • releaseUntil()方法其實就是經過一個while循環
  • hotPage開始,一直釋放,直到stop,即傳入的POOL_BOUNDARY
  • 最後設置釋放完的當前page爲hotPage
void releaseUntil(id *stop) 
    {
        // Not recursive: we donot want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I canot prove it
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;  // next指針是指向最後一個對象的後一個位置,因此須要先減1
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        setHotPage(this);

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

複製代碼

page->kill()

kill方法刪除雙向鏈表中的每個的page,找到當前pagechild 方向尾部 page,而後反向挨着釋放而且把其parent節點的 child 指針置空。

void kill() 
{
    // Not recursive: we donot 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);
}

複製代碼

Autoreleasepool嵌套探究

準備:

  • 因爲ARC環境下不能調用autorelease等方法,因此須要將工程切換爲MRC環境。
  • 使用 extern void _objc_autoreleasePoolPrint(void);方法來打印autoreleasePool的相關信息

單個page嵌套

int main(int argc, const char * argv[]) {
    _objc_autoreleasePoolPrint();             // print1
    @autoreleasepool { //r1 = push()
        _objc_autoreleasePoolPrint();         // print2
        NSObject *p1 = [[[NSObject alloc] init] autorelease];
        NSObject *p2 = [[[NSObject alloc] init] autorelease];
        _objc_autoreleasePoolPrint();         // print3
        @autoreleasepool { //r2 = push()
            NSObject *p3 = [[[NSObject alloc] init] autorelease];
            _objc_autoreleasePoolPrint();     // print4
            @autoreleasepool { //r3 = push()
                NSObject *p4 = [[[NSObject alloc] init] autorelease];
                _objc_autoreleasePoolPrint(); // print5
            } //pop(r3)
            _objc_autoreleasePoolPrint();     // print6
        } //pop(r2)
        _objc_autoreleasePoolPrint();         // print7
    } //pop(r1)
    _objc_autoreleasePoolPrint();             // print8
    return 0;
}
複製代碼

打印結果過以下,經過打印結果,咱們能夠印證上面原理的探索,其主要的進出棧流程以下圖所示,且做用域只在@autoreleasepool {}之間,超過以後就所有調用pop釋放

objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 0 releases pending.
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 0 releases pending.
objc[12943]: [0x1]  ................  PAGE (placeholder)
objc[12943]: [0x1]  ################ POOL (placeholder)
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 3 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: [0x7f924480b038]  ################ POOL 0x7f924480b038
objc[12943]: [0x7f924480b040]    0x600001b34070  NSObject
objc[12943]: [0x7f924480b048]    0x600001b34080  NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 5 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: [0x7f924480b038]  ################ POOL 0x7f924480b038
objc[12943]: [0x7f924480b040]    0x600001b34070  NSObject
objc[12943]: [0x7f924480b048]    0x600001b34080  NSObject
objc[12943]: [0x7f924480b050]  ################ POOL 0x7f924480b050
objc[12943]: [0x7f924480b058]    0x600001b34090  NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 7 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: [0x7f924480b038]  ################ POOL 0x7f924480b038
objc[12943]: [0x7f924480b040]    0x600001b34070  NSObject
objc[12943]: [0x7f924480b048]    0x600001b34080  NSObject
objc[12943]: [0x7f924480b050]  ################ POOL 0x7f924480b050
objc[12943]: [0x7f924480b058]    0x600001b34090  NSObject
objc[12943]: [0x7f924480b060]  ################ POOL 0x7f924480b060
objc[12943]: [0x7f924480b068]    0x600001b2c030  NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 5 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: [0x7f924480b038]  ################ POOL 0x7f924480b038
objc[12943]: [0x7f924480b040]    0x600001b34070  NSObject
objc[12943]: [0x7f924480b048]    0x600001b34080  NSObject
objc[12943]: [0x7f924480b050]  ################ POOL 0x7f924480b050
objc[12943]: [0x7f924480b058]    0x600001b34090  NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 3 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: [0x7f924480b038]  ################ POOL 0x7f924480b038
objc[12943]: [0x7f924480b040]    0x600001b34070  NSObject
objc[12943]: [0x7f924480b048]    0x600001b34080  NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 0 releases pending.
objc[12943]: [0x7f924480b000]  ................  PAGE  (hot) (cold)
objc[12943]: ##############

複製代碼

多個page嵌套

int main(int argc, const char * argv[]) {
    @autoreleasepool { //r1 = push()
        for (int i = 0; i < 600; i++) {
            NSObject *p = [[[NSObject alloc] init] autorelease];
        }
        @autoreleasepool { //r2 = push()
            for (int i = 0; i < 500; i++) {
                NSObject *p = [[[NSObject alloc] init] autorelease];
            }
            @autoreleasepool { //r3 = push()
                for (int i = 0; i < 200; i++) {
                    NSObject *p = [[[NSObject alloc] init] autorelease];
                }
                _objc_autoreleasePoolPrint();
            } //pop(r3)
        } //pop(r2)
    } //pop(r1)
    return 0;
}

複製代碼

能夠看到打印結果以下:根據原理的探究,咱們知道每一個page除了第一頁是504個對象外,其餘最多存儲505個對象,當一個page滿了時候,會建立一個新的page,而且每一個page之間是以爲結點經過雙向鏈表的形式組合而成。其主要流程以下圖所示

objc[69731]: ##############
objc[69731]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[69731]: 1303 releases pending. //當前自動釋放池中有1303個對象(3個POOL_BOUNDARY和1300個NSObject實例)
objc[69731]: [0x100806000]  ................  PAGE (full)  (cold) /* 第一個PAGE,full表明已滿,cold表明coldPage */
objc[69731]: [0x100806038]  ################ POOL 0x100806038 //POOL_BOUNDARY
objc[69731]: [0x100806040]       0x10182a040  NSObject            //p1
objc[69731]: [0x100806048]       .....................            //...
objc[69731]: [0x100806ff8]       0x101824e40  NSObject            //p504
objc[69731]: [0x102806000]  ................  PAGE (full)         /* 第二個PAGE */
objc[69731]: [0x102806038]       0x101824e50  NSObject            //p505
objc[69731]: [0x102806040]       .....................            //...
objc[69731]: [0x102806330]       0x101825440  NSObject            //p600
objc[69731]: [0x102806338]  ################ POOL 0x102806338 //POOL_BOUNDARY
objc[69731]: [0x102806340]       0x101825450  NSObject            //p601
objc[69731]: [0x102806348]       .....................            //...
objc[69731]: [0x1028067e0]       0x101825d90  NSObject            //p1008
objc[69731]: [0x102804000]  ................  PAGE  (hot)         /* 第三個PAGE,hot表明hotPage */
objc[69731]: [0x102804038]       0x101826dd0  NSObject            //p1009
objc[69731]: [0x102804040]       .....................            //...
objc[69731]: [0x102804310]       0x101827380  NSObject            //p1100
objc[69731]: [0x102804318]  ################ POOL 0x102804318 //POOL_BOUNDARY
objc[69731]: [0x102804320]       0x101827390  NSObject            //p1101
objc[69731]: [0x102804328]       .....................            //...
objc[69731]: [0x102804958]       0x10182b160  NSObject            //p1300
objc[69731]: ##############
複製代碼

@autorelease與RunLoop

@autorelease與RunLoop關係

關於RunLoop的相關知識,能夠查看文章☞iOS底層學習 - 深刻RunLoop

其中主要的的RunLoop運行流程以下圖所示

並且經過打印[NSRunLoop currentRunLoop],能夠發現其中有_wrapRunLoopWithAutoreleasePoolHandler()表明的相關AutoreleasePool的回調。

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

那麼,RunLoop和AutoreleasePool的主要關係以下

  • kCFRunLoopEntry:在即將進入RunLoop時,會自動建立一個__AtAutoreleasePool結構體對象,並調用objc_autoreleasePoolPush()函數。
  • kCFRunLoopBeforeWaiting:在RunLoop即將休眠時,會自動銷燬一個__AtAutoreleasePool對象,調用objc_autoreleasePoolPop()。而後建立一個新的__AtAutoreleasePool對象,並調用objc_autoreleasePoolPush()
  • kCFRunLoopBeforeExit,在即將退出RunLoop時,會自動銷燬最後一個建立的__AtAutoreleasePool對象,並調用objc_autoreleasePoolPop()

main函數變化分析

瞭解了他們之間的關係,咱們能夠經過main函數,來分析一下

// Xcode 11
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
--------------------------------------------------------------------------------
// Xcode 舊版本
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
複製代碼

咱們知道@autoreleasepool {}的做用域只在其大括號之間,並且UIApplicationMain主線程會建立主RunLoop,經過上面的探究,咱們知道在建立RunLoop的時候,也會對應的建立AutoreleasePool。其中使用autorelease修飾的對象都會添加到RunLoop建立的自動釋放池中。

因此Xcode 11和以前版本的區別,主要就是Xcode 11將@autoreleasepool {}提早,這能夠保證@autoreleasepool中的autorelease對象在程序啓動後當即釋放。而以前的版本是在主線程RunLoop建立的自動釋放池的外層的,意味着程序結束後main函數中的@autoreleasepool中的autorelease對象纔會釋放。

@autoreleasepool使用規則

在平時的開發中,咱們通常是不須要使用@autoreleasepool{}的,可是如下幾種狀況可使用

  • 若是你編寫的程序不是基於 UI 框架的,好比說命令行工具;
  • 若是你編寫的循環中建立了大量的臨時對象,你能夠在循環內使用@autoreleasepool在下一次迭代以前處理這些對象。在循環中使用@autoreleasepool有助於減小應用程序的最大內存佔用。
  • 若是你建立了輔助線程。一旦線程開始執行,就必須建立本身的@autoreleasepool;不然,你的應用程序將存在內存泄漏。

使用__autorelease修飾的對象,會被系統自動加入RunLoop建立的自動釋放池中,隨RunLoop生命週期釋放。

總結

  • Autoreleasepool目前經過@autoreleasepool{}來建立,能夠再適當的時機對對象調用release,保證了對象的延遲釋放
  • AutoreleasePoolPage底層結構
    • AutoreleasePoolPage是以爲結點經過雙向鏈表的形式組合而成;遵循先進後出規則,整個自動釋放池由一系列的AutoreleasePoolPage組成的,而AutoreleasePoolPage是以雙向鏈表的形式鏈接起來。
    • 自動釋放池與線程一一對應;
    • 調用objc_autoreleasePoolPush()來入棧,調用objc_autoreleasePoolPop()來出棧
    • 使用POOL_BOUNDARY哨兵對象來做爲出入棧的標誌位
      • 只是nil的別名。前世叫作POOL_SENTINEL,稱爲哨兵對象或者邊界對象;
      • POOL_BOUNDARY用來區分不一樣的自動釋放池,以解決自動釋放池嵌套的問題
      • 每當建立一個自動釋放池,就會調用push()方法將一個POOL_BOUNDARY入棧,並返回其存放的內存地址;
      • 當往自動釋放池中添加autorelease對象時,將autorelease對象的內存地址入棧,它們前面至少有一個POOL_BOUNDARY
      • 當銷燬一個自動釋放池時,會調用pop()方法並傳入一個POOL_BOUNDARY,會從自動釋放池中最後一個對象開始,依次給它們發送release消息,直到遇到這個POOL_BOUNDARY
    • 每一個AutoreleasePoolPage對象佔用4096字節內存,其中56個字節用來存放它內部的成員變量,剩下的空間(4040個字節)用來存放autorelease對象的地址。要注意的是第一頁只有504個對象,由於在建立page的時候會在next的位置插入1個POOL_BOUNDARY。
  • push()原理
    1. 調用了hotPage()得到新建立的未滿的Page
    2. 當前 Page 存在且未滿,走page->add(obj),將 autorelease 對象入棧,並將next指針指向這個對象的下一個位置,而後將該對象的位置返回
    3. 當前 Page 存在但已滿,走autoreleaseFullPage,循環查找page->child並判斷是否已滿,都已滿則建立新的AutoreleasePoolPage,並將 autorelease 對象入棧,設置HotPage
    4. 當沒有page時,走autoreleaseNoPage,先會判斷是否有空的自動釋放池存在並生成佔位符,而後建立一個新page並設置HotPage,依次插入POOL_BOUNDARY和autorelease 對象入棧
  • pop()原理
    1. 判斷token是否是EMPTY_POOL_PLACEHOLDER,是的話就清空這個自動釋放池
    2. 若是不是的話,就經過pageForPointer(token)拿到token所在的Page
    3. 經過page->releaseUntil(stop)將自動釋放池中的autorelease對象所有釋放,傳參stop即爲POOL_BOUNDARY的地址
    4. 判斷當前Page是否有子Page,有的話就銷燬
  • @autorelease與RunLoop
    • kCFRunLoopEntry:在即將進入RunLoop時,會自動建立一個__AtAutoreleasePool結構體對象,並調用objc_autoreleasePoolPush()函數。
    • kCFRunLoopBeforeWaiting:在RunLoop即將休眠時,會自動銷燬一個__AtAutoreleasePool對象,調用objc_autoreleasePoolPop()。而後建立一個新的__AtAutoreleasePool對象,並調用objc_autoreleasePoolPush()。
    • kCFRunLoopBeforeExit,在即將退出RunLoop時,會自動銷燬最後一個建立的__AtAutoreleasePool對象,並調用objc_autoreleasePoolPop()。

參考

iOS - 聊聊 autorelease 和 @autoreleasepool

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

iOS 底層拾遺:AutoreleasePool

相關文章
相關標籤/搜索