背景
自從蘋果推出了ARC管理內存后,對于iOS開發這而言,內存管理就變得so easy了,只要正確使用相關規則,再也不用擔心double release,野指針的等問題了,而ARC的背后,除了強大的編譯器之外,還要得益于運行時起作用的AutoReleasePool。
研究AutoReleasePool
iOS的項目中,除了特別需求外,整個項目就一個地方明確寫了autoReleasePool的代碼了,就是main函數:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
autoreleasepool做了什么?
我們知道oc代碼在編譯期間都會轉化為c/c++代碼,然后轉化為匯編,最終轉化為對應的架構的二進制文件;也可以這么說,oc的底層實現就是c/c++,既然這樣,我們把他轉化為對應的c/c++代碼應該就可以窺探到其中的密碼了:
轉化為c/c++代碼,Xcode有自帶的工具,打開命令行,輸入一下命令就可以:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
為了減少代碼量,重新建了一個macOS命令行項目:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
轉化為cpp文件,看下對應的代碼:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_k5_h0x40m15075dxn_z956dk2500000gn_T_main_6e2ecd_mi_0);
}
return 0;
}
從上面的C++源碼可以發現@autoreleasepool {}最后變成了:
{ __AtAutoreleasePool __autoreleasepool;//定義了一個__AtAutoreleasePool結構體變量
。。。
}
我們分析下流程:
1、進入大括號,定義了一個__AtAutoreleasePool的結構體局部變量;
2、定義這個結構體變量的時候,會走結構體的構造方法,間接的會調用objc_autoreleasePoolPush函數:
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
3、當走出大括號是,局部變量__autoreleasepool,會被銷毀,因此會走結構體的析構函數,間接就會調用objc_autoreleasePoolPop函數:
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
從上面的分析,我們發現了兩個重要的函數objc_autoreleasePoolPush和objc_autoreleasePoolPop,這兩個函數是全局函數,而且是以objc開頭的,應該是在objc的源碼中,下載objc源碼的地址,macOS 最新系統下面的objc4。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
他們調用的是AutoreleasePoolPage類對應的push跟pop兩個靜態函數,那么我們就要研究下AutoreleasePoolPage這個類了。
研究AutoreleasePoolPage
class AutoreleasePoolPage :
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
...
}
從AutoreleasePoolPage的成員變量可以分析出,AutoreleasePoolPage是一個雙向鏈表的結構,每個實例都會存在一個parent實例指針,跟一個child實例指針。其他成員變量暫時不知道表示什么意思,只能繼續研究AutoreleasePoolPage的實現邏輯了,還是從push跟pop函數入手:
AutoreleasePoolPage 的push函數:
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page .debug模式新建一個page對象,實際不需要關注
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {//實際走這里
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
追根溯源autoreleaseFast:
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
//拿到當前正在被使用的page,因為每個page都是有對象obj數量限
//制的,當page放滿了,就會創建一個child page來繼續放。
if (page && !page->full()) {//沒滿,直接添加到當前的page上
return page->add(obj);//添加obj
} else if (page) {//full,滿了
return autoreleaseFullPage(obj, page);//將obj添加到對應的未滿的child page里面,并將其設置為hot page
} else {//沒有page
return autoreleaseNoPage(obj);
}
}
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;
}
static __attribute__((noinline))
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;//若有child page,將當前的指針指向child page
else page = new AutoreleasePoolPage(page);//new一個新的page,并將其賦值給child page;
} while (page->full());//page是否滿了,沒滿,跳出
setHotPage(page);//將page設置為當前hotpage
return page->add(obj);//添加obj到page
}
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",
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();//設置一個占位的空的page
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.第一次創建一個page
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);//并將它設置為hotpage
// 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);//添加obj
}
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));//page的起始地址+成員變量的大小
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);///一個page的大小是size,4096字節
}
bool empty() {
return next == begin();
}
bool full() {
return next == end();
}
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;//將obj的指針賦值給next所在的位置,然后將next指向下一個位置
protect();
return ret;//返回前一個obj
}
總結一下:
- push操作從取出當前的hotpage,然后將一個哨兵對象(其實是nil)放入到page的next位置,并將next指向的位置向下+1;
- 取出的hotpage存在,但是hotpage是一個fullpage(一個page,PAGE_MAX_SIZE = 4096字節大小,除了存放內部成員變量的值之外,其他的都用來存放autorelease對象的地址),這時就會循環查找他的child指向的page對象,知道找到沒使用完的child page,如果沒有child page,則創建一個,將找到的child page并設置為hotpage,然后將一個哨兵對象添加進去;
- 取出的當前hotpage不存在,則通過autoreleaseNoPage創建一個新的page,并設置為hotpage,然后將然后將一個哨兵對象添加進去;
- 這樣做的結果,除了第一層page(沒有parent page),每次push返回的obj的都是哨兵對象,最開始的push返回的是第一層page最開始的位置page.begin(),后面的pop會用到這個返回值。
AutoreleasePoolPage 的pop函數:
static inline void pop(void *token)
{//token就是對應push返回值,上面提到過要么是哨兵對象,要么是第一層page最開始的位置
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) {
//要么是第一層page最開始的位置
// 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 {//其他情況不存在,壞的page
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);//釋放存放在page的指針所指的對象,直到遇到哨兵對象或者全部釋放完成
// 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) {//將page對象釋放掉
// 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();
}
}
}
void releaseUntil(id *stop)
{
// Not recursive: we don't 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 can't prove it
while (page->empty()) {//當前page釋放完,還沒遇到哨兵對象,拿到parent page,并設置為hotpage,繼續釋放,直到遇到哨兵對象或者全部釋放完
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;//拿到obj對象,并將next指針指向上一個位置
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));//清空next位置,這里是設置為0x3A
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);//釋放掉對象
}
}
setHotPage(this);//釋放完后,將當前page設置為hotpage
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
pop函數總結幾點:
- 會拿到最近一次push函數(pop函數與push一一對應)返回的哨兵對象,作為pop函數的入參;
- 遍歷hotpage的next指針指向的對象,并釋放,直到遇到入參的哨兵對象;
- 如果當前page釋放完了,還沒遇到哨兵對象,就會往parent page遍歷,直到遇到哨兵對象,一次類推;
- 最后釋放掉為空(empty)的page對象,但是需要注意的是:當他的parent的使用空間超過了1/2,保留它對應的child page。
從上面的分析大致了解了AutoreleasePoolPage的工作流程,在程序運行的時候是怎么樣工作的呢?
我們知道,在MRC時代,需要程序員手寫對象的retain和release,后面最智能的就是new一個對象的時候,我們需要帶上autorelease代碼:
[[[NSObject alloc] init] autorelease];//MRC手動管理內存
到了ARC,我們不需要這樣寫,因為編譯器在編譯的時候,會自動幫忙加上這些代碼,所以說不管是ARC還是MRC時期,oc對象的內存管理入口都是autorelease方法:
autorelease方法的研究:
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
inline id
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;//tagPointer 不需要
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);//調用的是AutoreleasePoolPage的autorelease函數
}
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);//調用autoreleaseFast,上面提到過
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
就是說,每個oc對象創建的時候(alloc/new/copy/mutableCopy),都是通過autorelease方法添加到page對象中的。pop那具體的調用時機又是什么呢?我們知道,項目只在main函數有一個autoreleasepool,其他的地方除非程序員自己手動添加,就不會有了,也就是說:我們暫且認為編譯器幫忙添加的autorelease,那也只是添加進page,pop函數還是只有一個,而且是程序退出的時候調用,如果是這種情況的話,程序整個運行期間,內存得不停的增長,因為只有申請,沒有釋放。顯然這種做法是行不通的 。
RunLoop& AutoReleasePool關系幾點說明:
- App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。
- 第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。
- 第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之后。
總結
通過上面的源碼及流程的分析,我們對于autoreleasepool的工作原理及流程有了充分的了解:
-
autoreleasepool底層的數據結構是一個autoreleasepage的雙向鏈表,每個page的大小為4096字節,除了存儲成員變量的大小,其他的位置都用來存儲autorelease對象的地址,next變量永遠指向下一個可以存放autorelease對象地址的地址空間,具體結構如下:數據結構
- 當一個page1存儲空間用完后,會創建一個新的page2,新的page2的parent指針指向滿了的page1,page1的child指針會指向page2;
- 程序啟動就會創建一個autoreleasepool,會調用push,程序結束時,最后會調用pop,回收所有autorelease對象的內存;
- 程序運行期間,同過監聽runloop的休眠狀態,調用push/pop方法,管理autorelease對象;
- 每次push的時候,會往page里面添加一個哨兵對象,這個哨兵對象作為下次pop函數的入參,遇到哨兵對象,說明這次runloop循環添加到page的autorelease對象release完畢。