autoreleasepool原理分析

@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}

1、原理分析

1.1、__AtAutoreleasePool

下面我們先通過macOS工程來分析@autoreleasepool的底層原理。 macOS工程中的main()函數(shù)什么都沒做,只是放了一個@autoreleasepool。

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

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

struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 構(gòu)造函數(shù),在創(chuàng)建結(jié)構(gòu)體的時候調(diào)用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    ~__AtAutoreleasePool() { // 析構(gòu)函數(shù),在結(jié)構(gòu)體銷毀的時候調(diào)用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    void * atautoreleasepoolobj;
};

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

可以看到:

  • @autoreleasepool底層是創(chuàng)建了一個__AtAutoreleasePool結(jié)構(gòu)體對象;

  • 在創(chuàng)建__AtAutoreleasePool結(jié)構(gòu)體時會在構(gòu)造函數(shù)中調(diào)用objc_autoreleasePoolPush()函數(shù),并返回一個atautoreleasepoolobj(POOL_BOUNDARY存放的內(nèi)存地址,下面會講到);

  • 在釋放__AtAutoreleasePool結(jié)構(gòu)體時會在析構(gòu)函數(shù)中調(diào)用objc_autoreleasePoolPop()函數(shù),并將atautoreleasepoolobj傳入。

1.2、AutoreleasePoolPage

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

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

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

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

自動釋放池的數(shù)據(jù)結(jié)構(gòu)

  • 自動釋放池的主要數(shù)據(jù)結(jié)構(gòu)是:__AtAutoreleasePoolAutoreleasePoolPage;
  • 調(diào)用了 autorelease的對象最終都是通過 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;   // 用來標(biāo)記已釋放的對象
    static size_t const SIZE =              // 每個 Page 對象占用 4096 個字節(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 的個數(shù)

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

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

  • 自動釋放池(即所有的AutoreleasePoolPage對象)是以棧為結(jié)點通過雙向鏈表的形式組合而成;

  • 自動釋放池與線程一一對應(yīng);

  • 每個AutoreleasePoolPage對象占用4096字節(jié)內(nèi)存,其中56個字節(jié)用來存放它內(nèi)部的成員變量,剩下的空間(4040個字節(jié))用來存放autorelease對象的地址。

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


圖片.png

1.2.1、POOL_BOUNDARY

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

  • POOL_BOUNDARY的前世叫做POOL_SENTINEL,稱為哨兵對象或者邊界對象;
  • POOL_BOUNDARY用來區(qū)分不同的自動釋放池,以解決自動釋放池嵌套的問題;
  • 每當(dāng)創(chuàng)建一個自動釋放池,就會調(diào)用push()方法將一個POOL_BOUNDARY入棧,并返回其存放的內(nèi)存地址;
  • 當(dāng)往自動釋放池中添加autorelease對象時,將autorelease對象的內(nèi)存地址入棧,它們前面至少有一個POOL_BOUNDARY;
  • 當(dāng)銷毀一個自動釋放池時,會調(diào)用pop()方法并傳入一個POOL_BOUNDARY,會從自動釋放池中最后一個對象開始,依次給它們發(fā)送release消息,直到遇到這個POOL_BOUNDARY

1.2.2、push

    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) { // 出錯時進入調(diào)試狀態(tài)
            // 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;
    }

當(dāng)創(chuàng)建一個自動釋放池時,會調(diào)用push()方法。push()方法中調(diào)用了autoreleaseFast()方法并傳入了POOL_BOUNDARY哨兵對象。
下面我們來看一下autoreleaseFast()方法的實現(xiàn):

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

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

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

1.2.3、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對應(yīng)在Page中的地址。當(dāng)銷毀自動釋放池時,會調(diào)用pop()方法將自動釋放池中的autorelease對象全部釋放(實際上是從自動釋放池的中的最后一個入棧的autorelease對象開始,依次給它們發(fā)送一條release消息,直到遇到這個POOL_BOUNDARY)。pop()方法的執(zhí)行過程如下:

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

1.2.4、begin、end、empty、full

下面再來看一下begin、endempty、full這些方法的實現(xiàn)。

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

2、查看自動釋放池的情況

可以通過以下私有函數(shù)來查看自動釋放池的情況:

extern void _objc_autoreleasePoolPrint(void);

3、iOS 工程示例分析

在iOS工程中,方法里的autorelease對象是什么時候釋放的呢?

有系統(tǒng)干預(yù)釋放和手動干預(yù)釋放兩種情況。

  • 系統(tǒng)干預(yù)釋放是不指定@autoreleasepool,所有autorelease對象都由主線程的RunLoop創(chuàng)建的@autoreleasepool來管理。
  • 手動干預(yù)釋放就是將autorelease對象添加進我們手動創(chuàng)建的@autoreleasepool中。

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

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

我們先來看以下 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]));
    }
}

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

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

- (void)viewDidLoad {
    [super viewDidLoad];    
    Person *person = [[[Person 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:]
// -[Person dealloc]
// -[ViewController viewDidAppear:]

可以看到,調(diào)用了autorelease方法的person對象不是在viewDidLoad方法結(jié)束后釋放,而是在viewWillAppear方法結(jié)束后釋放,說明在viewWillAppear方法結(jié)束的時候,調(diào)用了pop()方法釋放了person對象。
其實這是由RunLoop控制的,下面來講解一下RunLoop@autoreleasepool的關(guān)系。

3.2、RunLoop 與 @autoreleasepool

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

第1個Observer

  • 監(jiān)聽了kCFRunLoopEntry事件,會調(diào)用objc_autoreleasePoolPush();

第2個Observer

  • ① 監(jiān)聽了kCFRunLoopBeforeWaiting事件,會調(diào)用objc_autoreleasePoolPop()、objc_autoreleasePoolPush();
  • ② 監(jiān)聽了kCFRunLoopBeforeExit事件,會調(diào)用objc_autoreleasePoolPop()。
圖片.png

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

  • kCFRunLoopEntry:在即將進入RunLoop時,會自動創(chuàng)建一個__AtAutoreleasePool結(jié)構(gòu)體對象,并調(diào)用objc_autoreleasePoolPush()函數(shù)。
    -kCFRunLoopBeforeWaiting:在RunLoop即將休眠時,會自動銷毀一個__AtAutoreleasePool對象,調(diào)用objc_autoreleasePoolPop()。然后創(chuàng)建一個新的__AtAutoreleasePool對象,并調(diào)用objc_autoreleasePoolPush()
  • kCFRunLoopBeforeExit,在即將退出RunLoop時,會自動銷毀最后一個創(chuàng)建的__AtAutoreleasePool對象,并調(diào)用objc_autoreleasePoolPop()。

3.3、手動干預(yù)釋放

我們再來看一下手動干預(yù)釋放的情況。

- (void)viewDidLoad {
    [super viewDidLoad];    
    @autoreleasepool {
        Person *person = [[[Person 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__);
}

// -[Person dealloc]
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[ViewController viewDidAppear:]

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容