Autoreleasepool它到底長什么樣?

封面

來,來,來看看這個平時不用,其實它一直那里的Autoreleasepool長什么樣~
其實在main.m 文件入口就已經(jīng)給我們加好了@autoreleasepool的內(nèi)容是這樣的:

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

所以整個 iOS 的應(yīng)用都是包含在一個自動釋放池 block 中的。

實際工作時其實是這樣的:

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

objc_autoreleasePoolPushobjc_autoreleasePoolPop又是啥呢?

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

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

就是AutoreleasePoolPage的兩個pushpop方法,接下來我們這三個一個個看到底都是些啥玩意兒。

AutoreleasePoolPage

先看看 AutoreleasePoolPage 是個啥玩意兒呢?
其實,autoreleasepool 是沒有單獨的內(nèi)存結(jié)構(gòu)的,每一個自動釋放池都是由一系列的 AutoreleasePoolPage 組成的,并且每一個AutoreleasePoolPage 的大小都是 4096 字節(jié)(16 進制 0x1000)

AutoreleasePoolPage {
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}
  • magic用來校驗AutoreleasePoolPage結(jié)構(gòu)是否完整;
  • next指向第一個可用的地址;
  • thread指向當前的線程;
  • parent指向父類
  • child指向子類
autorelease雙向鏈表

Push

static inline void *push()
{
    //autoreleaseFast 關(guān)鍵
    id *dest = autoreleaseFast(POOL_SENTINEL);
    assert(*dest == POOL_SENTINEL);
    return dest;
}

再看autoreleaseFast執(zhí)行具體的插入操作

static inline id *autoreleaseFast(id obj)
{
    //hotPage 可以理解為當前正在使用的 AutoreleasePoolPage
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
    //當前 page 存在且沒有滿時,直接將對象添加到當前 page 中
        return page->add(obj);
    } else if (page) {
    //當前 page 存在且已滿時,創(chuàng)建一個新的 page ,并將對象添加到新創(chuàng)建的 page 中
        return autoreleaseFullPage(obj, page);
    } else {
    //當前 page 不存在時,即還沒有 page 時,創(chuàng)建第一個 page ,并將對象添加到新創(chuàng)建的 page 中
        return autoreleaseNoPage(obj);
    }
}
page->add 添加對象
id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
}

這個方法其實就是一個壓棧的操作,將對象加入 AutoreleasePoolPage 然后移動棧頂?shù)闹羔?/p>

autoreleaseFullPage(當前 hotPage 已滿)
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());
 
    setHotPage(page);
    return page->add(obj);
}

它會從傳入的 page 開始先遍歷整個雙向鏈表

  • 一直遍歷,直到找到一個未滿的AutoreleasePoolPage,
  • 如果找到最后還沒找到,就新建一個 AutoreleasePoolPage
  • 將該頁面標記成 hotPage
  • 調(diào)動 page->add方法添加對象。
autoreleaseNoPage(沒有 hotPage)
static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }
    return page->add(obj);
}

POOL_SENTINEL是一個邊界對象 nil,用來區(qū)別每個page即每個 AutoreleasePoolPage邊界

如果當前內(nèi)存中不存在 AutoreleasePoolPage,就要構(gòu)建一個新的自動釋放池的雙向鏈表,將當前頁標記為 hotPage。但是第一個 AutoreleasePoolPage 是沒有parent 指針的,所以會先向這個page中添加一個POOL_SENTINEL 對象,來確保在pop調(diào)用的時候,不會出現(xiàn)異常。

push小結(jié)

一個 push 操作其實就是創(chuàng)建一個新的 autoreleasepool ,對應(yīng) AutoreleasePoolPage 的具體實現(xiàn)就是往 AutoreleasePoolPage 中的 next 位置插入一個 POOL_SENTINEL,并且返回插入的POOL_SENTINEL 的內(nèi)存地址。

Autorelease

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj) //重點
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

autoreleasepush 操作的實現(xiàn)非常相似。只不過 push 操作插入的是一個POOL_SENTINEL ,而 autorelease 操作插入的是一個具體的autoreleased對象。

Pop

先放一張圖:

Pop.png
其實pop在內(nèi)存中的變化就是長這個樣子。將邊界對象指向的這一頁 AutoreleasePoolPage 內(nèi)的對象釋放

回頭看一看 objc_autoreleasePoolPop

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

pop 函數(shù)的入?yún)⒕褪?push 函數(shù)的返回值,也就是 POOL_SENTINEL 的內(nèi)存地址,即pool token 。當執(zhí)行pop 操作時,內(nèi)存地址在 pool token之后的所有autoreleased對象都會被 release。直到pool token所在 page 的 next 指向 pool token為止。

static inline void pop(void *token) {
//獲取當前 token 所在的 AutoreleasePoolPage
    AutoreleasePoolPage *page = pageForPointer(token);
    id *stop = (id *)token;
//釋放棧中的對象,直到 stop
    page->releaseUntil(stop);
    if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        } else if (page->child->child) {
            page->child->child->kill();
        }
    }
}
  • pageForPointer獲取當前 token 所在的 AutoreleasePoolPage
    -- 通過內(nèi)存地址的操作,獲取當前指針所在頁的首地址
  • releaseUntil 方法釋放棧中的對象,直到 stop
    -- 用循環(huán)持續(xù)釋放 AutoreleasePoolPage 中的內(nèi)容(objc_release ),直到 next 指向了 stop
  • childkill 方法
    --它會將當前頁面以及子頁面全部刪除

總結(jié)

AutoreleasePool = AutoreleasePoolPage (4096字節(jié)) * n;
AutoreleasePoolPage = push + autorelease+pop;
pushautorelease最終都是調(diào)用 autoreleaseFast方法,變了花的往next位置插POOL_SENTINEL或?qū)ο?br> pop 傳入邊界對象,然后對page 中的對象發(fā)送release 的消息

其實

通常情況下,我們是不需要手動添加 autoreleasepool 的,使用線程自動維護的 autoreleasepool 就好了。根據(jù)蘋果官方文檔中對 Using Autorelease Pool Blocks 的描述,我們知道在下面三種情況下是需要我們手動添加 autoreleasepool 的:

如果你編寫的程序不是基于 UI 框架的,比如說命令行工具;
如果你編寫的循環(huán)中創(chuàng)建了大量的臨時對象;
如果你創(chuàng)建了一個輔助線程。

參考資料

What is autoreleasepool? [duplicate]
NSAutoreleasePool
iOS之a(chǎn)utoreleasepool詳解
自動釋放池的前世今生 ---- 深入解析 Autoreleasepool
各個線程 Autorelease 對象的內(nèi)存管理

最后編輯于
?著作權(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ù)。

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