原文連接git
AutoreleasePool對於iOS開發者來講,能夠說是"熟悉的陌生人"。熟悉是由於每一個iOS程序都被包圍在一個autoreleasepool中,陌生是由於整個autoreleasepool是黑盒的,開發者看不到autoreleasepool中發生了什麼,並且項目開發中直接用到autoreleasepool的地方很少。本文結合Runtime源碼,分析一下AutoreleasePool的內部實現。github
咱們都知道,iOS程序的入口是main.m文件中的main方法。在Xcode中新建一個iOS項目,Xcode會自動生成main.m文件。main.m文件中只有一個main方法,絕大多數狀況下,不須要修改main.m中的代碼。面試
一個典型的main函數:bash
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文件。oop
生成的main.cpp文件很大,大概有10w行,不須要關注文件到底有多少行。將文件拖到最下面,看一下main函數變成了什麼:ui
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。this
在main.cpp中搜索一下,看看__AtAutoreleasePool是什麼。spa
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文件中。
看一下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的定義位於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的定義有了基本的瞭解以後,來看一下push方法和pop方法。
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);
}
}
複製代碼
我在代碼中已經加入了註釋,再來看一下里面涉及到的一些方法。
// 獲取正在使用的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;
複製代碼
static inline void setHotPage(AutoreleasePoolPage *page)
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}
複製代碼
將某個page設置成hotPage。
// 是否已滿
bool full() {
return next == end();
}
複製代碼
判斷當前的AutoreleasePoolPage是否已滿。判斷標準是next等於AutoreleasePoolPage的尾地址。上面已經提到了,AutoreleasePoolPage的大小是4096字節,既然大小是固定的,那麼確定有滿的一刻,full方法就是用來作這個得。
// 將對象添加到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指針。
// 新建一個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所作的操做有三步:
// 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,以後分爲三種狀況:
至此,AutoreleasePoolPage的push方法介紹完畢。
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的代碼以下:
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方法的代碼以下:
// 釋放對象
// 這裏須要注意的是,由於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方法中主要作了兩步:
至此,關於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的面試題。
確切地說,應該是AutoreleasePoolPage和線程的關係。AutoreleasePool是由AutoreleasePoolPage組成的雙向鏈表,根據AutoreleasePoolPage的定義,每個AutoreleasePoolPage都屬於一個特定的線程。也就是說,一個線程能夠有多個AutoreleasePoolPage,可是一個AutoreleasePoolPage只能屬於一個線程。
Runloop,即運行循環。從直觀上看,RunLoop和AutoreleasePool彷佛沒什麼關係,其實否則。在一個完整的RunLoop中,RunLoop開始的時候,會建立一個AutoreleasePool,在RunLoop運行期間,autorelease對象會加入到自動釋放池中。在RunLoop結束以前,AutoreleasePool會被銷燬,也就是調用AutoreleasePoolPage::pop方法,在該方法中,自動釋放池中的全部對象會收到release消息。正常狀況下,AutoreleasePool中的對象發送完release消息後,引用計數應該爲0,會被釋放,若是引用計數不爲0,則發生了內存泄露。
其實上面已經說過了,AutoreleasePool銷燬時,AutoreleasePool中的全部對象都會發送release消息,對象會釋放。那麼,AutoreleasePool何時銷燬呢?分兩種狀況: