iOS - 聊聊 autorelease 和 @autoreleasepool

前言

作為 iOS 開(kāi)發(fā)者蚜锨,在面試過(guò)程中經(jīng)常會(huì)碰到這樣一個(gè)問(wèn)題:在 ARC 環(huán)境下autorelease對(duì)象在什么時(shí)候釋放?如果你還不知道怎么回答沙绝,或者你只有比較模糊的概念搏明,那么你絕對(duì)不能錯(cuò)過(guò)本文。

本文將通過(guò)Runtime objc4-756.2版本源碼闪檬、macOS 與 iOS 工程示例來(lái)分析@autoreleasepool的底層原理星著。并在最后針對(duì)有關(guān)autorelease@autoreleasepool的一些問(wèn)題進(jìn)行解答。

網(wǎng)絡(luò)配圖.png

目錄

  • 1. 簡(jiǎn)單聊聊 ARC 與 MRC
  • 2. 自動(dòng)釋放池
  • 3. 原理分析
  • 4. 查看自動(dòng)釋放池的情況
  • 5. 使用 macOS 工程示例分析
  • 6. 使用 iOS 工程示例分析
    ?系統(tǒng)干預(yù)釋放
    ?RunLoop 與 @autoreleasepool
    ?手動(dòng)干預(yù)釋放
  • 相關(guān)問(wèn)題
    Q:ARC 環(huán)境下粗悯,autorelease 對(duì)象在什么時(shí)候釋放虚循?
    Q:ARC 環(huán)境下,需不需要手動(dòng)添加 @autoreleasepool样傍?

1. 簡(jiǎn)單聊聊 ARC 與 MRC

蘋(píng)果在 iOS 5 中引入了ARC(Automatic Reference Counting)自動(dòng)引用計(jì)數(shù)內(nèi)存管理技術(shù)横缔,通過(guò)LLVM編譯器和Runtime協(xié)作來(lái)進(jìn)行自動(dòng)管理內(nèi)存。LLVM編譯器會(huì)在編譯時(shí)在合適的地方為 OC 對(duì)象插入retain衫哥、releaseautorelease代碼茎刚,省去了在MRC(Manual Reference Counting)手動(dòng)引用計(jì)數(shù)下手動(dòng)插入這些代碼的工作,減輕了開(kāi)發(fā)者的工作量炕檩。

MRC下斗蒋,當(dāng)我們不需要一個(gè)對(duì)象的時(shí)候,要調(diào)用releaseautorelease方法來(lái)釋放它笛质。調(diào)用release會(huì)立即讓對(duì)象的引用計(jì)數(shù)減 1 泉沾,如果此時(shí)對(duì)象的引用計(jì)數(shù)為 0,對(duì)象就會(huì)被銷(xiāo)毀妇押。調(diào)用autorelease會(huì)將該對(duì)象添加進(jìn)自動(dòng)釋放池中跷究,它會(huì)在一個(gè)恰當(dāng)?shù)臅r(shí)刻自動(dòng)給對(duì)象調(diào)用release,所以autorelease相當(dāng)于延遲了對(duì)象的釋放敲霍。

ARC下俊马,autorelease方法已被禁用,我們可以使用__autoreleasing修飾符修飾對(duì)象將對(duì)象注冊(cè)到自動(dòng)釋放池中肩杈。詳情請(qǐng)參閱《iOS - 老生常談內(nèi)存管理(三):ARC 面世 —— 所有權(quán)修飾符》柴我。

2. 自動(dòng)釋放池

簡(jiǎn)介

官方文檔

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 “l(fā)ocal” autorelease pools to help to minimize the peak memory footprint.

以上是蘋(píng)果對(duì)自動(dòng)釋放池的一段介紹,其意思為:AppKit 和 UIKit 框架在事件循環(huán)(RunLoop)的每次循環(huán)開(kāi)始時(shí)扩然,在主線程創(chuàng)建一個(gè)自動(dòng)釋放池艘儒,并在每次循環(huán)結(jié)束時(shí)銷(xiāo)毀它,在銷(xiāo)毀時(shí)釋放自動(dòng)釋放池中的所有autorelease對(duì)象。通常情況下我們不需要手動(dòng)創(chuàng)建自動(dòng)釋放池界睁,但是如果我們?cè)谘h(huán)中創(chuàng)建了很多臨時(shí)的autorelease對(duì)象觉增,則手動(dòng)創(chuàng)建自動(dòng)釋放池來(lái)管理這些對(duì)象可以很大程度地減少內(nèi)存峰值。

事件循環(huán)圖

創(chuàng)建一個(gè)自動(dòng)釋放池

  • MRC下翻斟,可以使用NSAutoreleasePool或者@autoreleasepool逾礁。建議使用@autoreleasepool,蘋(píng)果說(shuō)它比NSAutoreleasePool快大約六倍访惜。
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // Code benefitting from a local autorelease pool.
    [pool release]; // [pool drain]

Q: 釋放NSAutoreleasePool對(duì)象嘹履,使用[pool release][pool drain]的區(qū)別?

Objective-C 語(yǔ)言本身是支持 GC 機(jī)制的疾牲,但有平臺(tái)局限性植捎,僅限于 MacOS 開(kāi)發(fā)中衙解,iOS 開(kāi)發(fā)用的是 RC 機(jī)制阳柔。在 iOS 的 RC 環(huán)境下[pool release][pool drain]效果一樣,但在 GC 環(huán)境下drain會(huì)觸發(fā) GC 而release不做任何操作蚓峦。使用[pool drain]更佳舌剂,一是它的功能對(duì)系統(tǒng)兼容性更強(qiáng),二是這樣可以跟普通對(duì)象的release區(qū)別開(kāi)暑椰。(注意:蘋(píng)果在引入ARC時(shí)稱(chēng)霍转,已在 OS X Mountain Lion v10.8 中棄用GC機(jī)制,而使用ARC替代)

  • 而在ARC下一汽,已經(jīng)禁止使用NSAutoreleasePool類(lèi)創(chuàng)建自動(dòng)釋放池避消,只能使用@autoreleasepool
    @autoreleasepool {
        // Code benefitting from a local autorelease pool.
    }

3. 原理分析

下面我們先通過(guò)macOS工程來(lái)分析@autoreleasepool的底層原理召夹。
macOS工程中的main()函數(shù)什么都沒(méi)做岩喷,只是放了一個(gè)@autoreleasepool

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

__AtAutoreleasePool

通過(guò) Clang clang -rewrite-objc main.m 將以上代碼轉(zhuǎn)換為 C++ 代碼监憎。

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底層是創(chuàng)建了一個(gè)__AtAutoreleasePool結(jié)構(gòu)體對(duì)象纱意;
  • 在創(chuàng)建__AtAutoreleasePool結(jié)構(gòu)體時(shí)會(huì)在構(gòu)造函數(shù)中調(diào)用objc_autoreleasePoolPush()函數(shù),并返回一個(gè)atautoreleasepoolobj(POOL_BOUNDARY存放的內(nèi)存地址鲸阔,下面會(huì)講到)偷霉;
  • 在釋放__AtAutoreleasePool結(jié)構(gòu)體時(shí)會(huì)在析構(gòu)函數(shù)中調(diào)用objc_autoreleasePoolPop()函數(shù),并將atautoreleasepoolobj傳入褐筛。

AutoreleasePoolPage

下面我們進(jìn)入Runtime objc4源碼查看以上提到的兩個(gè)函數(shù)的實(shí)現(xiàn)类少。

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

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

可以得知,objc_autoreleasePoolPush()objc_autoreleasePoolPop()兩個(gè)函數(shù)其實(shí)是調(diào)用了AutoreleasePoolPage類(lèi)的兩個(gè)類(lèi)方法push()pop()渔扎。所以@autoreleasepool底層就是使用AutoreleasePoolPage類(lèi)來(lái)實(shí)現(xiàn)的硫狞。

下面我們來(lái)看一下AutoreleasePoolPage類(lèi)的定義:

// objc4-756.2
// NSObject.mm
class AutoreleasePoolPage 
{
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)  // EMPTY_POOL_PLACEHOLDER:表示一個(gè)空自動(dòng)釋放池的占位符
#   define POOL_BOUNDARY nil                // POOL_BOUNDARY:哨兵對(duì)象
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;   // 用來(lái)標(biāo)記已釋放的對(duì)象
    static size_t const SIZE =              // 每個(gè) Page 對(duì)象占用 4096 個(gè)字節(jié)內(nèi)存
#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 的個(gè)數(shù)

    magic_t const magic;                // 用來(lái)校驗(yàn) Page 的結(jié)構(gòu)是否完整
    id *next;                           // 指向下一個(gè)可存放 autorelease 對(duì)象地址的位置,初始化指向 begin()
    pthread_t const thread;             // 指向當(dāng)前線程
    AutoreleasePoolPage * const parent; // 指向父結(jié)點(diǎn),首結(jié)點(diǎn)的 parent 為 nil
    AutoreleasePoolPage *child;         // 指向子結(jié)點(diǎn)妓忍,尾結(jié)點(diǎn)的 child  為 nil
    uint32_t const depth;               // Page 的深度虏两,從 0 開(kāi)始遞增
    uint32_t hiwat;
    ......
}

備注: 本文使用的是objc4-756.2版本源碼進(jìn)行分析。在objc4-779.1版本中世剖,AutoreleasePoolPage繼承自AutoreleasePoolPageData定罢,如下。不過(guò)原理不變旁瘫,不影響分析祖凫。

// objc4-779.1
// NSObject.mm
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
    ......
}

// NSObject-internal.h
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)
    {
    }
};

整個(gè)程序運(yùn)行過(guò)程中,可能會(huì)有多個(gè)AutoreleasePoolPage對(duì)象酬凳。從它的定義可以得知:

  • 自動(dòng)釋放池(即所有的AutoreleasePoolPage對(duì)象)是以為結(jié)點(diǎn)通過(guò)雙向鏈表的形式組合而成惠况;
  • 自動(dòng)釋放池與線程一一對(duì)應(yīng);
  • 每個(gè)AutoreleasePoolPage對(duì)象占用4096字節(jié)內(nèi)存宁仔,其中56個(gè)字節(jié)用來(lái)存放它內(nèi)部的成員變量稠屠,剩下的空間(4040個(gè)字節(jié))用來(lái)存放autorelease對(duì)象的地址。

其內(nèi)存分布圖如下:

AutoreleasePoolPage 雙向鏈表結(jié)構(gòu)

下面我們通過(guò)源碼來(lái)分析push()翎苫、pop()以及autorelease方法的實(shí)現(xiàn)权埠。

POOL_BOUNDARY

在分析這些方法之前,先介紹一下POOL_BOUNDARY煎谍。

  • POOL_BOUNDARY的前世叫做POOL_SENTINEL攘蔽,稱(chēng)為哨兵對(duì)象或者邊界對(duì)象;
  • POOL_BOUNDARY用來(lái)區(qū)分不同的自動(dòng)釋放池呐粘,以解決自動(dòng)釋放池嵌套的問(wèn)題满俗;
  • 每當(dāng)創(chuàng)建一個(gè)自動(dòng)釋放池,就會(huì)調(diào)用push()方法將一個(gè)POOL_BOUNDARY入棧作岖,并返回其存放的內(nèi)存地址唆垃;
  • 當(dāng)往自動(dòng)釋放池中添加autorelease對(duì)象時(shí),將autorelease對(duì)象的內(nèi)存地址入棧鳍咱,它們前面至少有一個(gè)POOL_BOUNDARY降盹;
  • 當(dāng)銷(xiāo)毀一個(gè)自動(dòng)釋放池時(shí),會(huì)調(diào)用pop()方法并傳入一個(gè)POOL_BOUNDARY谤辜,會(huì)從自動(dòng)釋放池中最后一個(gè)對(duì)象開(kāi)始蓄坏,依次給它們發(fā)送release消息,直到遇到這個(gè)POOL_BOUNDARY丑念。

push

    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) { // 出錯(cuò)時(shí)進(jìn)入調(diào)試狀態(tài)
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);  // 傳入 POOL_BOUNDARY 哨兵對(duì)象
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

當(dāng)創(chuàng)建一個(gè)自動(dòng)釋放池時(shí)涡戳,會(huì)調(diào)用push()方法。push()方法中調(diào)用了autoreleaseFast()方法并傳入了POOL_BOUNDARY哨兵對(duì)象脯倚。

下面我們來(lái)看一下autoreleaseFast()方法的實(shí)現(xiàn):

    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();     // 雙向鏈表中的最后一個(gè) Page
        if (page && !page->full()) {        // 如果當(dāng)前 Page 存在且未滿
            return page->add(obj);                 // 將 autorelease 對(duì)象入棧渔彰,即添加到當(dāng)前 Page 中嵌屎;
        } else if (page) {                  // 如果當(dāng)前 Page 存在但已滿
            return autoreleaseFullPage(obj, page); // 創(chuàng)建一個(gè)新的 Page,并將 autorelease 對(duì)象添加進(jìn)去
        } else {                            // 如果當(dāng)前 Page 不存在恍涂,即還沒(méi)創(chuàng)建過(guò) Page
            return autoreleaseNoPage(obj);         // 創(chuàng)建第一個(gè) Page宝惰,并將 autorelease 對(duì)象添加進(jìn)去
        }
    }

autoreleaseFast()中先是調(diào)用了hotPage()方法獲得未滿的Page,從AutoreleasePoolPage類(lèi)的定義可知再沧,每個(gè)Page的內(nèi)存大小為4096個(gè)字節(jié)尼夺,每當(dāng)Page滿了的時(shí)候,就會(huì)創(chuàng)建一個(gè)新的Page炒瘸。hotPage()方法就是用來(lái)獲得這個(gè)新創(chuàng)建的未滿的Page淤堵。
autoreleaseFast()在執(zhí)行過(guò)程中有三種情況:

  • ① 當(dāng)前Page存在且未滿時(shí),通過(guò)page->add(obj)autorelease對(duì)象入棧顷扩,即添加到當(dāng)前Page中拐邪;
  • ② 當(dāng)前Page存在但已滿時(shí),通過(guò)autoreleaseFullPage(obj, page)創(chuàng)建一個(gè)新的Page隘截,并將autorelease對(duì)象添加進(jìn)去扎阶;
  • ③ 當(dāng)前Page不存在,即還沒(méi)創(chuàng)建過(guò)Page技俐,通過(guò)autoreleaseNoPage(obj)創(chuàng)建第一個(gè)Page乘陪,并將autorelease對(duì)象添加進(jìn)去。

下面我們來(lái)看一下以上提到的三個(gè)方法的實(shí)現(xiàn):

    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)其實(shí)就是將autorelease對(duì)象添加到Page中的next指針?biāo)赶虻奈恢玫窭蓿?code>next指針指向這個(gè)對(duì)象的下一個(gè)位置,然后將該對(duì)象的位置返回贱勃。

    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()方法中通過(guò)while循環(huán)井赌,通過(guò)Pagechild指針找到最后一個(gè)Page

  • 如果最后一個(gè)Page未滿贵扰,就通過(guò)page->add(obj)autorelease對(duì)象添加到最后一個(gè)Page中仇穗;
  • 如果最后一個(gè)Page已滿,就創(chuàng)建一個(gè)新的Page并將該Page設(shè)置為hotPage戚绕,通過(guò)page->add(obj)autorelease對(duì)象添加進(jìn)去纹坐。
    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()方法中會(huì)創(chuàng)建第一個(gè)Page。該方法會(huì)判斷是否有空的自動(dòng)釋放池存在舞丛,如果沒(méi)有會(huì)通過(guò)setEmptyPoolPlaceholder()生成一個(gè)占位符耘子,表示一個(gè)空的自動(dòng)釋放池。接著創(chuàng)建第一個(gè)Page球切,設(shè)置它為hotPage谷誓。最后將一個(gè)POOL_BOUNDARY添加進(jìn)Page中,并返回POOL_BOUNDARY的下一個(gè)位置吨凑。

小結(jié):以上就是push操作的實(shí)現(xiàn)捍歪,往自動(dòng)釋放池中添加一個(gè)POOL_BOUNDARY户辱,并返回它存放的內(nèi)存地址。接著每有一個(gè)對(duì)象調(diào)用autorelease方法糙臼,會(huì)將它的內(nèi)存地址添加進(jìn)自動(dòng)釋放池中庐镐。

autorelease

autorelease方法的函數(shù)調(diào)用棧如下,詳細(xì)請(qǐng)參閱《iOS - 老生常談內(nèi)存管理(四):內(nèi)存管理方法源碼分析》变逃。

// NSObject.mm
① objc_autorelease
// objc-object.h 
② objc_object::autorelease
// NSObject.mm
③ autorelease
④ _objc_rootAutorelease
// objc-object.h
⑤ objc_object::rootAutorelease
// NSObject.mm
⑥ objc_object::rootAutorelease2
⑦ AutoreleasePoolPage::autorelease

AutoreleasePoolPage類(lèi)的autorelease方法實(shí)現(xiàn)如下:

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

可以看到焚鹊,調(diào)用了autorelease方法的對(duì)象,也是通過(guò)以上解析的autoreleaseFast()方法添加進(jìn)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對(duì)應(yīng)在Page中的地址末患。當(dāng)銷(xiāo)毀自動(dòng)釋放池時(shí),會(huì)調(diào)用pop()方法將自動(dòng)釋放池中的autorelease對(duì)象全部釋放(實(shí)際上是從自動(dòng)釋放池的中的最后一個(gè)入棧的autorelease對(duì)象開(kāi)始锤窑,依次給它們發(fā)送一條release消息璧针,直到遇到這個(gè)POOL_BOUNDARY)。pop()方法的執(zhí)行過(guò)程如下:

  • ① 判斷token是不是EMPTY_POOL_PLACEHOLDER渊啰,是的話就清空這個(gè)自動(dòng)釋放池探橱;
  • ② 如果不是的話,就通過(guò)pageForPointer(token)拿到token所在的Page(自動(dòng)釋放池的首個(gè)Page)绘证;
  • ③ 通過(guò)page->releaseUntil(stop)將自動(dòng)釋放池中的autorelease對(duì)象全部釋放隧膏,傳參stop即為POOL_BOUNDARY的地址;
  • ④ 判斷當(dāng)前Page是否有子Page嚷那,有的話就銷(xiāo)毀胞枕。

pop()方法中釋放autorelease對(duì)象的過(guò)程在releaseUntil()方法中,下面來(lái)看一下這個(gè)方法的實(shí)現(xiàn):

    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指針是指向最后一個(gè)對(duì)象的后一個(gè)位置魏宽,所以需要先減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()方法其實(shí)就是通過(guò)一個(gè)while循環(huán)腐泻,從最后一個(gè)入棧的autorelease對(duì)象開(kāi)始,依次給它們發(fā)送一條release消息队询,直到遇到這個(gè)POOL_BOUNDARY派桩。

AutoreleasePoolPage()

我們來(lái)看一下創(chuàng)建一個(gè)Page的過(guò)程。AutoreleasePoolPage()方法的參數(shù)為parentPage蚌斩,新創(chuàng)建的Pagedepth加 1铆惑,next指針的初始位置指向begin,將新創(chuàng)建的Pageparent指針指向parentPage送膳。將parentPagechild指針指向自己员魏,這就形成了雙向鏈表的結(jié)構(gòu)。

    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

下面再來(lái)看一下begin晒奕、end闻书、empty名斟、full這些方法的實(shí)現(xiàn)。

  • begin的地址為:Page自己的地址+Page對(duì)象的大小56個(gè)字節(jié)魄眉;
  • end的地址為:Page自己的地址+4096個(gè)字節(jié)砰盐;
  • 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();
    }

小結(jié):

  • push操作是往自動(dòng)釋放池中添加一個(gè)POOL_BOUNDARY岩梳,并返回它存放的內(nèi)存地址;
  • 接著每有一個(gè)對(duì)象調(diào)用autorelease方法晃择,會(huì)將它的內(nèi)存地址添加進(jìn)自動(dòng)釋放池中冀值。
  • pop操作是傳入一個(gè)POOL_BOUNDARY的內(nèi)存地址,從最后一個(gè)入棧的autorelease對(duì)象開(kāi)始宫屠,將自動(dòng)釋放池中的autorelease對(duì)象全部釋放(實(shí)際上是給它們發(fā)送一條release消息)列疗,直到遇到這個(gè)POOL_BOUNDARY

4. 查看自動(dòng)釋放池的情況

可以通過(guò)以下私有函數(shù)來(lái)查看自動(dòng)釋放池的情況:

extern void _objc_autoreleasePoolPrint(void);

5. 使用 macOS 工程示例分析

由于iOS工程中浪蹂,系統(tǒng)在自動(dòng)釋放池中注冊(cè)了一些對(duì)象抵栈。為了排除這些干擾,接下來(lái)我們通過(guò)macOS工程代碼示例坤次,結(jié)合AutoreleasePoolPage的內(nèi)存分布圖以及_objc_autoreleasePoolPrint()私有函數(shù)古劲,來(lái)幫助我們更好地理解@autoreleasepool的原理。

注意:

  • 由于ARC環(huán)境下不能調(diào)用autorelease等方法缰猴,所以需要將工程切換為MRC環(huán)境产艾。
  • 如果使用ARC,則可以使用__autoreleasing所有權(quán)修飾符替代autorelease方法洛波。

單個(gè) @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;
}
內(nèi)存分布圖
// 自動(dòng)釋放池的情況
objc[68122]: ############## (print1)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 0 releases pending. //當(dāng)前自動(dòng)釋放池中沒(méi)有任何對(duì)象
objc[68122]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68122]: ##############

objc[68122]: ############## (print2)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 1 releases pending. //當(dāng)前自動(dòng)釋放池中有1個(gè)對(duì)象胰舆,這個(gè)對(duì)象為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. //當(dāng)前自動(dòng)釋放池中有3個(gè)對(duì)象
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. //當(dāng)前自動(dòng)釋放池中沒(méi)有任何對(duì)象,因?yàn)锧autoreleasepool作用域結(jié)束蹬挤,調(diào)用pop方法釋放了對(duì)象
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;
}
內(nèi)存分布圖
// 自動(dòng)釋放池的情況
objc[68285]: ############## (print1)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 0 releases pending. //當(dāng)前自動(dòng)釋放池中沒(méi)有任何對(duì)象
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: ##############

objc[68285]: ############## (print2)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 1 releases pending. //當(dāng)前自動(dòng)釋放池中有1個(gè)對(duì)象
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. //當(dāng)前自動(dòng)釋放池中有3個(gè)對(duì)象(1個(gè)@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. //當(dāng)前自動(dòng)釋放池中有5個(gè)對(duì)象(2個(gè)@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. //當(dāng)前自動(dòng)釋放池中有7個(gè)對(duì)象(3個(gè)@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. //當(dāng)前自動(dòng)釋放池中有5個(gè)對(duì)象(第3個(gè)@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. //當(dāng)前自動(dòng)釋放池中有3個(gè)對(duì)象(第2、3個(gè)@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. //當(dāng)前自動(dòng)釋放池沒(méi)有任何對(duì)象(3個(gè)@autoreleasepool都已釋放)
objc[68285]: [0x102802000]  ................  PAGE  (hot) (cold)
objc[68285]: ##############

復(fù)雜情況 @autoreleasepool

AutoreleasePoolPage類(lèi)的定義可知棘幸,自動(dòng)釋放池(即所有的AutoreleasePoolPage對(duì)象)是以為結(jié)點(diǎn)通過(guò)雙向鏈表的形式組合而成焰扳。每當(dāng)Page滿了的時(shí)候,就會(huì)創(chuàng)建一個(gè)新的Page误续,并設(shè)置它為hotPage吨悍,而首個(gè)PagecoldPage。接下來(lái)我們來(lái)看一下多個(gè)Page和多個(gè)@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;
}

一個(gè)AutoreleasePoolPage對(duì)象的內(nèi)存大小為4096個(gè)字節(jié)育瓜,它自身成員變量占用內(nèi)存56個(gè)字節(jié),所以剩下的4040個(gè)字節(jié)用來(lái)存儲(chǔ)autorelease對(duì)象的內(nèi)存地址栽烂。又因?yàn)?code>64bit 系統(tǒng)下一個(gè)OC對(duì)象的指針?biāo)純?nèi)存為8個(gè)字節(jié)躏仇,所以一個(gè)Page可以存放505個(gè)對(duì)象的地址恋脚。POOL_BOUNDARY也是一個(gè)對(duì)象,因?yàn)樗闹禐?code>nil焰手。所以以上代碼的自動(dòng)釋放池內(nèi)存分布圖如下所示糟描。

內(nèi)存分布圖
objc[69731]: ##############
objc[69731]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[69731]: 1303 releases pending. //當(dāng)前自動(dòng)釋放池中有1303個(gè)對(duì)象(3個(gè)POOL_BOUNDARY和1300個(gè)HTPerson實(shí)例)
objc[69731]: [0x100806000]  ................  PAGE (full)  (cold) /* 第一個(gè)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)         /* 第二個(gè)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)         /* 第三個(gè)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]: ##############

6. 使用 iOS 工程示例分析

從以上macOS工程示例可以得知,在@autoreleasepool大括號(hào)結(jié)束的時(shí)候躲履,就會(huì)調(diào)用Pagepop()方法见间,給@autoreleasepool中的autorelease對(duì)象發(fā)送release消息。

那么在iOS工程中工猜,方法里的autorelease對(duì)象是什么時(shí)候釋放的呢米诉?有系統(tǒng)干預(yù)釋放和手動(dòng)干預(yù)釋放兩種情況。

  • 系統(tǒng)干預(yù)釋放是不指定@autoreleasepool域慷,所有autorelease對(duì)象都由主線程的RunLoop創(chuàng)建的@autoreleasepool來(lái)管理荒辕。
  • 手動(dòng)干預(yù)釋放就是將autorelease對(duì)象添加進(jìn)我們手動(dòng)創(chuàng)建的@autoreleasepool中。

下面還是在MRC環(huán)境下進(jìn)行分析犹褒。

系統(tǒng)干預(yù)釋放

我們先來(lái)看以下 Xcode 11 版本的iOS程序中的main()函數(shù)抵窒,和舊版本的差異。

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

注意:

網(wǎng)上對(duì)于iOS工程的main()函數(shù)中的@autoreleasepool有一種解釋?zhuān)?/strong>
iOS工程的main()函數(shù)中有一個(gè)@autoreleasepool,這個(gè)@autoreleasepool負(fù)責(zé)了應(yīng)用程序所有autorelease對(duì)象的釋放敛摘。

其實(shí)這個(gè)解釋是錯(cuò)誤的温学。
如果你的程序使用了AppKitUIKit框架,那么主線程的RunLoop就會(huì)在每次事件循環(huán)迭代中創(chuàng)建并處理@autoreleasepool掉房。也就是說(shuō),應(yīng)用程序所有autorelease對(duì)象的都是由RunLoop創(chuàng)建的@autoreleasepool來(lái)管理慰丛。而main()函數(shù)中的@autoreleasepool只是負(fù)責(zé)管理它的作用域中的autorelease對(duì)象卓囚。
在以上《使用 MacOS 工程示例分析》章節(jié)中提到了嵌套@autoreleasepool的情況。Xcode 舊版本的main函數(shù)中是將整個(gè)應(yīng)用程序運(yùn)行(UIApplicationMain)放在@autoreleasepool內(nèi)诅病,而主線程的RunLoop就是在UIApplicationMain中創(chuàng)建哪亿,所以RunLoop創(chuàng)建的@autoreleasepool是嵌套在main函數(shù)的@autoreleasepool內(nèi)的。RunLoop會(huì)在每次事件循環(huán)中對(duì)自動(dòng)釋放池進(jìn)行poppush(以下會(huì)詳細(xì)講解)贤笆,但是它的pop只會(huì)釋放掉它的POOL_BOUNDARY之后的對(duì)象蝇棉,它并不會(huì)影響到外層即main函數(shù)中@autoreleasepool

新版本 Xcode 11 中的 main 函數(shù)發(fā)生了哪些變化芥永?
舊版本是將整個(gè)應(yīng)用程序運(yùn)行放在@autoreleasepool內(nèi)篡殷,由于RunLoop的存在,要return即程序結(jié)束后@autoreleasepool作用域才會(huì)結(jié)束埋涧,這意味著程序結(jié)束后main函數(shù)中的@autoreleasepool中的autorelease對(duì)象才會(huì)釋放板辽。
而在 Xcode 11中奇瘦,觸發(fā)主線程RunLoopUIApplicationMain函數(shù)放在了@autoreleasepool外面,這可以保證@autoreleasepool中的autorelease對(duì)象在程序啟動(dòng)后立即釋放戳气。正如新版本的@autoreleasepool中的注釋所寫(xiě) “Setup code that might create autoreleased objects goes here.”(如上代碼)链患,可以將autorelease對(duì)象放在此處。

接著我們來(lái)看 “系統(tǒng)干預(yù)釋放” 情況的示例:

- (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:]

可以看到瓶您,調(diào)用了autorelease方法的person對(duì)象不是在viewDidLoad方法結(jié)束后釋放麻捻,而是在viewWillAppear方法結(jié)束后釋放,說(shuō)明在viewWillAppear方法結(jié)束的時(shí)候呀袱,調(diào)用了pop()方法釋放了person對(duì)象贸毕。其實(shí)這是由RunLoop控制的,下面來(lái)講解一下RunLoop@autoreleasepool的關(guān)系夜赵。

RunLoop 與 @autoreleasepool

學(xué)習(xí)這個(gè)知識(shí)點(diǎn)之前明棍,需要先搞懂RunLoop的事件循環(huán)機(jī)制以及它的6種活動(dòng)狀態(tài),可以參閱:
《深入淺出 RunLoop(二):數(shù)據(jù)結(jié)構(gòu)》
《深入淺出 RunLoop(三):事件循環(huán)機(jī)制》

iOS在主線程的RunLoop中注冊(cè)了兩個(gè)Observer寇僧。

  • 第 1 個(gè)Observer監(jiān)聽(tīng)了kCFRunLoopEntry事件摊腋,會(huì)調(diào)用objc_autoreleasePoolPush()
  • 第 2 個(gè)Observer
    ① 監(jiān)聽(tīng)了kCFRunLoopBeforeWaiting事件嘁傀,會(huì)調(diào)用objc_autoreleasePoolPop()兴蒸、objc_autoreleasePoolPush()
    ② 監(jiān)聽(tīng)了kCFRunLoopBeforeExit事件细办,會(huì)調(diào)用objc_autoreleasePoolPop()橙凳。
image.png

所以,在iOS工程中系統(tǒng)干預(yù)釋放的autorelease對(duì)象的釋放時(shí)機(jī)是由RunLoop控制的笑撞,會(huì)在當(dāng)前RunLoop每次循環(huán)結(jié)束時(shí)釋放岛啸。以上person對(duì)象在viewWillAppear方法結(jié)束后釋放,說(shuō)明viewDidLoadviewWillAppear方法在同一次循環(huán)里茴肥。

  • kCFRunLoopEntry:在即將進(jìn)入RunLoop時(shí)坚踩,會(huì)自動(dòng)創(chuàng)建一個(gè)__AtAutoreleasePool結(jié)構(gòu)體對(duì)象,并調(diào)用objc_autoreleasePoolPush()函數(shù)瓤狐。
  • kCFRunLoopBeforeWaiting:在RunLoop即將休眠時(shí)堕虹,會(huì)自動(dòng)銷(xiāo)毀一個(gè)__AtAutoreleasePool對(duì)象,調(diào)用objc_autoreleasePoolPop()芬首。然后創(chuàng)建一個(gè)新的__AtAutoreleasePool對(duì)象,并調(diào)用objc_autoreleasePoolPush()逼裆。
  • kCFRunLoopBeforeExit郁稍,在即將退出RunLoop時(shí),會(huì)自動(dòng)銷(xiāo)毀最后一個(gè)創(chuàng)建的__AtAutoreleasePool對(duì)象胜宇,并調(diào)用objc_autoreleasePoolPop()耀怜。

手動(dòng)干預(yù)釋放

我們?cè)賮?lái)看一下手動(dòng)干預(yù)釋放的情況恢着。

- (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:]

可以看到,添加進(jìn)手動(dòng)指定的@autoreleasepool中的autorelease對(duì)象财破,在@autoreleasepool大括號(hào)結(jié)束時(shí)就會(huì)釋放掰派,不受RunLoop控制。

相關(guān)問(wèn)題

Q:ARC 環(huán)境下左痢,autorelease 對(duì)象在什么時(shí)候釋放靡羡?

回到我們最初的面試題,在ARC環(huán)境下俊性,autorelease對(duì)象在什么時(shí)候釋放略步?我們就分系統(tǒng)干預(yù)釋放手動(dòng)干預(yù)釋放兩種情況回答。

Q:ARC 環(huán)境下定页,需不需要手動(dòng)添加 @autoreleasepool趟薄?

AppKit 和 UIKit 框架會(huì)在RunLoop每次事件循環(huán)迭代中創(chuàng)建并處理@autoreleasepool,因此典徊,你通常不必自己創(chuàng)建@autoreleasepool杭煎,甚至不需要知道創(chuàng)建@autoreleasepool的代碼怎么寫(xiě)。但是卒落,有些情況需要自己創(chuàng)建@autoreleasepool羡铲。

例如,如果我們需要在循環(huán)中創(chuàng)建了很多臨時(shí)的autorelease對(duì)象导绷,則手動(dòng)添加@autoreleasepool來(lái)管理這些對(duì)象可以很大程度地減少內(nèi)存峰值犀勒。比如在for循環(huán)中alloc圖片數(shù)據(jù)等內(nèi)存消耗較大的場(chǎng)景,需要手動(dòng)添加@autoreleasepool妥曲。

蘋(píng)果給出了三種需要手動(dòng)添加@autoreleasepool的情況:

  • ① 如果你編寫(xiě)的程序不是基于 UI 框架的贾费,比如說(shuō)命令行工具;
  • ② 如果你編寫(xiě)的循環(huán)中創(chuàng)建了大量的臨時(shí)對(duì)象檐盟;
    你可以在循環(huán)內(nèi)使用@autoreleasepool在下一次迭代之前處理這些對(duì)象褂萧。在循環(huán)中使用@autoreleasepool有助于減少應(yīng)用程序的最大內(nèi)存占用。
  • ③ 如果你創(chuàng)建了輔助線程葵萎。
    一旦線程開(kāi)始執(zhí)行导犹,就必須創(chuàng)建自己的@autoreleasepool;否則羡忘,你的應(yīng)用程序?qū)⒋嬖趦?nèi)存泄漏谎痢。

版本更新

版本日期 更新內(nèi)容
2020.3.17 發(fā)布文章
2020.4.17 1.【Feature】對(duì)釋放 NSAutoreleasePool 對(duì)象,使用 [pool release] 與 [pool drain] 的區(qū)別卷雕?問(wèn)題的解釋?zhuān)?br>2.【Update】錯(cuò)誤的解釋在 iOS 中 main 函數(shù)中的 @autoreleasepool 負(fù)責(zé)整個(gè)應(yīng)用程序 autorelease 對(duì)象的釋放节猿;
3.【Feature】對(duì)新版本 Xcode 11 中的 main 函數(shù)發(fā)生了哪些變化?問(wèn)題的解釋?zhuān)?br>4.【Feature】源碼分析,Runtime最新源代碼objc4-779.1中對(duì)AutoreleasePoolPage類(lèi)的更新:AutoreleasePoolPage現(xiàn)在繼承自AutoreleasePoolPageData滨嘱。

我的博客即將同步至騰訊云+社區(qū)峰鄙,邀請(qǐng)大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1g6tbyhm63kr3

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市太雨,隨后出現(xiàn)的幾起案子吟榴,更是在濱河造成了極大的恐慌,老刑警劉巖囊扳,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吩翻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡宪拥,警方通過(guò)查閱死者的電腦和手機(jī)仿野,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)她君,“玉大人脚作,你說(shuō)我怎么就攤上這事〉奚玻” “怎么了球涛?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)校镐。 經(jīng)常有香客問(wèn)我亿扁,道長(zhǎng),這世上最難降的妖魔是什么鸟廓? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任从祝,我火速辦了婚禮,結(jié)果婚禮上引谜,老公的妹妹穿的比我還像新娘牍陌。我一直安慰自己,他們只是感情好员咽,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布毒涧。 她就那樣靜靜地躺著,像睡著了一般贝室。 火紅的嫁衣襯著肌膚如雪契讲。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天滑频,我揣著相機(jī)與錄音捡偏,去河邊找鬼。 笑死峡迷,一個(gè)胖子當(dāng)著我的面吹牛霹琼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼枣申,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了看杭?” 一聲冷哼從身側(cè)響起忠藤,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎楼雹,沒(méi)想到半個(gè)月后模孩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贮缅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年榨咐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谴供。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡块茁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出桂肌,到底是詐尸還是另有隱情数焊,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布崎场,位于F島的核電站佩耳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏谭跨。R本人自食惡果不足惜干厚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望螃宙。 院中可真熱鬧蛮瞄,春花似錦、人聲如沸污呼。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)燕酷。三九已至籍凝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間苗缩,已是汗流浹背饵蒂。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留酱讶,地道東北人退盯。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親渊迁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子慰照,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 【原創(chuàng)博文,轉(zhuǎn)載請(qǐng)注明出處琉朽!】 寫(xiě)在最前面:你們別光看啊毒租,發(fā)現(xiàn)我說(shuō)的不對(duì)的地方請(qǐng)指出或留言,希望我們一起進(jìn)步箱叁。 i...
    RephontilZhou閱讀 1,259評(píng)論 1 12
  • 原文鏈接 AutoreleasePool對(duì)于iOS開(kāi)發(fā)者來(lái)說(shuō)墅垮,可以說(shuō)是"熟悉的陌生人"。熟悉是因?yàn)槊總€(gè)iOS程序都...
    acBool閱讀 1,166評(píng)論 0 19
  • 內(nèi)存管理一直是學(xué)習(xí) Objective-C 的重點(diǎn)和難點(diǎn)之一耕漱,在實(shí)際的軟件開(kāi)發(fā)工作中算色,經(jīng)常會(huì)遇見(jiàn)由于內(nèi)存原因而導(dǎo)致...
    高思陽(yáng)閱讀 3,822評(píng)論 1 4
  • 好記性不如爛筆頭,勿在浮沙筑高臺(tái)螟够,不積跬步無(wú)以至千里灾梦,做人做事要有安排有計(jì)劃。 內(nèi)存管理一直是學(xué)習(xí) Objecti...
    BigLuckyHaha閱讀 7,052評(píng)論 9 19
  • AutoreleasePool(自動(dòng)釋放池)是OC中的一種內(nèi)存自動(dòng)回收機(jī)制齐鲤,它可以延遲加入AutoreleaseP...
    coder_my閱讀 512評(píng)論 0 0