在上一篇文章中,詳細分析了IOS內存管理的內存佈局、內存管理方案、引用計數等內容,本篇文章將繼續上篇文章的內容探索自動釋放池
autoreleasepool
的相關知識。html
熟悉OC開發的都知道,在main
函數中就有@autoreleasepool
這樣一個東西,其實這就是自動釋放池。那麼@autoreleasepool
的底層實現是什麼樣的呢?咱們在命令行中使用 clang -rewrite-objc main.m -o main.cpp
讓編譯器從新改寫這個文件,講獲得一個main.cpp文件,打開該文件,找到其中的main函數。c#
int main(int argc, const char *argv[])
{
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
return 0;
}
複製代碼
咱們能夠看到@autoreleasepool
轉化成了__AtAutoreleasePool
這樣一個結構體,那麼意味着@autoreleasepool
的本質就是__AtAutoreleasePool
結構體。bash
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
複製代碼
這個結構體會在初始化時調用objc_autoreleasePoolPush()
方法,會在析構時調用 objc_autoreleasePoolPop
方法。app
這就說明了main函數在實際工做的時候是這樣的:less
int main(int argc, const char *argv[])
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
return 0;
}
複製代碼
彷佛一切都是圍繞着objc_autoreleasePoolPush()
和objc_autoreleasePoolPop
這兩個方法展開。那麼咱們來看下這兩個方法的源碼實現:ide
void *
objc_autoreleasePoolPush(void)
{
// 調用了AutoreleasePoolPage中的push方法
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
// 調用了AutoreleasePoolPage中的pop方法
AutoreleasePoolPage::pop(ctxt);
}
複製代碼
上面的兩個方法看上去是對AutoreleasePoolPage
對應靜態方法push
和pop
的封裝。函數
在runtime中的源碼(objc4-756.2版本)中找到了一段註釋,這段註釋對咱們理解AutoreleasePoolPage
的底層結構會有所幫助。oop
- A thread's autorelease pool is a stack of pointers.
- Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
- A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
- The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
- Thread-local storage points to the hot page, where newly autoreleased objects are stored.
翻譯中文以下:佈局
- 一個線程的自動釋放池是一個指針的堆棧結構。
- 每一個指針表明一個須要釋放的對象或者POOL_BOUNDARY(自動釋放池邊界)
- 一個 pool token 就是這個 pool 所對應的 POOL_BOUNDARY 的內存地址。當這個 pool 被 pop 的時候,全部內存地址在 pool token 以後的對象都會被 release。
- 這個堆棧被劃分紅了一個以 page 爲結點的雙向鏈表。pages 會在必要的時候動態地增長或刪除。
- Thread-local storage(線程局部存儲)指向 hot page ,即最新添加的 autoreleased 對象所在的那個 page 。
從上面這段註釋中咱們能夠知道自動釋放池是一種棧的結構,遵循先進後出的原則,每個自動釋放池都是由一系列的AutoreleasePoolPage
組成的,而AutoreleasePoolPage
是以雙向鏈表的形式鏈接起來。post
來看一下AutoreleasePoolPage
的代碼定義(只列出了關鍵代碼,部分代碼省略)。
class AutoreleasePoolPage
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
// AutoreleasePoolPage的大小,經過宏定義,能夠看到是4096字節
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;//16字節
id *next;//8字節
pthread_t const thread;//8字節
AutoreleasePoolPage * const parent;//8字節
AutoreleasePoolPage *child;//8字節
uint32_t const depth;//4字節
uint32_t hiwat;//4字節
}
複製代碼
- magic:用來校驗
AutoreleasePoolPage
的結構是否完整。- *next:next指向的是下一個
AutoreleasePoolPage
中下一個爲空的內存地址(新來的對象會存儲到next
處),初始化時指向begin()
。- thread:保存了當前頁所在的線程(一個
AutoreleasePoolPage
屬於一個線程,一個線程中能夠有多個AutoreleasePoolPage
)。- *parent:指向父節點,第一個
parent
節點爲nil
。- *child:指向子節點,最後一個
child
節點爲nil
。- depth:表明深度,從0開始,遞增+1。
- hiwat:表明
high water Mark
最大入棧數。- SIZE:
AutoreleasePoolPage
的大小,值爲PAGE_MAX_SIZE
,4096個字節。- POOL_BOUNDARY:只是
nil
的別名。在每一個自動釋放池初始化調用objc_autoreleasePoolPush
的時候,都會把一個POOL_SENTINEL
push到自動釋放池的棧頂,而且返回這個POOL_SENTINEL
自動釋放池邊界。而當方法objc_autoreleasePoolPop
調用時,就會向自動釋放池中的對象發送release
消息,直到第一個POOL_SENTINEL
。
在AutoreleasePoolPage
中的第一個對象是存儲在next
後面,那麼就造成以下圖所示這樣一個結構。
其中的56個字節存儲的AutoreleasePoolPage
的成員變量,其餘的區域存儲加載到自動釋放池的對象。 當next==begin()
時表示AutoreleasePoolPage
爲空,當next==end()
的時表示AutoreleasePoolPage
已滿。
在上一個小節的內容中咱們分析了AutoreleasePoolPage
的結構,瞭解到每個AutoreleasePoolPage
的大小是4096字節,其中56字節用於存儲成員變量,剩下的區域存儲加載到自動釋放池的對象,那麼彷佛答案呼之欲出,一個AutoreleasePoolPage
能夠存儲(4096-56)/8=505個對象。可是有一個注意的點,第一個page存放的須要釋放的對象的容量應該是504個,由於在建立page的時候會在next
的位置插入1POOL_SENTINEL
。
經過前面小節的分析,咱們知道objc_autoreleasePoolPush
的本質就是調用push
方法。咱們先來看下push方法的源碼。
static inline void *push()
{
id *dest;
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
複製代碼
在push
方法中實際上調用的是autoreleaseFast
方法,而且首先將一個POOL_BOUNDARY
對象插入到棧頂。slowpath
表示小几率發生。
以下是autoreleaseFast
方法的源碼
static inline id *autoreleaseFast(id obj)
{
// hotPage就是當前正在使用的AutoreleasePoolPage
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 有hotPage且hotPage不滿,將對象添加到hotPage中
return page->add(obj);
} else if (page) {
// 有hotPage可是hotPage已滿
// 使用autoreleaseFullPage初始化一個新頁,並將對象添加到新的AutoreleasePoolPage中
return autoreleaseFullPage(obj, page);
} else {
// 無hotPage
// 使用autoreleaseNoPage建立一個hotPage,並將對象添加到新建立的page中
return autoreleaseNoPage(obj);
}
}
複製代碼
autoreleaseFast
方法的代碼很簡單,只要是三個判斷分支。
- 若是有
hotPage
且沒有滿,則調用add
方法將對象添加到hotPage中。不然執行第2步。- 若是有
hotPage
可是已經滿了,則調用autoreleaseFullPage
方法初始化一個新頁,並將對象添加到新的AutoreleasePoolPage
中。不然執行第3步。- 若是沒有
hotPage
,則調用autoreleaseNoPage
方法建立一個hotPage
,並將對象添加到新建立的page
中
hotPage 能夠理解爲當前正在使用的 AutoreleasePoolPage。
add
方法將對象添加到AutoreleasePoolPage
中。
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;//將obj存放在next處,並將next指向下一個位置
protect();
return ret;
}
複製代碼
這個方法其實就是一個壓棧操做,將對象添加到AutoreleasePoolPage
中,而後移動棧頂指針。
autoreleaseFullPage
方法會從新開闢一個新的AutoreleasePoolPage
頁,並將對象添加到其中。
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 {
// 若是page->child不爲空,那麼使用page->child
if (page->child) page = page->child;
// 不然的話,初始化一個新的AutoreleasePoolPage
else page = new AutoreleasePoolPage(page);
} while (page->full());
// 將找到的合適的page設置成hotPage
setHotPage(page);
// 將對象添加到hotPage中
return page->add(obj);
}
複製代碼
遍歷找到未滿的的page,若是沒有找到則初始化一個新的page,並將page設置爲hotPage,同時將對象添加到這個page中。
若是當前內存中不存在hotPage
,就會調用autoreleaseNoPage
方法初始化一個AutoreleasePoolPage
。
id *autoreleaseNoPage(id obj) {
// Install the first page.
// 初始化一個AutoreleasePoolPage
// 當前內存中不存在AutoreleasePoolPage,則從頭開始構建AutoreleasePoolPage,也就是其parent爲nil
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
// 將初始化的AutoreleasePoolPage設置成hotPage
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
// 添加一個邊界對象(nil)
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
// 將對象添加到AutoreleasePoolPage中
return page->add(obj);
}
複製代碼
當前內存中不存在AutoreleasePoolPage
,則從頭開始構建AutoreleasePoolPage
,也就是其parent
爲nil
。初始化以後,將當前頁標記爲hotPage
,而後會先向這個page
中添加一個POOL_SENTINEL
對象,來確保在pop
調用的時候,不會出現異常。最後,將對象添加到自動釋放池中。
上面小節咱們探索了objc_autoreleasePoolPush
,下面咱們看看objc_autoreleasePoolPop
。 objc_autoreleasePoolPop
的本質就是調用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();
}
}
}
複製代碼
上面方法作了以下幾件事:
- 調用
pageForPointer
獲取當前token所在的page。- 調用
releaseUntil
方法釋放棧中的對象,直到stop
。- 調用
child
的kill
方法。
pageForPointer
方法主要是經過經過內存地址的操做,獲取當前token所在頁的首地址。
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;
ASSERT(offset >= sizeof(AutoreleasePoolPage));
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
複製代碼
將指針與頁面的大小(4096)取模,能夠獲得當前指針的偏移量。而後將指針的地址減偏移量即可以獲得首地址。
void releaseUntil(id *stop) {
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
// 釋放AutoreleasePoolPage中的對象,直到next指向stop
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
// hotPage能夠理解爲當前正在使用的page
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
// 若是page爲空的話,將page指向上一個page
// 註釋寫到猜想這裏可使用if,我感受也可使用if
// 由於根據AutoreleasePoolPage的結構,理論上不可能存在連續兩個page都爲空
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
// obj = page->next; page->next--;
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
// POOL_BOUNDARY爲nil,是哨兵對象
if (obj != POOL_BOUNDARY) {
// 釋放obj對象
objc_release(obj);
}
}
// 從新設置hotPage
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
複製代碼
由於AutoreleasePool
實際上就是由AutoreleasePoolPage
組成的雙向鏈表,所以,*stop
可能不是在最新的AutoreleasePoolPage
中,即hotPage
,這時須要從hotPage
開始,一直釋放,直到stop
,中間所通過的全部AutoreleasePoolPage
裏面的對象都要釋放。
對象的釋放objc_release
方法請移步前面的文章iOS內存管理一:Tagged Pointer&引用計數。
kill
方法刪除雙向鏈表中的每個page
void kill() {
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
AutoreleasePoolPage *page = this;
// 找到鏈表最末尾的page
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
// 循環刪除每個page
do {
deathptr = page;
page = page->parent;
if (page) {
page->unprotect();
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
複製代碼
官方文檔Using Autorelease Pool Blocks中關於自動釋放池和線程的關係有以下一段描述。
Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.
If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.
翻譯成中文以下:
應用程序中的每一個線程都維護本身的自動釋放池塊堆棧。若是您正在編寫一個僅限基礎的程序,或者正在分離一個線程,那麼您須要建立本身的自動釋放池塊。
若是您的應用程序或線程是長生命週期的,而且可能會生成大量的自動釋放對象,那麼您應該使用自動釋放池塊(如在主線程上使用AppKit和UIKit);不然,自動釋放的對象會累積,內存佔用會增長。若是分離的線程不進行Cocoa調用,則不須要使用自動釋放池塊。
從上面這段秒速咱們能夠知道自動釋放池和線程是緊密相關的,每個自動釋放池只對應一個線程。
通常不多會將自動釋放池和RunLoop
聯繫起來,可是若是打印[NSRunLoop currentRunLoop]
結果中會發現和自動釋放池相關的回調。
<CFRunLoopObserver 0x6000024246e0 [0x7fff8062ce20]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
<CFRunLoopObserver 0x600002424640 [0x7fff8062ce20]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
複製代碼
即App啓動後,蘋果會給RunLoop
註冊不少個observers
,其中有兩個是跟自動釋放池相關的,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()
。\
observer
監聽的是activities=0x1(kCFRunLoopEntry)
,也就是在即將進入loop
時,其回調會調用_objc_autoreleasePoolPush()
建立自動釋放池;observer
監聽的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit)
, 即監聽的是準備進入睡眠和即將退出loop
兩個事件。在準備進入睡眠以前,由於睡眠可能時間很長,因此爲了避免佔用資源先調用_objc_autoreleasePoolPop()
釋放舊的釋放池,並調用_objc_autoreleasePoolPush()
建立新建一個新的,用來裝載被喚醒後要處理的事件對象;在最後即將退出loop
時則會 _objc_autoreleasePoolPop()
釋放池子。AutoreleasePoolPage
以雙向鏈表的方式實現的。autorelease
方法時,會將對象加入AutoreleasePoolPage
的棧中。AutoreleasePoolPage::pop
方法會向棧中的對象發送release
消息。