iOS 自動釋放池autoreleasepool(一)
前言
在前面幾篇文章,說了關於OC中的記憶體佈局、記憶體管理方案、以及MRC情況下的retainCount、retain、release,但是MRC也已經是過去式了,這次來說說ARC。 從MRC到ARC的變化 就取決於@autoreleasepool。
@autoreleasepool 自動釋放池: 管理記憶體的池,把不需要的物件放在自動釋放池中,自動釋放(延遲釋放)這個池子內的物件。
@autoreleasepool的應用場景:
- 存在大量臨時變數的時候
- 非UI操作,如:命令列
- 自己建立輔助執行緒
從哪開始
AutoreleasePool建立和釋放
我們可以在工程中隨便找個地方打個斷點
po [NSRunLoop currentRunLoop]
- App啟動後,蘋果在主執行緒 RunLoop 裡註冊了兩個 Observer,其回撥都是 _wrapRunLoopWithAutoreleasePoolHandler()。
- 第一個 Observer 監視的事件是 Entry(即將進入Loop),其回撥內會呼叫 _objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先順序最高,保證建立釋放池發生在其他所有回撥之前。
- 第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時呼叫_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並建立新池;Exit(即將退出Loop) 時呼叫 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先順序最低,保證其釋放池子發生在其他所有回撥之後。
- 在主執行緒執行的程式碼,通常是寫在諸如事件回撥、Timer回撥內的。這些回撥會被 RunLoop 建立好的 AutoreleasePool 環繞著,所以不會出現記憶體洩漏,開發者也不必顯示建立 Pool 了。
也就是說AutoreleasePool建立是在一個RunLoop事件開始之前(push),AutoreleasePool釋放是在一個RunLoop事件即將結束之前(pop)。 AutoreleasePool裡的Autorelease物件的加入是在RunLoop事件中,AutoreleasePool裡的Autorelease物件的釋放是在AutoreleasePool釋放時。 以上內容參考自這位老哥
檢視@autoreleasepool{ }編譯成C++程式碼
使用編譯器clang編譯main.m轉化成main.cpp檔案(在終端:clang -rewrite-objc main.m)
$ cd main.m所在資料夾
$ clang -rewrite-objc main.m -o main.cpp
會在原有路徑下生成一個main.cpp,點開之後我們可以找到如下程式碼:
可知:main函式在c++中被編譯成了上圖中的模樣,核心重點一看就知道是這個__AtAutoreleasePool
,在當前程式碼直接搜尋,很容易就能找到下面這些程式碼
注意:這裡的~
是C++的解構函式(destructor) 與建構函式相反,當物件脫離其作用域時(例如物件所在的函式已呼叫完畢),系統自動執行解構函式。
從上圖我們可以找到兩個東西,從名字也很淺顯,一個進池子一個出池子objc_autoreleasePoolPush
objc_autoreleasePoolPop
結合上面的注意點,我覺得可以先這麼總結一下
@autoreleasepool,就是把在它作用域(就是"{}")中的程式碼,先push進去,然後等這些程式碼都幹完活了,再把他們pop出去。
走進原始碼
我們先看一下這個 objc_autoreleasePoolPush
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
複製程式碼
這裡出現了一個AutoreleasePoolPage,且語法是C++的語法。我們這裡先看看這個AutoreleasePoolPage是何方神聖。
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
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // 4096位元組 size and alignment,power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic; // 4*4位元組 用來校驗 AutoreleasePoolPage 的結構是否完整;
id *next; // 8位元組 指向最新新增的 autoreleased 物件的下一個位置,初始化時指向 begin() ;
pthread_t const thread; // 8位元組 指向當前執行緒;
AutoreleasePoolPage * const parent; // 8位元組,指向父結點,第一個結點的 parent 值為 nil ;雙向連結串列上一個節點
AutoreleasePoolPage *child; //8位元組,指向子結點,最後一個結點的 child 值為 nil ;雙向連結串列下一個節點
uint32_t const depth; // 4位元組,代表深度,從 0 開始,往後遞增 1;
uint32_t hiwat; // 4位元組 , 代表 high water mark 。
//這幾個加起來是56位元組
...
}
複製程式碼
由上圖可以看到AutoreleasePoolPage的大小為4096位元組,其中自身變數佔用56個位元組。
這裡可以來驗證一下。
這裡先介紹一下這個函式,這個可以列印當前AutoreleasePool中Page的狀態
void
_objc_autoreleasePoolPrint(void);
複製程式碼
已知AutoreleasePoolPage size = 4096,自身佔用56,那麼剩餘空間為4096-56=4040。 一個NSObject的大小為8位元組,就是說一個AutoreleasePoolPage的剩餘空間還可以容納4040/8=505個NSObject。 所以就來一波這種操作
void
_objc_autoreleasePoolPrint(void);
int main(int argc,const char * argv[]) {
@autoreleasepool {
for (int i = 0 ; i<505; i++) {
NSObject *obj = [[NSObject new]autorelease];
}
_objc_autoreleasePoolPrint();
}
return 0;
}
複製程式碼
然後看一下列印資訊
中間省略幾百個NSObject...
可以看到上面兩張圖合起來的話,一共出現了2個PAGE。- 第一個PAGE標記了(full)(cold)第一個詞很容易理解,就是滿了,第二個詞大意就是“涼了”可以理解成不活躍了。
- 第二個PAGE標記了(hot)就是(cold)的反義詞咯,那就理解成活躍的意思。 總而言之,就是第一個PAGE滿了且被標記成非活躍狀態,第二個PAGE沒滿且是活躍狀態。
那麼問題來了,按照我們先前的計算,505個不是應該剛剛好裝滿一個PAGE嗎?這第二頁是什麼鬼?明顯就是多出了一個什麼奇怪的東西。 我們觀察一下這三個地址
objc[32613]: [0x102002000] ................ PAGE (full) (cold) objc[32613]: [0x102002038] ################ POOL 0x102002038 objc[32613]: [0x102002040] 0x100f8d780 NSObject
前兩個地址的區別就在於一個38,這裡是16進位制, 所以38在十進位制中為 3*16+8 = 56,所以這一系列的#號為變數進入pool的起點 第三個地址就是從38變成了40,38要變成40,在16機制中就是需要38中的8再加上8,讓3變成4。 所以就是這 0x102002038 裡面的東西在作怪。那這個又是什麼呢? 這裡我先貼一下部分程式碼
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());
// 翻譯:pushExtraBoundary 推入一個額外邊界
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
//當push一個新頁或者第一頁的時候,在push之前需要先push一個poolboundary(邊界)
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// 異常情況,拋錯
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",pthread_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();
}
// 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);
}
複製程式碼
這裡解讀一下這段程式碼 當我們push一個物件進來時,沒有page或者page滿了,需要到新的一頁的時候,他就會將pushExtraBoundary設定為true,在底下會判斷pushExtraBoundary,如果為true,就會先push一個POOL_BOUNDARY進入page中。
所以這個POOL_BOUNDARY就是我們上文中的“奇怪的東西”,從字面翻譯我們可以理解為這是一個“邊界”或者叫“邊界符”
總結:
- autoreleasepool由許許多多的AutoreleasePoolPage組成
- 當一個AutoreleasePoolPage裝滿之後,就會建立新的AutoreleasePoolPage,兩個Page之間用parent/child互相關聯,從而證明雙向連結串列的說法
- 在AutoreleasePoolPage自身變數的56個位元組之後,當push物件進page時,會先push一個邊界符進去POOL_BOUNDARY。這個邊界符也佔8個位元組
這裡給一張圖,可以更好的理解,加深印象
這篇文章先到這裡,下一篇autoreleasepool將會解析push跟pop