AutoreleasePool對于iOS開發者來說,可以說是"熟悉的陌生人"。熟悉是因為每個iOS程序都被包圍在一個autoreleasepool中,陌生是因為整個autoreleasepool是黑盒的,開發者看不到autoreleasepool中發生了什么,而且項目開發中直接用到autoreleasepool的地方不多。本文結合Runtime源碼,分析一下AutoreleasePool的內部實現。
iOS程序入口
我們都知道,iOS程序的入口是main.m文件中的main方法。在Xcode中新建一個iOS項目,Xcode會自動生成main.m文件。main.m文件中只有一個main方法,絕大多數情況下,不需要修改main.m中的代碼。
一個典型的main函數:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
可以看到,main函數的函數體是包含在一個autoreleasepool中的??上У氖?,通過command + 鼠標左鍵,并不能看到autoreleasepool的定義。不過我們可以使用clang,將main.m文件編譯成C++代碼,看看autoreleasepool發生了什么。
使用命令:
clang -rewrite-objc main.m
生成main.cpp文件。
生成的main.cpp文件很大,大概有10w行,不需要關注文件到底有多少行。將文件拖到最下面,看一下main函數變成了什么:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_09_mbt6ttpn7_39cpx9j6zg6h440000gp_T_main_f1e080_mi_0);
return 0;
}
}
整個函數的函數體被包圍在了__AtAutoreleasePoool __autoreleasepool中。而且前面有關于@autoreleasepool的注釋,因此可以猜測autoreleasepool被表示成了__AtAutoreleasePool。
在main.cpp中搜索一下,看看__AtAutoreleasePool是什么。
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
__AtAutoreleasePool是一個結構體,結構體中包含構造函數和析構函數。構造函數中調用了
atautoreleasepoolobj = objc_autoreleasePoolPush();
析構函數中調用了
objc_autoreleasePoolPop(atautoreleasepoolobj);
于是,關注的重點就成了objc_autoreleasePoolPush和objc_autoreleasePoolPop函數。
objc_autoreleasePoolPush和objc_autoreleasePoolPop函數在Runtime源碼中可以找到,位于NSObject.mm文件中。
AutoreleasePoolPage
看一下Runtime源碼中objc_autoreleasePoolPush和objc_autoreleasePoolPop函數的實現。
void * objc_autoreleasePoolPush(void)
{
// 調用了AutoreleasePoolPage中的push方法
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
// 調用了AutoreleasePoolPage中的pop方法
AutoreleasePoolPage::pop(ctxt);
}
通過源碼可以看到分別調用了AutoreleasePoolPage的push方法和pop方法。
AutoreleasePoolPage的定義
AutoreleasePoolPage的定義位于NSObject.mm文件中:
// AutoreleasePoolPage的大小是4096字節
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;
// 一個AutoreleasePoolPage中會存儲多個對象
// next指向的是下一個AutoreleasePoolPage中下一個為空的內存地址(新來的對象會存儲到next處)
id *next;
// 保存了當前頁所在的線程(一個AutoreleasePoolPage屬于一個線程,一個線程中可以有多個AutoreleasePoolPage)
pthread_t const thread;
// AutoreleasePoolPage是以雙向鏈表的形式連接
// 前一個節點
AutoreleasePoolPage * const parent;
// 后一個節點
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
除此之外,還定義了很多方法,方法的作用及實現下面會分析。
在上面的定義中,我已經加了些注釋。通過注釋可以得到:
- 一個AutoreleasePoolPage的大小是4096字節(和操作系統中一頁的大小一致)。
- parent指針和child指針特別有意思,指向的同樣是AutoreleasePoolPage,如果對數據結構比較熟悉的話,看到類似的定義,應該可以聯想到雙向鏈表或者樹結構。實際上也正是如此,下面我們會提到AutoreleasePoolPage組成的雙向鏈表。
- thread表示當前AutoreleasePoolPage所屬的線程。
- next指針指向了下一個空的地址。一個AutoreleasePoolPage中可以存儲多個對象地址,新來的對象地址會存放到next處,然后next移動到下一個地址。這樣的操作有沒有聯想到哪種數據結構?是不是和棧的top指針特別類似?
對AutoreleasePoolPage的定義有了基本的了解之后,來看一下push方法和pop方法。
AutoreleasePoolPage::push方法
AutoreleasePoolPage中的push方法,經過簡化之后如下:
static inline void *push()
{
id *dest;
// POOL_BOUNDARY其實就是nil
dest = autoreleaseFast(POOL_BOUNDARY);
return dest;
}
push方法中主要調用了autoreleaseFast方法,所傳入的參數是POOL_BOUNDARY,也就是nil。看你一下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);
}
}
我在代碼中已經加入了注釋,再來看一下里面涉及到的一些方法。
hotPage方法
// 獲取正在使用的AutoreleasePoolPage
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
hotPage可以理解成當前正在使用的page。上面也提到了,AutoreleasePoolPage中有parent和child指針,實際上AutoreleasePool就是由一個個AutoreleasePoolPage組成的雙向鏈表。這里得到的hotPage可以理解成鏈表最末尾的結點。
獲取hotPage的方法是tls_get_direct(key),key是AutoreleasePoolPage結構中定義的
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
setHotPage方法
static inline void setHotPage(AutoreleasePoolPage *page)
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}
將某個page設置成hotPage。
full方法
// 是否已滿
bool full() {
return next == end();
}
判斷當前的AutoreleasePoolPage是否已滿。判斷標準是next等于AutoreleasePoolPage的尾地址。上面已經提到了,AutoreleasePoolPage的大小是4096字節,既然大小是固定的,那么肯定有滿的一刻,full方法就是用來做這個得。
add方法
// 將對象添加到AutoreleasePoolPage中
id *add(id obj)
{
id *ret = next; // faster than `return next-1` because of aliasing
// next = obj; next++;
// 也就是將obj存放在next處,并將next指向下一個位置
*next++ = obj;
return ret;
}
add方法所做的操作也比較簡單,就是將當前對象存放在next指向的位置,并且將next指向下一個位置。可以理解成一個棧,next指針類似于棧的top指針。
autoreleaseFullPage方法
// 新建一個AutoreleasePoolPage,并將obj添加到新的AutoreleasePoolPage中
// 參數page是新AutoreleasePoolPage的父節點
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
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);
}
autoreleaseFullPage所做的操作有三步:
- 首先找到一個合適的AutoreleasePoolPage,這里合適的page指的是不滿的page。具體找的過程是從傳過來的參數page的child開始找,如果page->child存在,則判斷page->child是否是合適的page;如果page->child不存在,則初始化一個新的AutoreleasePoolPage,這里使用的是AutoreleasePoolPage的構造函數,傳入的page是新的AutoreleasePoolPage的父節點。
- 將找到的AutoreleasePoolPage對象設置成hotPage
- 調用add方法,將對象添加到找到的page中
autoreleaseNoPage方法
// AutoreleasePool中還沒有AutoreleasePoolPage
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// 初始化一個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);
}
autoreleaseNoPage方法處理的是當前autoreleasePool中還沒有autoreleasePoolPage的情況。既然沒有,需要新建一個AutoreleasePoolPage,且該page的父指針指向空,然后將該page設置成hotPage。之后向該page中先是添加了POOL_BOUNDARY,然后在把對象obj添加到page中。
關于為什么需要添加POOL_BOUNDARY的原因,后面會說到。
現在已經把autoreleaseFast方法中涉及到的方法都弄明白了,再來看一下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,也就是當前的page,之后分為三種情況:
- 如果hotPage存在,且hotPage還不滿,則將對象添加到hotPage中
- 如果hotPage存在,但是hotPage已滿,則調用autoreleaseFullPage方法
autoreleaseFullPage方法上面已經說了,做的操作就是從page開始找,找到一個不滿的page,將找到的page設置成hotPage,并且將對象添加到找到的page中。 - 如果hotPage不存在,則調用autoreleaseNoPage方法
autoreleaseNoPage方法上面說了,做的操作就是新建一個AutoreleasePoolPage,并且將對象添加到新建的AutoreleasePoolPage中。
至此,AutoreleasePoolPage的push方法介紹完畢。
AutoreleasePoolPage::pop方法
AutoreleasePoolPage::pop方法的代碼經過簡化之后如下:
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
page->releaseUntil(stop);
}
同理,還是先看一下里面調用到的方法的實現。不過,在介紹pop內部調用的方法之前,先來看一下pop方法中的參數到底是什么。
在文章開始處,我們是從clang重寫之后的main.cpp文件引入到Runtime源碼的,現在再回過去看一下main.cpp文件中的__AtAutoreleasePool結構體:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
objc_autoreleasePoolPop中的參數是atautoreleasepoolobj,而atautoreleasepoolobj是objc_autoreleasePoolPush方法返回的。也就是說,AutoreleasePoolPage中pop方法的參數是AutoreleasePoolPage中push方法返回的,比較拗口,可以多理解一下。那么,AutoreleasePoolPage中push方法返回的是什么呢?
上面已經介紹過push方法了,push方法內部分為了三種情況,無論哪種情況,最終都調用了add方法,并且返回了add方法的返回值。add方法的實現如下:
id *add(id obj)
{
id *ret = next; // faster than `return next-1` because of aliasing
// next = obj; next++;
// 也就是將obj存放在next處,并將next指向下一個位置
*next++ = obj;
return ret;
}
add方法返回的就是所要添加對象在AutoreleasePoolPage中的地址。
而在push方法中,添加的對象是哨兵對象POOL_BOUNDARY,所以,在pop方法中,參數也是哨兵對象POOL_BOUNDARY。
pageForPointer方法
pageForPointer的代碼如下:
static AutoreleasePoolPage *pageForPointer(const void *p)
{
// 調用了pageForPointer方法
return pageForPointer((uintptr_t)p);
}
// 根據內存地址,獲取指針所在的AutoreleasePage的首地址
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
// 偏移量
uintptr_t offset = p % SIZE;
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
pageForPointer方法的作用是根據指針位置,找到該指針位于哪個AutoreleasePoolPage,并返回找到的AutoreleasePoolPage(之前已經提到了,AutoreleasePool是由一個個AutoreleasePoolPage組成的雙向鏈表,不止一個AutoreleasePoolPage)。
releaseUntil方法
releaseUntil方法的代碼如下:
// 釋放對象
// 這里需要注意的是,因為AutoreleasePool實際上就是由AutoreleasePoolPage組成的雙向鏈表
// 因此,*stop可能不是在最新的AutoreleasePoolPage中,也就是下面的hotPage,這時需要從hotPage
// 開始,一直釋放,直到stop,中間所經過的所有AutoreleasePoolPage,里面的對象都要釋放
void releaseUntil(id *stop)
{
// 釋放AutoreleasePoolPage中的對象,直到next指向stop
while (this->next != stop) {
// 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);
}
// obj = page->next; page->next--;
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
// POOL_BOUNDARY為nil,是哨兵對象
if (obj != POOL_BOUNDARY) {
// 釋放obj對象
objc_release(obj);
}
}
// 重新設置hotPage
setHotPage(this);
}
代碼中我已經加了注釋,releaseUntil做的操作就是持續釋放AutoreleasePoolPage中的對象,直到next = stop。
回過頭來再來看一下pop方法:
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
page->releaseUntil(stop);
}
pop方法中主要做了兩步:
- 根據token,也就是哨兵對象找到該哨兵對象所處的page
- 從hotPage開始,一直刪除到第一步找到的page.next==stop(哨兵對象)的位置
至此,關于AutoreleasePoolPage以及其中的關鍵方法就全部介紹完畢了。如果到這里,關于AutoreleasePool、AutoreleasePoolPage、哨兵對象還有點蒙的話,不要著急,繼續往下看。
AutoreleasePool和AutoreleasePoolPage的關系
實際上,Runtime中并沒有AutoreleasePool結構的定義,AutoreleasePool是由AutoreleasePoolPage組成的雙向鏈表,如下圖:
在autoreleasepool的開始處,會調用AutoreleasePoolPage的push方法;在autoreleasepool的結束處,會調用AutoreleasePoolPage的pop方法。在AutoreleasePoolPage的push方法中,會往AutoreleasePoolPage中插入哨兵對象,之后的對象依次插入到AutoreleasePoolPage中。如下表示:
AutoreleasePoolPage::push(); // 這里會向AutoreleasePoolPage中插入哨兵對象
*** // 開發者自己寫的代碼,代碼中的對象會依次插入到AutoreleasePoolPage中
***
AutoreleasePoolPage::pop(nil);
當AutoreleasePoolPage滿之后,會新建一個AutoreleasePoolPage,繼續將對象添加到新的AutoreleasePoolPage中。
通過上面的介紹,可以知道,AutoreleasePool由多個AutoreleasePoolPage組成,且AutoreleasePool的開始處必定是一個哨兵對象。到這里,哨兵對象的作用也就清楚了,哨兵對象是用來分隔不同的AutoreleasePool的。
當調用AutoreleasePoolPage::pop(nil)方法時,會從某個autoreleasepool開始,一直釋放到參數哨兵對象所屬的autoreleasepool??梢允峭粋€autoreleasepool,也可以不是同一個autoreleasepool,當不是同一個autoreleasepool時,可以理解成是嵌套autoreleasepool釋放。
到這里,AutoreleasePool、AutoreleasePoolPage、哨兵對象之間的關系應該就理解了。
關于AutoreleasePool的一些面試題
AutoreleasePool在面試中出現的頻率也非常高,接下來分享幾道關于AutoreleasePool的面試題。
AutoreleasePool和線程的關系
確切地說,應該是AutoreleasePoolPage和線程的關系。AutoreleasePool是由AutoreleasePoolPage組成的雙向鏈表,根據AutoreleasePoolPage的定義,每一個AutoreleasePoolPage都屬于一個特定的線程。也就是說,一個線程可以有多個AutoreleasePoolPage,但是一個AutoreleasePoolPage只能屬于一個線程。
AutoreleasePool和Runloop的關系
Runloop,即運行循環。從直觀上看,RunLoop和AutoreleasePool似乎沒什么關系,其實不然。在一個完整的RunLoop中,RunLoop開始的時候,會創建一個AutoreleasePool,在RunLoop運行期間,autorelease對象會加入到自動釋放池中。在RunLoop結束之前,AutoreleasePool會被銷毀,也就是調用AutoreleasePoolPage::pop方法,在該方法中,自動釋放池中的所有對象會收到release消息。正常情況下,AutoreleasePool中的對象發送完release消息后,引用計數應該為0,會被釋放,如果引用計數不為0,則發生了內存泄露。
AutoreleasePool中對象什么時候釋放?
其實上面已經說過了,AutoreleasePool銷毀時,AutoreleasePool中的所有對象都會發送release消息,對象會釋放。那么,AutoreleasePool什么時候銷毀呢?分兩種情況:
- 一種情況就是上面提到的,當前RunLoop結束之前,AutoreleasePool會銷毀。這種情況適用于系統自動生成的AutoreleasePool。
- 第二種情況是開發者自己寫的AutoreleasePool,常見于for循環中,將循環體包在一個AutoreleasePool中。這種情況下,在AutoreleasePool作用域之后(也就是大括號),AutoreleasePool會銷毀。