iOS底層學習 - 記憶體管理之Autoreleasepool
有關記憶體管理的相關優化方案和引用計數的相關原理,我們已經瞭解,本章來講解在記憶體管理中的另一個方案Autoreleasepool
初探Autoreleasepool
Autoreleasepool作用
通過之前章節的學習,我們知道在ARC
下,LLVM編譯器會自動幫我們生產retain
、release
和autorelease
等程式碼,減少了在MRC
下的工作量。呼叫autorelease
會將該物件新增進自動釋放池中,它會在一個恰當的時刻自動給物件呼叫release
,所以autorelease
但是在ARC
下,autorelease
方法已被禁用,我們可以使用__autoreleasing
修飾符修飾物件將物件註冊到自動釋放池中。
Autoreleasepool建立
- 在
MRC
下,可以使用NSAutoreleasePool
或者@autoreleasepool
。建議使用@autoreleasepool
,蘋果說它比NSAutoreleasePool
快大約六倍。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release];
複製程式碼
- 而在ARC下,已經禁止使用
NSAutoreleasePool
類建立自動釋放池,只能使用@autoreleasepool。
@autoreleasepool {
// Code benefitting from a local autorelease pool.
}
複製程式碼
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. If you use the Application Kit,you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop,however,it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.
以上是蘋果對自動釋放池的一段介紹,其意思為:AppKit
和 UIKit
框架在事件迴圈(RunLoop
)的每次迴圈開始時,在主執行緒建立一個自動釋放池,並在每次迴圈結束時銷燬它,在銷燬時釋放自動釋放池中的所有autorelease
物件。通常情況下我們不需要手動建立自動釋放池,但是如果我們在迴圈中建立了很多臨時的autorelease
物件,則手動建立自動釋放池來管理這些物件可以很大程度地減少記憶體峰值。
Autoreleasepool原理探究
Autoreleasepool底層結構
我們知道在main
函式中,會建立一個@autoreleasepool {}
物件,那麼其底層的結構是怎樣的呢?
int main(int argc,const char * argv[]) {
@autoreleasepool {}
return 0;
}
複製程式碼
我們還是使用clang -rewrite-objc main.m
命令,轉換為C++程式碼檢視。通過以下程式碼,我們可以發現轉換後@autoreleasepool
主要做了以下幾點:
-
@autoreleasepool
底層是建立了一個__AtAutoreleasePool
結構體物件; - 在建立
__AtAutoreleasePool
結構體時會在建構函式中呼叫objc_autoreleasePoolPush()
函式,並返回一個atautoreleasepoolobj
(POOL_BOUNDARY存放的記憶體地址,下面會講到); - 在釋放
__AtAutoreleasePool
結構體時會在解構函式中呼叫objc_autoreleasePoolPop()
函式,並將atautoreleasepoolobj
傳入。
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
int main(int argc,const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; }
return 0;
}
複製程式碼
AutoreleasePoolPage底層結構
首先來看AutoreleasePoolPage
的相關原始碼,其幾個成員變數的含義如下:
- 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個位元組,其中56
個位元組用來儲存自己的變數,剩下的4040
個位元組用來儲存要釋放的物件,也就是最多505
個物件。 -
POOL_BOUNDARY:
- 只是nil的別名。前世叫做
POOL_SENTINEL
,稱為哨兵物件或者邊界物件; -
POOL_BOUNDARY
用來區分不同的自動釋放池,以解決自動釋放池巢狀的問題 - 每當建立一個自動釋放池,就會呼叫
push()
方法將一個POOL_BOUNDARY
入棧,並返回其存放的記憶體地址; - 當往自動釋放池中新增
autorelease
物件時,將autorelease
物件的記憶體地址入棧,它們前面至少有一個POOL_BOUNDARY
; - 當銷燬一個自動釋放池時,會呼叫
pop()
方法並傳入一個POOL_BOUNDARY
,會從自動釋放池中最後一個物件開始,依次給它們傳送release
訊息,直到遇到這個POOL_BOUNDARY
。
- 只是nil的別名。前世叫做
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
magic_t const magic;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
AutoreleasePoolPageData(__unsafe_unretained id* _next,pthread_t _thread,AutoreleasePoolPage* _parent,uint32_t _depth,uint32_t _hiwat)
: magic(),next(_next),thread(_thread),parent(_parent),child(nil),depth(_depth),hiwat(_hiwat)
{
}
};
----------------------------------------------------------------------------------
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MIN_SIZE; // size and alignment,power of 2
#endif
private:
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const COUNT = SIZE / sizeof(id);
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
// SIZE-sizeof(*this) bytes of contents follow
......
}
複製程式碼
/***********************************************************************
Autorelease pool implementation
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
是以棧為結點通過雙向連結串列的形式組合而成;遵循先進後出規則,整個自動釋放池由一系列的AutoreleasePoolPage
組成的,而AutoreleasePoolPage
是以雙向連結串列的形式連線起來。 - 自動釋放池與執行緒一一對應;
- 每個
AutoreleasePoolPage
物件佔用4096
位元組記憶體,其中56
個位元組用來存放它內部的成員變數,剩下的空間(4040個位元組)用來存放autorelease物件的地址。要注意的是第一頁只有504個物件,因為在建立page的時候會在next
的位置插入1個POOL_SENTINEL
。 -
POOL_BOUNDARY
為哨兵物件,入棧時插入,出棧時釋放物件到此傳入的哨兵物件
該圖表示AutoreleasePoolPage
的雙向列表結構
AutoreleasePoolPage
的雙向列表和棧結構
AutoreleasePoolPage::push()原理
首先我們看objc_autoreleasePoolPush
的原始碼,發現其內部就是呼叫了AutoreleasePoolPage
的push()
方法。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
複製程式碼
來到AutoreleasePoolPage
內部的push()
方法,其中slowpath
表示很少會走到,是底部的容錯處理,所以最終會走到autoreleaseFast
方法中
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;
}
複製程式碼
檢視autoreleaseFast
原始碼,先是呼叫了hotPage()
,hotPage()
方法就是用來獲得新建立的未滿的Page。其內部主要是判斷邏輯:
- 如果當前 Page 存在且未滿,走
page->add(obj)
將 autorelease 物件入棧,即新增到當前 Page 中 - 如果當前 Page 存在但已滿,走
autoreleaseFullPage
,建立一個新的 Page,並將 autorelease 物件新增進去 - 如果當前 Page 不存在,即還沒建立過 Page,建立第一個 Page,並將 autorelease 物件新增進去
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage(); // 雙向連結串列中的最後一個 Page
if (page && !page->full()) {// 如果當前 Page 存在且未滿
return page->add(obj); // 將 autorelease 物件入棧,即新增到當前 Page 中;
} else if (page) { // 如果當前 Page 存在但已滿
return autoreleaseFullPage(obj,page); // 建立一個新的 Page,並將 autorelease 物件新增進去
} else {// 如果當前 Page 不存在,即還沒建立過 Page
return autoreleaseNoPage(obj); // 建立第一個 Page,並將 autorelease 物件新增進去
}
}
複製程式碼
page->full()
首先我們來看一下,如何判斷當前page是否是滿狀態的。
-
begin
的地址為:Page自己的地址+Page物件的大小56個位元組; -
end
的地址為:Page自己的地址+4096個位元組; -
empty
:判斷Page是否為空的條件是next地址是不是等於begin; -
full
:判斷Page是否已滿的條件是next地址是不是等於end(棧頂)。
我們知道next
指向的是下一個AutoreleasePoolPage
中下一個為空的記憶體地址,新物件會存在next
,如果此時next
指向end
則代表當前AutoreleasePoolPage
已滿。
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();
}
複製程式碼
page->add(obj)
當page沒有存滿時,會呼叫此方法,內部的原理非常簡單,就是一個壓棧的操作,並將next
指標指向這個物件的下一個位置,然後將該物件的位置返回。
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
複製程式碼
autoreleaseFullPage(obj,page)
如果當前 Page 存在但已滿,會呼叫此方法。其內部實現的主要方法就是一個do..while
迴圈,主要實現了一下的邏輯
- 由於page是連結串列結構,所以通過迴圈查詢
page->child
- 一級級判斷是否
page->full()
- 如果到最後一個page都是滿的,那麼就新
new
一個AutoreleasePoolPage
- 如果有不滿的,或者新建立的,呼叫
setHotPage(page)
將當前頁設定為活躍 - 最後將物件通過
page->add
壓棧
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);
}
複製程式碼
autoreleaseNoPage(obj)
當沒有page時,會走到此方法,其主要邏輯如下:
- 先會判斷是否有空的自動釋放池存在,如果沒有會通過
setEmptyPoolPlaceholder()
生成一個佔位符,表示一個空的自動釋放池 - 建立第一個
Page
,設定它為hotPage
- 將一個
POOL_BOUNDARY
新增進Page中,並返回POOL_BOUNDARY
的下一個位置。 - 插入第一個物件
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
ASSERT(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that,push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",objc_thread_self(),(void*)obj,object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// We are pushing a pool with no pool in place,// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
return page->add(obj);
}
複製程式碼
AutoreleasePoolPage::pop(ctxt)原理
看完物件入棧的實現,我們再來看一下出棧的實現。
首先pop的入參token
即為POOL_BOUNDARY
對應在Page中的地址。當銷燬自動釋放池時,會從從自動釋放池的中的最後一個入棧的autorelease物件開始,依次給它們傳送一條release
訊息,直到遇到這個POOL_BOUNDARY
,具體的步驟如下:
- 判斷
token
是不是EMPTY_POOL_PLACEHOLDER
,是的話就清空這個自動釋放池 - 如果不是的話,就通過
pageForPointer(token)
拿到token
所在的Page
- 通過
page->releaseUntil(stop)
將自動釋放池中的autorelease
物件全部釋放,傳參sto
p即為POOL_BOUNDARY
的地址 - 判斷當前Page是否有子Page,有的話就銷燬
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)
該方法,主要是通過token來拿到當前所在的page。主要實現原理是將指標token與頁面的大小(4096)取模,可以得到當前指標的偏移量。然後將指標的地址減偏移量便可以得到首地址。即該page的地址
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;
}
複製程式碼
page->releaseUntil(stop)
pop()方法中釋放autorelease物件的過程在releaseUntil()方法中,下面來看一下這個方法的實現:
-
releaseUntil()
方法其實就是通過一個while
迴圈 - 從
hotPage
開始,一直釋放,直到stop
,即傳入的POOL_BOUNDARY
- 最後設定釋放完的當前page為
hotPage
void releaseUntil(id *stop)
{
// Not recursive: we donot want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
while (this->next != stop) {
// Restart from hotPage() every time,in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`,but I canot prove it
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next; // next指標是指向最後一個物件的後一個位置,所以需要先減1
memset((void*)page->next,SCRIBBLE,sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
複製程式碼
page->kill()
kill
方法刪除雙向連結串列中的每一個的page,找到當前page
的 child
方向尾部 page
,然後反向挨著釋放並且把其parent
節點的 child
指標置空。
void kill()
{
// Not recursive: we donot 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);
}
複製程式碼
Autoreleasepool巢狀探究
準備:
- 由於ARC環境下不能呼叫autorelease等方法,所以需要將工程切換為MRC環境。
- 使用
extern void _objc_autoreleasePoolPrint(void);
方法來列印autoreleasePool的相關資訊
單個page巢狀
int main(int argc,const char * argv[]) {
_objc_autoreleasePoolPrint(); // print1
@autoreleasepool { //r1 = push()
_objc_autoreleasePoolPrint(); // print2
NSObject *p1 = [[[NSObject alloc] init] autorelease];
NSObject *p2 = [[[NSObject alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print3
@autoreleasepool { //r2 = push()
NSObject *p3 = [[[NSObject alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print4
@autoreleasepool { //r3 = push()
NSObject *p4 = [[[NSObject alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print5
} //pop(r3)
_objc_autoreleasePoolPrint(); // print6
} //pop(r2)
_objc_autoreleasePoolPrint(); // print7
} //pop(r1)
_objc_autoreleasePoolPrint(); // print8
return 0;
}
複製程式碼
列印結果過如下,通過列印結果,我們可以印證上面原理的探索,其主要的進出棧流程如下圖所示,且作用域只在@autoreleasepool {}
之間,超過之後就全部呼叫pop釋放
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 0 releases pending.
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 0 releases pending.
objc[12943]: [0x1] ................ PAGE (placeholder)
objc[12943]: [0x1] ################ POOL (placeholder)
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 3 releases pending.
objc[12943]: [0x7f924480b000] ................ PAGE (hot) (cold)
objc[12943]: [0x7f924480b038] ################ POOL 0x7f924480b038
objc[12943]: [0x7f924480b040] 0x600001b34070 NSObject
objc[12943]: [0x7f924480b048] 0x600001b34080 NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 5 releases pending.
objc[12943]: [0x7f924480b000] ................ PAGE (hot) (cold)
objc[12943]: [0x7f924480b038] ################ POOL 0x7f924480b038
objc[12943]: [0x7f924480b040] 0x600001b34070 NSObject
objc[12943]: [0x7f924480b048] 0x600001b34080 NSObject
objc[12943]: [0x7f924480b050] ################ POOL 0x7f924480b050
objc[12943]: [0x7f924480b058] 0x600001b34090 NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 7 releases pending.
objc[12943]: [0x7f924480b000] ................ PAGE (hot) (cold)
objc[12943]: [0x7f924480b038] ################ POOL 0x7f924480b038
objc[12943]: [0x7f924480b040] 0x600001b34070 NSObject
objc[12943]: [0x7f924480b048] 0x600001b34080 NSObject
objc[12943]: [0x7f924480b050] ################ POOL 0x7f924480b050
objc[12943]: [0x7f924480b058] 0x600001b34090 NSObject
objc[12943]: [0x7f924480b060] ################ POOL 0x7f924480b060
objc[12943]: [0x7f924480b068] 0x600001b2c030 NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 5 releases pending.
objc[12943]: [0x7f924480b000] ................ PAGE (hot) (cold)
objc[12943]: [0x7f924480b038] ################ POOL 0x7f924480b038
objc[12943]: [0x7f924480b040] 0x600001b34070 NSObject
objc[12943]: [0x7f924480b048] 0x600001b34080 NSObject
objc[12943]: [0x7f924480b050] ################ POOL 0x7f924480b050
objc[12943]: [0x7f924480b058] 0x600001b34090 NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 3 releases pending.
objc[12943]: [0x7f924480b000] ................ PAGE (hot) (cold)
objc[12943]: [0x7f924480b038] ################ POOL 0x7f924480b038
objc[12943]: [0x7f924480b040] 0x600001b34070 NSObject
objc[12943]: [0x7f924480b048] 0x600001b34080 NSObject
objc[12943]: ##############
objc[12943]: ##############
objc[12943]: AUTORELEASE POOLS for thread 0x11aee5dc0
objc[12943]: 0 releases pending.
objc[12943]: [0x7f924480b000] ................ PAGE (hot) (cold)
objc[12943]: ##############
複製程式碼
多個page巢狀
int main(int argc,const char * argv[]) {
@autoreleasepool { //r1 = push()
for (int i = 0; i < 600; i++) {
NSObject *p = [[[NSObject alloc] init] autorelease];
}
@autoreleasepool { //r2 = push()
for (int i = 0; i < 500; i++) {
NSObject *p = [[[NSObject alloc] init] autorelease];
}
@autoreleasepool { //r3 = push()
for (int i = 0; i < 200; i++) {
NSObject *p = [[[NSObject alloc] init] autorelease];
}
_objc_autoreleasePoolPrint();
} //pop(r3)
} //pop(r2)
} //pop(r1)
return 0;
}
複製程式碼
可以看到列印結果如下:根據原理的探究,我們知道每個page除了第一頁是504個物件外,其他最多儲存505個物件,當一個page滿了時候,會建立一個新的page,並且每個page之間是以棧
為結點通過雙向連結串列
的形式組合而成。其主要流程如下圖所示
objc[69731]: ##############
objc[69731]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[69731]: 1303 releases pending. //當前自動釋放池中有1303個物件(3個POOL_BOUNDARY和1300個NSObject例項)
objc[69731]: [0x100806000] ................ PAGE (full) (cold) /* 第一個PAGE,full代表已滿,cold代表coldPage */
objc[69731]: [0x100806038] ################ POOL 0x100806038 //POOL_BOUNDARY
objc[69731]: [0x100806040] 0x10182a040 NSObject //p1
objc[69731]: [0x100806048] ..................... //...
objc[69731]: [0x100806ff8] 0x101824e40 NSObject //p504
objc[69731]: [0x102806000] ................ PAGE (full) /* 第二個PAGE */
objc[69731]: [0x102806038] 0x101824e50 NSObject //p505
objc[69731]: [0x102806040] ..................... //...
objc[69731]: [0x102806330] 0x101825440 NSObject //p600
objc[69731]: [0x102806338] ################ POOL 0x102806338 //POOL_BOUNDARY
objc[69731]: [0x102806340] 0x101825450 NSObject //p601
objc[69731]: [0x102806348] ..................... //...
objc[69731]: [0x1028067e0] 0x101825d90 NSObject //p1008
objc[69731]: [0x102804000] ................ PAGE (hot) /* 第三個PAGE,hot代表hotPage */
objc[69731]: [0x102804038] 0x101826dd0 NSObject //p1009
objc[69731]: [0x102804040] ..................... //...
objc[69731]: [0x102804310] 0x101827380 NSObject //p1100
objc[69731]: [0x102804318] ################ POOL 0x102804318 //POOL_BOUNDARY
objc[69731]: [0x102804320] 0x101827390 NSObject //p1101
objc[69731]: [0x102804328] ..................... //...
objc[69731]: [0x102804958] 0x10182b160 NSObject //p1300
objc[69731]: ##############
複製程式碼
@autorelease與RunLoop
@autorelease與RunLoop關係
關於RunLoop的相關知識,可以檢視文章☞iOS底層學習 - 深入RunLoop
其中主要的的RunLoop執行流程如下圖所示
而且通過列印[NSRunLoop currentRunLoop]
,可以發現其中有_wrapRunLoopWithAutoreleasePoolHandler()
代表的相關AutoreleasePool的回撥。
<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,order = -2147483647,values = (0 : <0x7fc18f80e038>)}}
複製程式碼
那麼,RunLoop和AutoreleasePool的主要關係如下
-
kCFRunLoopEntry
:在即將進入RunLoop時,會自動建立一個__AtAutoreleasePool
結構體物件,並呼叫objc_autoreleasePoolPush()
函式。 -
kCFRunLoopBeforeWaiting
:在RunLoop即將休眠時,會自動銷燬一個__AtAutoreleasePool
物件,呼叫objc_autoreleasePoolPop()
。然後建立一個新的__AtAutoreleasePool物件
,並呼叫objc_autoreleasePoolPush()
。 -
kCFRunLoopBeforeExi
t,在即將退出RunLoop時,會自動銷燬最後一個建立的__AtAutoreleasePool
物件,並呼叫objc_autoreleasePoolPop()
。
main函式變化分析
瞭解了他們之間的關係,我們可以通過main函式,來分析一下
// 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,NSStringFromClass([AppDelegate class]));
}
}
複製程式碼
我們知道@autoreleasepool {}
的作用域只在其大括號之間,而且UIApplicationMain
主執行緒會建立主RunLoop,通過上面的探究,我們知道在建立RunLoop的時候,也會對應的建立AutoreleasePool
。其中使用autorelease
修飾的物件都會新增到RunLoop建立的自動釋放池中。
所以Xcode 11和之前版本的區別,主要就是Xcode 11將@autoreleasepool {}
提前,這可以保證@autoreleasepool
中的autorelease
物件在程式啟動後立即釋放。而之前的版本是在主執行緒RunLoop建立的自動釋放池的外層的,意味著程式結束後main函式中的@autoreleasepool
中的autorelease
物件才會釋放。
@autoreleasepool使用規則
在平時的開發中,我們一般是不需要使用@autoreleasepool{}
的,但是以下幾種情況可以使用
- 如果你編寫的程式不是基於 UI 框架的,比如說命令列工具;
- 如果你編寫的迴圈中建立了大量的臨時物件,你可以在迴圈內使用
@autoreleasepool
在下一次迭代之前處理這些物件。在迴圈中使用@autoreleasepool有助於減少應用程式的最大記憶體佔用。 - 如果你建立了輔助執行緒。一旦執行緒開始執行,就必須建立自己的@autoreleasepool;否則,你的應用程式將存在記憶體洩漏。
使用__autorelease
修飾的物件,會被系統自動加入RunLoop建立的自動釋放池中,隨RunLoop生命週期釋放。
總結
- Autoreleasepool目前通過@autoreleasepool{}來建立,可以再適當的時機對物件呼叫
rekease
,保證了物件的延遲釋放 - AutoreleasePoolPage底層結構
- AutoreleasePoolPage是以棧為結點通過雙向連結串列的形式組合而成;遵循先進後出規則,整個自動釋放池由一系列的
AutoreleasePoolPage
組成的,而AutoreleasePoolPage
是以雙向連結串列的形式連線起來。 - 自動釋放池與執行緒一一對應;
- 呼叫
objc_autoreleasePoolPush()
來入棧,呼叫objc_autoreleasePoolPop()
來出棧 - 使用
POOL_BOUNDARY
哨兵物件來作為出入棧的標誌位- 只是nil的別名。前世叫做POOL_SENTINEL,稱為哨兵物件或者邊界物件;
- POOL_BOUNDARY用來區分不同的自動釋放池,以解決自動釋放池巢狀的問題
- 每當建立一個自動釋放池,就會呼叫
push()
方法將一個POOL_BOUNDARY
入棧,並返回其存放的記憶體地址; - 當往自動釋放池中新增
autorelease物件
時,將autorelease物件的記憶體地址入棧,它們前面至少有一個POOL_BOUNDARY
; - 當銷燬一個自動釋放池時,會呼叫
pop()
方法並傳入一個POOL_BOUNDARY
,會從自動釋放池中最後一個物件開始,依次給它們傳送release
訊息,直到遇到這個POOL_BOUNDARY
。
- 每個AutoreleasePoolPage物件佔用
4096
位元組記憶體,其中56個
位元組用來存放它內部的成員變數,剩下的空間(4040個位元組
)用來存放autorelease物件的地址。要注意的是第一頁只有504個物件
,因為在建立page的時候會在next的位置插入1個POOL_BOUNDARY。
- AutoreleasePoolPage是以棧為結點通過雙向連結串列的形式組合而成;遵循先進後出規則,整個自動釋放池由一系列的
- push()原理
- 呼叫了hotPage()獲得新建立的未滿的Page
- 當前 Page 存在且未滿,走
page->add(obj)
,將 autorelease 物件入棧,並將next指標指向這個物件的下一個位置,然後將該物件的位置返回 - 當前 Page 存在但已滿,走
autoreleaseFullPage
,迴圈查詢page->child並判斷是否已滿,都已滿則建立新的AutoreleasePoolPage,並將 autorelease 物件入棧,設定HotPage - 當沒有page時,走
autoreleaseNoPage
,先會判斷是否有空的自動釋放池存在並生成佔位符,然後建立一個新page並設定HotPage,依次插入POOL_BOUNDARY和autorelease 物件入棧
- pop()原理
- 判斷token是不是EMPTY_POOL_PLACEHOLDER,是的話就清空這個自動釋放池
- 如果不是的話,就通過pageForPointer(token)拿到token所在的Page
- 通過page->releaseUntil(stop)將自動釋放池中的autorelease物件全部釋放,傳參stop即為POOL_BOUNDARY的地址
- 判斷當前Page是否有子Page,有的話就銷燬
- @autorelease與RunLoop
-
kCFRunLoopEntry
:在即將進入RunLoop時,會自動建立一個__AtAutoreleasePool結構體物件,並呼叫objc_autoreleasePoolPush()函式。 -
kCFRunLoopBeforeWaiting
:在RunLoop即將休眠時,會自動銷燬一個__AtAutoreleasePool物件,呼叫objc_autoreleasePoolPop()。然後建立一個新的__AtAutoreleasePool物件,並呼叫objc_autoreleasePoolPush()。 -
kCFRunLoopBeforeExit
,在即將退出RunLoop時,會自動銷燬最後一個建立的__AtAutoreleasePool物件,並呼叫objc_autoreleasePoolPop()。
-
參考
iOS - 聊聊 autorelease 和 @autoreleasepool