面試題:
Autoreleasepool
里面的對象什么時候銷毀。
這個問題經(jīng)常被拿來做面試題可训,問很多人昌妹,很少能答對。很多答案都是“當前作用域大括號結束時釋放”握截,顯然沒有正確理解 Autoreleasepool
的機制飞崖。
在沒有手動加入 Autoreleasepool
的情況下,Autorelease
對象是在當前的 runloop
迭代結束時釋放的谨胞,而它能夠釋放的原因是系統(tǒng)在每個 runloop
迭代中都加入了自動釋放池 push
和 pop
固歪。
對于,在手動加入 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
操作旱捧。
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
從圖中可以看出
-
AutoreleasePoolPage
是由雙向鏈表來實現(xiàn)的幅垮,parent
和child
就是用來構造雙向鏈表的指針。 -
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
消息萨螺,就是將這個對象加入到當前 AutoreleasePoolPage
的 id *next
指針指向的位置窄做。
POOL_SENTINEL(哨兵對象)
POOL_SENTINEL
只是 nil
的別名。
在每個自動釋放池初始化調(diào)用 objc_autoreleasePoolPush
的時候慰技,都會把一個 POOL_SENTINEL push
到自動釋放池的棧頂椭盏,并且返回這個 POOL_SENTINEL
哨兵對象。
而當方法 objc_autoreleasePoolPop
調(diào)用時吻商,就會向自動釋放池中的對象發(fā)送 release
消息掏颊,直到第一個 POOL_SENTINEL
。
objc_autoreleasePoolPush
objc_autoreleasePoolPush()
函數(shù)本質上就是調(diào)用的 AutoreleasePoolPage
的 push
函數(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
指針到正確位置。