前言
在內存管理的學習中自動釋放池的原理學習是必須的,作為一個合格的iOS開發者,必須要明白自動釋放池的操作原理,這篇文章的目的就是探索自動釋放池的底層原理。
準備工作
1. 自動釋放池
1.1 相關概念
- 如果在函數、方法的開始處將對象的引用計數
加1
,在函數、方法不需要該對象的時候將其引用計數減1
,這思想基本OK
。 - 有些函數、方法需要返回一個對象,而系統可能在該對象被返回之前,就已經銷毀了對象。那么為了保證函數、方法返回的對象在被返回之前不被銷毀,我們就要使用自動釋放池進行
延遲銷毀
(NSAutoreleasePool
)。 - 所謂
自動釋放池
,是指它是一個存放對象的容器
(集合
),而自動釋放池
會保證延遲銷毀該池中所有的對象
。出于自動釋放池
的考慮,所有的對象都應該添加到自動釋放池
中,這樣可以讓自動釋放池
在銷毀之前,先銷毀池中的所有對象。 -
autorelease
方法。該方法不會改變對象的引用計數
,只是將該對象添加到自動釋放池
中。該方法會返回調用該方法的對象本身
。 - 當程序在
自動釋放池
上下文中調用某個對象的autorelease
方法時,該方法只是將對象添加到自動釋放池
中,當該自動釋放池
釋放時,自動釋放池
會讓池中所有的對象執行release
方法。 -
自動釋放池的銷毀
和其他普通對象相同,只要其引用計數為0
,系統就會自動銷毀自動釋放池對象
。系統會在調用NSAotoreleasePool
的dealloc
方法時回收該池中的所有對象
。 -
NSAutoreleasePool
還提供了一個drain
方法來銷毀自動釋放池
中的對象。與release
不同,release
會使自動釋放池自身的引用計數變為0
,從而讓系統回收NSAutoreleasePool
對象,在回收NSAutoreleasePool
對象之前,系統會回收該池中的所有對象。而drain
方法則只是回收池中的所有對象,并不會銷毀自動釋放池
。
1.2 運行邏輯
AutoReleasePool
是OC
的內存自動回收機制
,將加入到AutoReleasePool
中的變量release
時機延遲
。在正常情況下,創建的變量會在超出其作用域的時候release
,但是如果將變量加入AutoreleasePool
,那么release
將延遲執行,即使超出作用域也不會立即釋放
,直到runloop休眠
或者超出AutoReleasePool作用域才會釋放
。
自動釋放池的運行機制:
- 程序啟動到加載完成,主線程對應的
Runloop
處于休眠狀態
,直到用戶點擊交互喚醒Runloop
- 用戶每次交互都會啟動一次
Runloop
用來處理用戶的點擊、交互事件 -
Runloop
被喚醒后,會自動創建AutoReleasePool
,并將所有延遲釋放的對象添加到AutoReleasePool
- 在一次完整的
Runloop
執行結束前,會自動向AutoReleasePool
中的對象發送release
消息,然后銷毀AutoReleasePool
注意:AutoreleasePool
和Runloop
的運行機制和關系,在后面講解Runloop
時會詳細說明。
1.3 使用效果分析
下面通過一個案例來說明自動釋放池
的作用。我們常用以下兩種方式創建字符串
:
// 方式1
NSString * string1 = [[NSString alloc] initWithFormat:@"hello world..."];
// 方式2
NSString * string2 = [NSString stringWithFormat:@"hello world auto relase..."];
那么以上兩種方式有什么不同呢?還是老規矩,查看匯編!!!
- 方式1
NSString * string1 = [[NSString alloc] initWithFormat:@"hello world..."];
打開匯編斷點查看流程如下:
這個流程有點熟悉,就是標準的
對象創建
過程嘛。使用alloc
出來的方式,字符串在調用release
的時候被回收(假設該字符串沒有被其他對象引用
,變量會在超出其作用域的時候release
)。
- 方式2
NSString * string2 = [NSString stringWithFormat:@"hello world auto relase..."];
打開匯編斷點查看流程如下:
使用
stringWith
的方式,字符串在api
內部會被設置成autorelease
,不用手動釋放,系統會回收
,因此將會在最近的
一個自動釋放池drain
或release
時被回收。
下面通過一個案例來深入了解自動釋放池的作用。案例中,使用兩種方式創建了字符串,并且把字符串賦值給__weak
修飾的成員變量。
- 案例1
__weak NSString *weakSrting;
__weak NSString *weakSrtingAutoRelease;
@implementation ViewController
- (void)createStringFunc {
// 方式1
NSString * string1 = [[NSString alloc] initWithFormat:@"hello world..."];
weakSrting = string1;
// 方式2
NSString * string2 = [NSString stringWithFormat:@"hello world auto relase..."];
weakSrtingAutoRelease = string2;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self createStringFunc];
NSLog(@"weakSrting: %@", weakSrting);
NSLog(@"weakSrtingAutoRelease: %@", weakSrtingAutoRelease);
}
- (void)viewWillAppear:(BOOL)animated
{
NSLog(@"view will appear weakSrting: %@", weakSrting);
NSLog(@"view will appear weakSrtingAutoRelease: %@", weakSrtingAutoRelease);
}
- (void) viewDidAppear:(BOOL)animated
{
NSLog(@"view did appear weakSrting: %@", weakSrting);
NSLog(@"view did appear weakSrtingAutoRelease: %@", weakSrtingAutoRelease);
}
查看運行結果,如下:
結果分析如下:
- 使用
方式1
創建的字符串weakSrting
,在createStringFunc
方法執行完成后就會釋放
(作用域結束
),弱引用weakSrting
也會釋放掉。所以weakSrting
打印結果都是空。 - 使用
方式2
創建的對象weakSrtingAutoRelease
,這個對象被系統自動添加
到了當前的autoreleasepool
中,起到了延遲釋放
的效果。這個對象是一個autoreleased
對象,autoreleased
對象是被添加到了當前最近的autoreleasepool
中,只有當這個autoreleasepool
自身drain
的時候,autoreleasepool
中的autoreleased
對象才會被release
。
對象weakSrtingAutoRelease
,在viewWillAppear
中打印這個對象的時候,能夠輸出,說明此時對象還沒有被釋放。但是在viewDidAppear
中打印這個對象的時候就成了null
了,那么這個對象一定是在viewWillAppear
和viewDidAppear
方法之間的某個時候被釋放了,并且是由于它所在的autoreleasepool
被release
的時候釋放的。我們可以在lldb
調試中設置觀察點
(watchpoint set v weakSrtingAutoRelease
),來查看對象的釋放過程,如下:
在運行棧中可以發現,
weakSrtingAutoRelease
對象在自動釋放池釋放時
完成了釋放
。
-
案例2
案例1看起來不夠直接,那么我們來一個直接點的案例。代碼中手動添加了一個@autoreleasepool
,在自動釋放池內,weakSrtingAutoRelease
一直不會釋放,而出了自動釋放池就會釋放。如下:
案例2
明顯看出使用方式2
創建的對象weakSrtingAutoRelease
在自動釋放池內都能夠正常使用,出了自動釋放池就會被釋放,起到延遲釋放
的效果。
但是使用方式1
創建的字符串weakSrting
,為什么在自動釋放池內就釋放了呢?他不會加入到自動釋放池嗎?帶著疑問繼續往下走!!
2. 自動釋放池原理分析
2.1 原理初探
通過clang
查看自動釋放池的實現原理,如下:
@autoreleasepool
在編譯后變成了以下代碼:
__AtAutoreleasePool __autoreleasepool;
main.cpp文件中全局搜索__AtAutoreleasePool
的定義,找到__AtAutoreleasePool
結構體的定義,如下:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
該結構體提供了一個構造函數objc_autoreleasePoolPush
和一個析構函數objc_autoreleasePoolPop
。所以自動釋放池在底層其實是一個結構體
,其通過objc_autoreleasePoolPush
完成自動釋放池的創建
,objc_autoreleasePoolPop
來釋放自動釋放池
。
設置objc_autoreleasePoolPush
的符號斷點,的確能夠進入,匯編如下:
可以確定自動釋放池其實現源碼在我們最熟悉的
libobjc.A.dylib
庫。那就非常的nice
了!
2.2 結構分析
下面通過源碼進行分析。跟蹤objc_autoreleasePoolPush
的方法實現,如下:
其調用了
objc_autoreleasePoolPush()
方法,繼續跟蹤代碼:在該方法的實現中,其調用了
AutoreleasePoolPage
的push
方法。那么AutoreleasePoolPage
的結構是怎么的呢?如下:通過
AutoreleasePoolPage
類的注釋可以得到以下關鍵信息:
-
一個線程
的自動釋放池是一堆指針
- 每個指針要么是一個
要釋放
的對象,要么是POOL_BOUNDARY
(自動釋放池邊界-哨兵對象
) -
堆棧
被分成一個雙向鏈接
的頁面列表
, 頁面已添加對象并根據需要刪除 - 線程本地存儲指向新自動釋放的熱點頁面對象被存儲
AutoreleasePoolPage
繼承于AutoreleasePoolPageData
,那么查看AutoreleasePoolPageData
的結構如下:
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
struct AutoreleasePoolEntry {
uintptr_t ptr: 48;
uintptr_t count: 16;
static const uintptr_t maxCount = 65535; // 2^16 - 1
};
static_assert((AutoreleasePoolEntry){ .ptr = MACH_VM_MAX_ADDRESS }.ptr == MACH_VM_MAX_ADDRESS, "MACH_VM_MAX_ADDRESS doesn't fit into AutoreleasePoolEntry::ptr!");
#endif
magic_t const magic; // 16
__unsafe_unretained id *next; // 8
pthread_t const thread; // 8
AutoreleasePoolPage * const parent; // 8
AutoreleasePoolPage *child; // 8
uint32_t const depth; // 4
uint32_t hiwat; // 4
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)
{
}
};
屬性相關說明:
-
magic
?來校驗AutoreleasePoolPage
的結構是否完整
-
next
指向最新添加的autoreleased
對象的下?個位置
,初始化時指向begin()
-
thread
指向當前線程 -
parent
指向?結點,第?個結點的parent
值為nil
-
child
指向?結點,最后?個結點的child
值為nil
-
depth
代表深度,從0
開始,往后遞增1
-
hiwat
代表high water mark
最??棧數量
標記
2.3 源碼實現
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::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;
}
在非debug
模式下首先調用autoreleaseFast
方法,并傳入邊界對象
(哨兵對象
)。查看autoreleaseFast
實現源碼,如下:
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);
}
}
- 首先獲取當前
hotPage
,如果不為空且沒有滿
,則會向該頁中添加obj
; - 如果該頁
已滿
,則調用autoreleaseFullPage
方法; - 如果當前
hotPage不存在
,也就是沒有page
,則調用autoreleaseNoPage
方法。autoreleaseNoPage
實現源碼如下:
autoreleaseNoPage
在完成AutoreleasePoolPage
創建后,首先添加哨兵對象
,然后在加入obj
。 首先查看AutoreleasePoolPage
構造函數,如下:
AutoreleasePoolPage
通過調用AutoreleasePoolPageData
的構造函數實現初始化
,并確定頁之間的鏈表關系
。通過上面的結構我們可以確定AutoreleasePoolPageData
屬性占56
個字節。見下圖:
AutoreleasePoolPageData屬性
因為頁中next
字段用于設置存儲obj
的位置,那么因為每個頁自身有一些屬性需要占用一部分空間,所以next
的起始值是page
首地址平移56
個字節,也就是構造函數中begin()
方法所確定下來的值。
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
斷點調試如下:
如果
頁滿
時,就會調用上面的autoreleaseFullPage
方法,見下面實現源碼:
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);
}
do..while
循環找到最后一個page
,如果page
沒有滿,就將page
設置為hotPage
。如果page
已滿,則會新建一個page
,也將page
設置為hotPage
。最后往page
中添加obj
。
綜合上面的數據結構和源碼實現,我們可以得出以下結論:
-
Autoreleasepool
是由多個AutoreleasePoolPage
以雙向鏈表
的形式連接起來的 -
Autoreleasepool
的基本原理:在自動釋放池創建的時候,會在當前的AutoreleasePoolPage
中設置一個標記位
(邊界
),在此期間,當有對象調用autorelease
時,會把對象添加到AutoreleasePoolPage
中 - 如果當前頁加
滿了
,會初始化一個新頁,然后用雙向鏈表鏈接起來,并把初始化的一頁設置為hotPage
,當自動釋放池pop
時,從最下面依次往上pop
,調用每個對象的release
方法,直到遇到標志位
2.4 滿頁臨界值
自動釋放池一頁能夠存儲多少個對象呢?如果能夠打印輸出自動釋放池的數據,會更便于我們對自動釋放池的了解。在源碼中也提供了相關的打印數據結構的方法,如下:
void
_objc_autoreleasePoolPrint(void)
{
AutoreleasePoolPage::printAll();
}
printAll()
方法如下:
__attribute__((noinline, cold))
static void printAll()
{
_objc_inform("##############");
_objc_inform("AUTORELEASE POOLS for thread %p", objc_thread_self());
AutoreleasePoolPage *page;
ptrdiff_t objects = 0;
for (page = coldPage(); page; page = page->child) {
objects += page->next - page->begin();
}
_objc_inform("%llu releases pending.", (unsigned long long)objects);
if (haveEmptyPoolPlaceholder()) {
_objc_inform("[%p] ................ PAGE (placeholder)",
EMPTY_POOL_PLACEHOLDER);
_objc_inform("[%p] ################ POOL (placeholder)",
EMPTY_POOL_PLACEHOLDER);
}
else {
for (page = coldPage(); page; page = page->child) {
page->print();
}
}
_objc_inform("##############");
}
創建一個案例查看其內部存儲結構,如下:
通過上面的輸出可以發現,該自動釋放池的起始頁是
0x10380a000
,地址平移56
個字節后放入的是哨兵對象
,哨兵對象地址為0x10380a038
,緊接著放入4
個對象。那么一頁能放多少呢?源碼中也有定義,如下:
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
#define PAGE_MIN_SHIFT 12
#define PAGE_MIN_SIZE (1 << PAGE_MIN_SHIFT)
通過以上的源碼定義發現其大小為1<<12
,也即是4096
,而每頁自身屬性的占用56
個字節,同時第一頁需要一個哨兵對象8
個字節,所以首頁最多可以放(4096 - 56 - 8) / 8 = 504
個對象。驗證一下:
通過輸出自動釋放池的數據結構可以發現,當放入
505
個對象時,會新開辟一頁,并且第二頁中只有一個對象。(哨兵對象只會放在第一頁
)所以第一頁最多可以放504
個對象,之后每頁可以存儲505
個對象。
3. 自動釋放池注意點
3.1 對象release而非銷毀
引入案例,如下:
當自動釋放池結束的時候,僅僅是對存儲在自動釋放池中的對象發送
1
條release
消息,而不是銷毀對象
。
3.2 自動釋放池的嵌套
引入案例,如下:
通過該案例可以發現,自動釋放池嵌套并不會影響數據結構,只是
多插入一個哨兵對象
。
3.3 哪些對象可以放入自動釋放池
引入案例,如下:
-
MRC
環境
MRC環境 -
ARC
環境
ARC環境
總結: - 主動調用
autorelase
方法的,用alloc
,init
,copy
等方法創建的對象,這些我們自己持有的,我們想讓他延遲釋放,就調用autorelase
方法,這樣在自動釋放池出棧
的時候,對象就會釋放掉
。 - 對于那種
stringWithFormt
這種從名字來看,沒有被調用者持有的情況,要么是自動加到自動釋放池里
的,要么是常量字符串
,不用引用計數來管理
。