iOS - 聊聊 autorelease 和 @autoreleasepool

前言: 做爲 iOS 開發者,在面試過程當中常常會碰到這樣一個問題:在 ARC 環境下,autorelease 對象在何時釋放?這也是 iOS 內存管理的重要知識點,本文將針對這道面試題,講解 autorelease 和 @autoreleasepool。html

網絡配圖.png

蘋果在 iOS5 中引入了ARC(Automatic Reference Counting)自動引用計數,經過LLVM編譯器和Runtime協做來進行自動管理內存。LLVM編譯器會在編譯時在合適的地方爲 OC 對象插入retainreleaseautorelease代碼,省去了在MRC(Manual Reference Counting)手動引用計數下手動插入這些代碼的工做,減輕了開發者的工做量。面試

MRC下,當咱們不須要一個對象的時候,要調用releaseautorelease方法來釋放它。調用release會當即讓對象的引用計數減1,若是此時對象的引用計數爲0,就會當即釋放該對象的內存空間。調用autorelease會將該對象添加進自動釋放池中,它會在一個恰當的時刻自動給對象調用release,因此autorelease至關於延遲了對象的釋放。網絡

1. 自動釋放池

官方文檔數據結構

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.app

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

事件循環圖

建立一個自動釋放池

  • MRC下使用NSAutoreleasePool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release];
複製代碼
  • ARC下使用@autoreleasepool
@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}
複製代碼

2. 原理分析

下面咱們先經過macOS工程來分析@autoreleasepool的底層原理。 macOS工程中的main()函數什麼都沒作,只是放了一個@autoreleasepoolless

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

__AtAutoreleasePool

經過 Clang clang -rewrite-objc main.m 將以上代碼轉換爲 C++ 代碼。ide

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

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

能夠看到:函數

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

AutoreleasePoolPage

下面咱們進入Runtime objc4源碼查看以上提到的兩個函數的實現。工具

備註: 本文使用的是objc4-756.2源碼進行分析。

// NSObject.mm
void * objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

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

能夠得知,objc_autoreleasePoolPush()objc_autoreleasePoolPop()兩個函數實際上是調用了AutoreleasePoolPage類的兩個類方法push()pop()。因此@autoreleasepool底層就是使用AutoreleasePoolPage類來實現的。

下面咱們來看一下AutoreleasePoolPage類的定義:

class AutoreleasePoolPage 
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1) // EMPTY_POOL_PLACEHOLDER:表示一個空自動釋放池的佔位符
# define POOL_BOUNDARY nil // POOL_BOUNDARY:哨兵對象
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;   // 用來標記已釋放的對象
    static size_t const SIZE =              // 每一個 Page 對象佔用 4096 個字節內存
#if PROTECT_AUTORELEASEPOOL // PAGE_MAX_SIZE = 4096
        PAGE_MAX_SIZE;  // must be muliple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);  // Page 的個數

    magic_t const magic;                // 用來校驗 Page 的結構是否完整
    id *next;                           // 指向下一個可存放 autorelease 對象地址的位置,初始化指向 begin()
    pthread_t const thread;             // 指向當前線程
    AutoreleasePoolPage * const parent; // 指向父結點,首結點的 parent 爲 nil
    AutoreleasePoolPage *child;         // 指向子結點,尾結點的 child 爲 nil
    uint32_t const depth;               // Page 的深度,從 0 開始遞增
    uint32_t hiwat;
    ......
}
複製代碼

整個程序運行過程當中,可能會有多個AutoreleasePoolPage對象。從它的定義能夠得知:

  • 自動釋放池(即全部的AutoreleasePoolPage對象)是以爲結點經過雙向鏈表的形式組合而成;
  • 自動釋放池與線程一一對應;
  • 每一個AutoreleasePoolPage對象佔用4096字節內存,其中56個字節用來存放它內部的成員變量,剩下的空間(4040個字節)用來存放autorelease對象的地址。

其內存分佈圖以下:

AutoreleasePoolPage 雙向鏈表結構

下面咱們經過源碼來分析push()pop()以及autorelease方法的實現。

push

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);  // 傳入 POOL_BOUNDARY 哨兵對象
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
複製代碼

當建立一個自動釋放池時,會調用push()方法。push()方法中調用了autoreleaseFast()方法並傳入了POOL_BOUNDARY哨兵對象。

這裏對POOL_BOUNDARY作一下介紹:

  • POOL_BOUNDARY的前世叫作POOL_SENTINEL,稱爲哨兵對象或者邊界對象;
  • POOL_BOUNDARY用來區分不一樣的自動釋放池,以解決自動釋放池嵌套的問題;
  • 每當建立一個自動釋放池,就會調用push()方法將一個POOL_BOUNDARY入棧,並返回其存放的內存地址;
  • 當往自動釋放池中添加autorelease對象時,將autorelease對象的內存地址入棧,它們前面至少有一個POOL_BOUNDARY
  • 當銷燬一個自動釋放池時,會調用pop()方法並傳入一個POOL_BOUNDARY,會從自動釋放池中最後一個對象開始,依次給它們發送release消息,直到遇到這個POOL_BOUNDARY

下面咱們來看一下autoreleaseFast()方法的實現:

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 對象添加進去
        }
    }
複製代碼

autoreleaseFast()中先是調用了hotPage()方法得到未滿的Page,從AutoreleasePoolPage類的定義可知,每一個Page的內存大小爲4096個字節,每當Page滿了的時候,就會建立一個新的PagehotPage()方法就是用來得到這個新建立的未滿的PageautoreleaseFast()在執行過程當中有三種狀況:

  • ① 當前Page存在且未滿時,經過page->add(obj)autorelease對象入棧,即添加到當前Page中;
  • ② 當前Page存在但已滿時,經過autoreleaseFullPage(obj, page)建立一個新的Page,並將autorelease對象添加進去;
  • ③ 當前Page不存在,即還沒建立過Page,經過autoreleaseNoPage(obj)建立第一個Page,並將autorelease對象添加進去。

下面咱們來看一下以上提到的三個方法的實現:

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

page->add(obj)其實就是將autorelease對象添加到Page中的next指針所指向的位置,並將next指針指向這個對象的下一個位置,而後將該對象的位置返回。

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

autoreleaseFullPage()方法中經過while循環,經過Pagechild指針找到最後一個Page

  • 若是最後一個Page未滿,就經過page->add(obj)autorelease對象添加到最後一個Page中;
  • 若是最後一個Page已滿,就建立一個新的Page並經過page->add(obj)autorelease對象添加進去,並將該Page設置爲hotPage
static __attribute__((noinline))
    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", 
                         pthread_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);
    }
複製代碼

autoreleaseNoPage()方法中會建立第一個Page。該方法會判斷是否有空的自動釋放池存在,若是沒有會經過setEmptyPoolPlaceholder()生成一個佔位符,表示一個空的自動釋放池。接着建立第一個Page,設置它爲hotPage。最後將一個POOL_BOUNDARY添加進Page中,並返回POOL_BOUNDARY的下一個位置。

小結: 以上就是push操做的實現,往自動釋放池中添加一個POOL_BOUNDARY,並返回它存放的內存地址。接着每有一個對象調用autorelease方法,會將它的內存地址添加進自動釋放池中。

autorelease

static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }
複製代碼

能夠看到,調用了autorelease方法的對象,也是經過以上解析的autoreleaseFast()方法添加進Page中。

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

pop()方法的傳參token即爲POOL_BOUNDARY對應在Page中的地址。當銷燬自動釋放池時,會調用pop()方法將自動釋放池中的autorelease對象所有釋放(其實是從自動釋放池的中的最後一個入棧的autorelease對象開始,依次給它們發送一條release消息,直到遇到這個POOL_BOUNDARY)。pop()方法的執行過程以下:

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

pop()方法中釋放autorelease對象的過程在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
        
        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 can't 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
    }
複製代碼

releaseUntil()方法其實就是經過一個while循環,從最後一個入棧的autorelease對象開始,依次給它們發送一條release消息,直到遇到這個POOL_BOUNDARY

AutoreleasePoolPage()

咱們來看一下建立一個Page的過程。AutoreleasePoolPage()方法的參數爲parentPage,新建立的Pagedepth加一,next指針的初始位置指向begin,將新建立的Pageparent指針指向parentPage。將parentPagechild指針指向本身,這就造成了雙向鏈表的結構。

AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
        : magic(), next(begin()), thread(pthread_self()),
          parent(newParent), child(nil), 
          depth(parent ? 1+parent->depth : 0), 
          hiwat(parent ? parent->hiwat : 0)
    { 
        if (parent) {
            parent->check();
            assert(!parent->child);
            parent->unprotect();
            parent->child = this;
            parent->protect();
        }
        protect();
    }
複製代碼

begin、end、empty、full

下面再來看一下beginendemptyfull這些方法的實現。

  • begin的地址爲:Page本身的地址+Page對象的大小56個字節;
  • end的地址爲:Page本身的地址+4096個字節;
  • empty判斷Page是否爲空的條件是next地址是否是等於begin
  • full判斷Page是否已滿的條件是next地址是否是等於end(棧頂)。
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();
    }
複製代碼

小結:

  • push操做是往自動釋放池中添加一個POOL_BOUNDARY,並返回它存放的內存地址;
  • 接着每有一個對象調用autorelease方法,會將它的內存地址添加進自動釋放池中。
  • pop操做是傳入一個POOL_BOUNDARY的內存地址,從最後一個入棧的autorelease對象開始,將自動釋放池中的autorelease對象所有釋放(其實是給它們發送一條release消息),直到遇到這個POOL_BOUNDARY

3. 查看自動釋放池的狀況

能夠經過如下私有函數來查看自動釋放池的狀況:

extern void _objc_autoreleasePoolPrint(void);
複製代碼

4. macOS 工程示例分析

接下來咱們經過macOS工程代碼示例,結合AutoreleasePoolPage的內存分佈圖以及_objc_autoreleasePoolPrint()私有函數,來幫助咱們更好地理解@autoreleasepool的原理。

注意: 因爲ARC環境下不能調用autorelease等方法,因此須要將工程切換爲MRC環境。

單個 @autoreleasepool

int main(int argc, const char * argv[]) {
    _objc_autoreleasePoolPrint();     // print1
    @autoreleasepool {
        _objc_autoreleasePoolPrint(); // print2
        HTPerson *p1 = [[[HTPerson alloc] init] autorelease];
        HTPerson *p2 = [[[HTPerson alloc] init] autorelease];
        _objc_autoreleasePoolPrint(); // print3
    }
    _objc_autoreleasePoolPrint();     // print4
    return 0;
}
複製代碼

內存分佈圖

// 自動釋放池的狀況
objc[68122]: ############## (print1)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 0 releases pending. //當前自動釋放池中沒有任何對象
objc[68122]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68122]: ##############

objc[68122]: ############## (print2)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 1 releases pending. //當前自動釋放池中有1個對象,這個對象爲POOL_BOUNDARY
objc[68122]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68122]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68122]: ##############

objc[68122]: ############## (print3)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 3 releases pending. //當前自動釋放池中有3個對象
objc[68122]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68122]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68122]: [0x102802040]       0x100704a10  HTPerson          //p1
objc[68122]: [0x102802048]       0x10075cc30  HTPerson          //p2
objc[68122]: ##############

objc[68156]: ############## (print4)
objc[68156]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68156]: 0 releases pending. //當前自動釋放池中沒有任何對象,由於@autoreleasepool做用域結束,調用pop方法釋放了對象
objc[68156]: [0x100810000]  ................  PAGE  (hot) (cold)
objc[68156]: ##############
複製代碼

嵌套 @autoreleasepool

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

內存分佈圖

// 自動釋放池的狀況
objc[68285]: ############## (print1)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 0 releases pending. //當前自動釋放池中沒有任何對象
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: ##############

objc[68285]: ############## (print2)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 1 releases pending. //當前自動釋放池中有1個對象
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: ##############

objc[68285]: ############## (print3)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 3 releases pending. //當前自動釋放池中有3個對象(1個@autoreleasepool)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: [0x102802040]       0x100707d80  HTPerson          //p1
objc[68285]: [0x102802048]       0x100707de0  HTPerson          //p2
objc[68285]: ##############

objc[68285]: ############## (print4)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 5 releases pending. //當前自動釋放池中有5個對象(2個@autoreleasepool)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: [0x102802040]       0x100707d80  HTPerson          //p1
objc[68285]: [0x102802048]       0x100707de0  HTPerson          //p2
objc[68285]: [0x102802050]  ################ POOL 0x102802050 //POOL_BOUNDARY
objc[68285]: [0x102802058]       0x1005065b0  HTPerson          //p3
objc[68285]: ##############

objc[68285]: ############## (print5)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 7 releases pending. //當前自動釋放池中有7個對象(3個@autoreleasepool)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: [0x102802040]       0x100707d80  HTPerson          //p1
objc[68285]: [0x102802048]       0x100707de0  HTPerson          //p2
objc[68285]: [0x102802050]  ################ POOL 0x102802050 //POOL_BOUNDARY
objc[68285]: [0x102802058]       0x1005065b0  HTPerson          //p3
objc[68285]: [0x102802060]  ################ POOL 0x102802060 //POOL_BOUNDARY
objc[68285]: [0x102802068]       0x100551880  HTPerson          //p4
objc[68285]: ##############

objc[68285]: ############## (print6)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 5 releases pending. //當前自動釋放池中有5個對象(第3個@autoreleasepool已釋放)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038
objc[68285]: [0x102802040]       0x100707d80  HTPerson
objc[68285]: [0x102802048]       0x100707de0  HTPerson
objc[68285]: [0x102802050]  ################ POOL 0x102802050
objc[68285]: [0x102802058]       0x1005065b0  HTPerson
objc[68285]: ##############

objc[68285]: ############## (print7)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 3 releases pending. //當前自動釋放池中有3個對象(第二、3個@autoreleasepool已釋放)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: [0x102802038]  ################ POOL 0x102802038
objc[68285]: [0x102802040]       0x100707d80  HTPerson
objc[68285]: [0x102802048]       0x100707de0  HTPerson
objc[68285]: ##############

objc[68285]: ############## (print8)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 0 releases pending. //當前自動釋放池沒有任何對象(3個@autoreleasepool都已釋放)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: ##############
複製代碼

複雜狀況 @autoreleasepool

AutoreleasePoolPage類的定義可知,自動釋放池(即全部的AutoreleasePoolPage對象)是以爲結點經過雙向鏈表的形式組合而成。每當Page滿了的時候,就會建立一個新的Page,並設置它爲hotPage,而首個PagecoldPage。接下來咱們來看一下多個Page和多個@autoreleasepool嵌套的狀況。

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

一個AutoreleasePoolPage對象的內存大小爲4096個字節,它自身成員變量佔用內存56個字節,因此剩下的4040個字節用來存儲autorelease對象的內存地址。又由於64bit下一個OC對象的指針所佔內存爲8個字節,因此一個Page能夠存放505個對象的地址。POOL_BOUNDARY也是一個對象,由於它的值爲nil。因此以上代碼的自動釋放池內存分佈圖以下所示。

內存分佈圖

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

5. iOS 工程示例分析

從以上macOS工程示例能夠得知,在@autoreleasepool大括號結束的時候,就會調用Pagepop()方法,給@autoreleasepool中的autorelease對象發送release消息。

那麼在iOS工程中,方法裏的autorelease對象是何時釋放的呢?有系統干預釋放和手動干預釋放兩種狀況。系統干預釋放是不指定@autoreleasepool,全部autorelease對象都由main函數中的@autoreleasepool管理。手動干預釋放就是將autorelease對象添加進咱們手動建立的@autoreleasepool

下面仍是在MRC環境下進行分析。

系統干預釋放

iOS工程的main()函數中也有一個@autoreleasepool,這個@autoreleasepool負責了應用程序全部autorelease對象的釋放。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
複製代碼
- (void)viewDidLoad {
    [super viewDidLoad];    
    HTPerson *person = [[[HTPerson alloc] init] autorelease];    
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];    
    NSLog(@"%s", __func__);
}

// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[HTPerson dealloc]
// -[ViewController viewDidAppear:]
複製代碼

能夠看到,調用了autorelease方法的person對象不是在viewDidLoad方法結束後釋放,而是在viewWillAppear方法結束後釋放,說明在viewWillAppear方法結束的時候,調用了pop()方法釋放了person對象。其實這是由RunLoop控制的,下面來說解一下RunLoop@autoreleasepool的關係。

RunLoop 與 @autoreleasepool

學習這個知識點以前,須要先搞懂RunLoop的事件循環機制以及它的6種活動狀態,能夠查看個人文章:
《深刻淺出 RunLoop(二):數據結構》
《深刻淺出 RunLoop(三):事件循環機制》

iOS在主線程的RunLoop中註冊了兩個Observer

  • 第1個Observer監聽了kCFRunLoopEntry事件,會調用objc_autoreleasePoolPush()
  • 第2個Observer
    ① 監聽了kCFRunLoopBeforeWaiting事件,會調用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
    ② 監聽了kCFRunLoopBeforeExit事件,會調用objc_autoreleasePoolPop()

image.png

因此,在iOS工程中系統干預釋放的autorelease對象的釋放時機是由RunLoop控制的,會在當前RunLoop每次循環結束時釋放。以上person對象在viewWillAppear方法結束後釋放,說明viewDidLoadviewWillAppear方法在同一次循環裏。

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

手動干預釋放

咱們再來看一下手動干預釋放的狀況。

- (void)viewDidLoad {
    [super viewDidLoad];    
    @autoreleasepool {
        HTPerson *person = [[[HTPerson alloc] init] autorelease];  
    }  
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];    
    NSLog(@"%s", __func__);
}

// -[HTPerson dealloc]
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[ViewController viewDidAppear:]
複製代碼

能夠看到,添加進手動指定的@autoreleasepool中的autorelease對象,在@autoreleasepool大括號結束時就會釋放,不受RunLoop控制。

Q:ARC 環境下方法裏的局部對象何時釋放?

以上都是在MRC環境下分析,由於ARC下不能給對象調用retain releaseautorelease等方法。

ARC中方法裏的局部對象何時釋放?其實只要知道LLVM編譯器在編譯時給對象插入release仍是autorelease方法就知道了。

  • 經過alloc/new/copy/mutableCopy方法建立的對象,LLVM編譯器在編譯時會給對象插入release方法,因此這類局部對象,在方法結束時就會釋放。
  • 經過其餘方法(如類方法)建立的對象LLVM編譯器在編譯時會給對象插入autorelease方法,因此這類對象的釋放時機由RunLoop控制。

Q:ARC 環境下,autorelease 對象在何時釋放?

回到咱們最初的面試題,在ARC環境下,autorelease對象在何時釋放?咱們就分系統干預釋放手動干預釋放兩種狀況回答。

Q:ARC 環境下需不須要手動添加 @autoreleasepool?

iOS工程在ARC環境下,main函數中的@autoreleasepool負責了應用程序全部autorelease對象的釋放。一般狀況下咱們不須要手動添加@autoreleasepool,可是若是咱們須要在循環中建立了不少臨時的autorelease對象,則手動添加@autoreleasepool來管理這些對象能夠很大程度地減小內存峯值。好比在for循環中alloc圖片數據等內存消耗較大的場景,須要手動添加@autoreleasepool

蘋果給出了三種須要手動添加@autoreleasepool的狀況:

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

更多關於@autoreleasepool的使用能夠查看蘋果官方文檔《Advanced Memory Management Programming Guide》

相關文章
相關標籤/搜索