淺談 AutoreleasePool 的實現(xiàn)原理

面試題:

Autoreleasepool 里面的對象什么時候銷毀。
這個問題經(jīng)常被拿來做面試題可训,問很多人昌妹,很少能答對。很多答案都是“當前作用域大括號結束時釋放”握截,顯然沒有正確理解 Autoreleasepool 的機制飞崖。

在沒有手動加入 Autoreleasepool 的情況下,Autorelease 對象是在當前的 runloop 迭代結束時釋放的谨胞,而它能夠釋放的原因是系統(tǒng)在每個 runloop 迭代中都加入了自動釋放池 pushpop固歪。

對于,在手動加入 Autoreleasepool,會在大括號結束時釋放牢裳。如果是嵌套的 Autoreleasepool 中逢防,只有最里層的 Autoreleasepool 使對象的引用計數(shù)加1。反過來說就是最里層的 Autoreleasepool 會阻止外層的 Autoreleasepool 對對象的引用蒲讯。

這樣就可以解釋為什么在方法里面忘朝,如果有 for 循環(huán)的話,應該對 for 循環(huán)加 Autoreleasepool了 判帮,因為這個 Autoreleasepool 阻止了 RunLoop 的一次迭代中加入的 Autoreleasepool 對對象的引用局嘁。這樣在一次循環(huán)結束后,在循環(huán)中創(chuàng)建的變量就會被釋放晦墙。

Autoreleasepool 銷毀時悦昵,在調(diào)用堆棧中可以發(fā)現(xiàn),系統(tǒng)調(diào)用了 -[NSAutoreleasePool release] 方法偎痛,這個方法最終通過調(diào)用 AutoreleasePoolPage::pop(void *) 函數(shù)來負責對 Autoreleasepool 中的 autorelease 對象執(zhí)行 release 操作旱捧。

image

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

蘋果官方文檔:
在開始每一個事件循環(huán)之前系統(tǒng)會在主線程創(chuàng)建一個自動釋放池, 并且在事件循環(huán)結束的時候把前面創(chuàng)建的釋放池釋放, 回收內(nèi)存。

程序運行 -> 開啟事件循環(huán) -> 發(fā)生觸摸事件 -> 創(chuàng)建自動釋放池 -> 處理觸摸事件 -> 事件對象加入自動釋放池 -> 一次事件循環(huán)結束, 銷毀自動釋放池踩麦。

@autoreleasepool 使用時機

蘋果官方文檔

If you are writing a program that is not based on a UI framework, such as a command-line tool.
If you write a loop that creates many temporary objects.You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application.
If you spawn a secondary thread.You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects.

即以下三種情況

  • 非UI程序
  • 循環(huán)中嵌套大量臨時對象時
  • 自己創(chuàng)建了一個輔助線程時

autorelease 方法的實現(xiàn)

調(diào)用棧:

- [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)

autorelease 方法的調(diào)用棧中枚赡,最終都會調(diào)用上面提到的 autoreleaseFast 方法,將當前對象加到 AutoreleasePoolPage中谓谦。

AutoreleasePool 的實現(xiàn)原理

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

@autoreleasepool

使用 clang -rewrite-objc 命令將下面的 Objective-C 代碼重寫成 C++ 代碼:

clang -rewrite-objc main.m

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    }
}

聲明一個 __AtAutoreleasePool 類型的局部變量 __autoreleasepool 來實現(xiàn) @autoreleasepool {}贫橙。當聲明 __autoreleasepool 變量時,構造函數(shù) __AtAutoreleasePool() 被調(diào)用反粥,即執(zhí)行:

atautoreleasepoolobj = objc_autoreleasePoolPush();

當出了當前作用域時卢肃,析構函數(shù) ~__AtAutoreleasePool() 被調(diào)用,即執(zhí)行:

objc_autoreleasePoolPop(atautoreleasepoolobj);

也就是說 @autoreleasepool {} 的實現(xiàn)代碼可以進一步簡化如下:

/* @autoreleasepool */ {
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    // 用戶代碼才顿,所有接收到 autorelease 消息的對象會被添加到這個 autoreleasepool 中
    objc_autoreleasePoolPop(atautoreleasepoolobj);
}

因此莫湘,單個 autoreleasepool 的運行過程可以簡單地理解為 objc_autoreleasePoolPush()[obj release]objc_autoreleasePoolPop(void *) 三個過程郑气。

AutoreleasePoolPage

image.png

從圖中可以看出

  • AutoreleasePoolPage 是由雙向鏈表來實現(xiàn)的幅垮,parentchild 就是用來構造雙向鏈表的指針。
  • magic 用來校驗 AutoreleasePoolPage 的結構是否完整尾组;
  • AutoreleasePool 是按線程一一對應的忙芒,結構中的 thread 指針指向當前線程。
  • AutoreleasePoolPage 會為每個對象會開辟 4096 字節(jié)內(nèi)存讳侨。
  • id *next 指向了下一個為空的內(nèi)存地址(初始化為棧底)呵萨,如果有添加進來的 autorelease 對象,移動到下一個為空的內(nèi)存地址中跨跨。

如果 AutoreleasePoolPage 里面的 autorelease 對象滿了潮峦,也就是 id *next 指針指向了棧頂,會新建一個 AutoreleasePoolPage 對象,連接鏈表跑杭,后來添加的 autorelease 對象在新的 AutoreleasePoolPage 加入铆帽,id *next 指針指向新的 AutoreleasePoolPage 為空的內(nèi)存地址咆耿,即棧底德谅。所以,向一個對象發(fā)送 release 消息萨螺,就是將這個對象加入到當前 AutoreleasePoolPageid *next 指針指向的位置窄做。

POOL_SENTINEL(哨兵對象)

image.png

POOL_SENTINEL 只是 nil 的別名。

在每個自動釋放池初始化調(diào)用 objc_autoreleasePoolPush 的時候慰技,都會把一個 POOL_SENTINEL push 到自動釋放池的棧頂椭盏,并且返回這個 POOL_SENTINEL 哨兵對象。

而當方法 objc_autoreleasePoolPop 調(diào)用時吻商,就會向自動釋放池中的對象發(fā)送 release 消息掏颊,直到第一個 POOL_SENTINEL

詳細參考POOL_SENTINEL(哨兵對象)

objc_autoreleasePoolPush

objc_autoreleasePoolPush() 函數(shù)本質上就是調(diào)用的 AutoreleasePoolPagepush 函數(shù)艾帐。

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

根據(jù)源碼得出乌叶,每次執(zhí)行 objc_autoreleasePoolPush 其實就是創(chuàng)建了一個新的 autoreleasepool,然后會把一個 POOL_SENTINEL push 到自動釋放池的棧頂柒爸,并且返回這個 POOL_SENTINEL 哨兵對象准浴。

static inline void *push() {
    id *dest = autoreleaseFast(POOL_SENTINEL);
    assert(*dest == POOL_SENTINEL);
    return dest;
}

push 函數(shù)通過調(diào)用 autoreleaseFast 函數(shù)并傳入哨兵對象 POOL_SENTINEL 來執(zhí)行具體的插入操作。

static inline id *autoreleaseFast(id obj) {
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}

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

id *autoreleaseNoPage(id obj) {
    // No pool in place.
    assert(!hotPage());
    
    if (obj != POOL_SENTINEL  &&  DebugMissingPools) {
        // We are pushing an object with no pool in place,
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug",
                     (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    
    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    
    // Push an autorelease pool boundary if it wasn't already requested.
    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }
    
    // Push the requested object.
    return page->add(obj);
}

autoreleaseFast 函數(shù)在執(zhí)行一個具體的插入操作時捎稚,分別對三種情況進行了不同的處理:

  • 當前 hotPage 存在且沒有滿時乐横,調(diào)用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中。
  • 當前 hotPage 存在且已滿時今野,調(diào)用 autoreleaseFullPage 初始化一個新的 page葡公,調(diào)用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中。
  • 當前 hotPage 不存在時条霜,調(diào)用 autoreleaseNoPage 創(chuàng)建一個 hotPage催什,調(diào)用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中。

objc_autoreleasePoolPop

objc_autoreleasePoolPop(void *)函數(shù)本質上也是調(diào)用的AutoreleasePoolPage的pop函數(shù)蛔外。

void objc_autoreleasePoolPop(void *ctxt) {
    if (UseGC) return;
    // fixme rdar://9167170
    if (!ctxt) return;
    AutoreleasePoolPage::pop(ctxt);
}

static inline void pop(void *token) {
    AutoreleasePoolPage *page = pageForPointer(token);
    id *stop = (id *)token;

    page->releaseUntil(stop);

    if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        } else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

pop 函數(shù)的入?yún)⒕褪?push 函數(shù)的返回值蛆楞,也就是POOL_SENTINEL 的內(nèi)存地址。根據(jù)這個內(nèi)存地址找到所在的 AutoreleasePoolPage 然后使用 objc_release 釋放 POOL_SENTINEL 指針之前的對象夹厌。

總結:
每調(diào)用一次 push 操作就會創(chuàng)建一個新的 autoreleasepool豹爹,然后往 AutoreleasePoolPage 中插入一個 POOL_SENTINEL,并且返回插入的 POOL_SENTINEL 的內(nèi)存地址.
在執(zhí)行 pop 操作的時候傳入 POOL_SENTINEL矛纹,根據(jù)傳入的哨兵對象地址找到哨兵對象所處的 page
在當前AutoreleasePoolPage中臂聋,然后使用 objc_release 釋放 POOL_SENTINEL 指針之前的對象,并把 id next 指針到正確位置。

參考

自動釋放池的前世今生 ---- 深入解析 Autoreleasepool

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末孩等,一起剝皮案震驚了整個濱河市艾君,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肄方,老刑警劉巖冰垄,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異权她,居然都是意外死亡虹茶,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門隅要,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炊苫,“玉大人轰传,你說我怎么就攤上這事泽疆“窕蓿” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵廓啊,是天一觀的道長欢搜。 經(jīng)常有香客問我,道長崖瞭,這世上最難降的妖魔是什么狂巢? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮书聚,結果婚禮上唧领,老公的妹妹穿的比我還像新娘。我一直安慰自己雌续,他們只是感情好斩个,可當我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著驯杜,像睡著了一般受啥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸽心,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天滚局,我揣著相機與錄音,去河邊找鬼顽频。 笑死藤肢,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的糯景。 我是一名探鬼主播嘁圈,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼省骂,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了最住?” 一聲冷哼從身側響起钞澳,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎涨缚,沒想到半個月后轧粟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡仗岖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年逃延,在試婚紗的時候發(fā)現(xiàn)自己被綠了览妖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片轧拄。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖讽膏,靈堂內(nèi)的尸體忽然破棺而出檩电,到底是詐尸還是另有隱情,我是刑警寧澤府树,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布俐末,位于F島的核電站,受9級特大地震影響奄侠,放射性物質發(fā)生泄漏卓箫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一垄潮、第九天 我趴在偏房一處隱蔽的房頂上張望烹卒。 院中可真熱鬧,春花似錦弯洗、人聲如沸旅急。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽藐吮。三九已至,卻和暖如春逃贝,著一層夾襖步出監(jiān)牢的瞬間谣辞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工沐扳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留泥从,地道東北人。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓迫皱,卻偏偏與公主長得像歉闰,于是被迫代替她去往敵國和親辖众。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,969評論 2 355

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