本文是經(jīng)過翻閱博客、論壇學(xué)習(xí)以及代碼調(diào)試的總結(jié),如有疑惑或不準確的地方,歡迎評論溝通指正。
通讀本文你將理解:
- 自動釋放池底層結(jié)構(gòu)
- 自動釋放池何時釋放(換言之a(chǎn)utorelease何時執(zhí)行release操作)
- ARC下什么樣的初始化方法系統(tǒng)會為我們做一次autorelease操作
Objc源碼下載地址https://opensource.apple.com/tarballs/objc4/
一、自動釋放池底層結(jié)構(gòu)
總結(jié)一句話就是:以棧為節(jié)點,以雙向鏈表形式組合而成的一個數(shù)據(jù)結(jié)構(gòu)。通俗一些講自動釋放池是以多個AutoreleasePoolPage為結(jié)點,通過鏈表的方式串連起來的結(jié)構(gòu),這一整串就是自動釋放池。


- magic 用于對當前 AutoreleasePoolPage 完整性的校驗
- thread 保存了當前頁所在的線程
- id *next指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置
-
每個AutoreleasePoolPage大小均為4096
釋放池結(jié)構(gòu)
二、自動釋放池何時釋放
//ARC 環(huán)境
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
YMObject *object = [[YMObject alloc] init];
};
}
實際的函數(shù)調(diào)用是這樣的


需要注意的是,整個程序中push和pop的操作都是一一對應(yīng)的,下面會說明
來分析一下 objc_autoreleasePoolPush和objc_autoreleasePoolPop這兩個函數(shù)做了什么
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
void *
_objc_autoreleasePoolPush(void)
{
return objc_autoreleasePoolPush();
}
void
_objc_autoreleasePoolPop(void *ctxt)
{
objc_autoreleasePoolPop(ctxt);
}
Objc4源碼中可以看到實際上都是AutoreleasePoolPage類的push和pop函數(shù)。Push函數(shù)中會調(diào)用一個比較核心的方法: autoreleaseFast
//這個函數(shù)的作用就是,找到最頂層的一個AutoreleasePoolPage對象,如果沒有那就創(chuàng)建一個;如果找到了,判斷他是否已經(jīng)裝滿了full(),因為一個AutoreleasePoolPage只有4096個字節(jié)大小,如果滿了那就會調(diào)用autoreleaseNoPage()創(chuàng)建一個AutoreleasePoolPage對象并添加add;如果沒滿則直接執(zhí)行add(obj)。
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage(); //hotPage()函數(shù)會對應(yīng)線程去取自動釋放池,這里也可以看出釋放池和線程是一一對應(yīng)的關(guān)系
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
需要注意的是上述add(obj)中的obj是一個POOL_BOUNDARY對象(可以稱為哨兵對象),并不是我們的autorelease的對象,每次執(zhí)行push操作時都會插入一個哨兵對象,并且把哨兵對象的地址作為返回值返回了,pop函數(shù)需要用到這個哨兵對象的地址,對應(yīng)的每次pop都是尋找到上一個哨兵對象,對期間所有的autorelease對象執(zhí)行一次release操作。
再來看下pop函數(shù):其中會調(diào)用一個比較核心的函數(shù)releaseUntil(id *stop)
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中的autorelease對象已釋放完畢則會重新遍歷父結(jié)點的page,知道找到傳遞來的哨兵對象為止
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
上述就是我們自己通過@autoreleasePool{ }形式創(chuàng)建的釋放池的創(chuàng)建和銷毀,以及其內(nèi)部的主要邏輯分析。
當然系統(tǒng)默認也是會在當前runloop結(jié)束本次循環(huán)即將進入休眠的時候也會執(zhí)行一次pop操作來對釋放池內(nèi)的對象進行一次release操作,然后再重新執(zhí)行一次push操作。
再補充一下:(直接從別處搬來的…)
App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()。
- 第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池。其 order 是-2147483647,優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
- 第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。
總結(jié)可以下來就是:app啟動 調(diào)用了一次push來創(chuàng)建釋放池,中間我們自己可能會通過@autoreleasePool{ }再執(zhí)行n次push和pop操作,runloop即將休眠時又會調(diào)用一次pop和一次push,runloop在Exit時,又會調(diào)用一次pop操作,可以說是一個push就有一個pop與之對應(yīng)的,從而保證對象能正常釋放。
三、ARC下什么樣的初始化方法系統(tǒng)會為我們做一次autorelease操作
ARC下以alloc/new/copy/mutableCopy開頭的方法,其返回的對象歸調(diào)用者所有。歸調(diào)用者所有的意思是:調(diào)用上述四個方法的那段代碼負責釋放方法返回的對象。(摘自Effective Objective-C 2.0 P124)
非alloc/new/copy/mutableCopy開頭的方法編譯器都會自動幫我們調(diào)用autorelease方法。
//自定義類方法
+ (instancetype)createSark {
return [self new]; //這里是有autorelease的(誰創(chuàng)建誰釋放)
}
// caller
Sark *sark = [Sark createSark]; //此處有retain操作,作用域結(jié)束會有release操作
值得一提的是,ARC下,runtime有一套對autorelease返回值的優(yōu)化策略:
秉著誰創(chuàng)建誰釋放的原則,返回值需要是一個autorelease對象才能配合調(diào)用方正確管理內(nèi)存,于是乎編譯器改寫成了形如下面的代碼:
+ (instancetype)createSark {
id tmp = [self new];
return objc_autoreleaseReturnValue(tmp); // 代替我們調(diào)用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我們調(diào)用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相當于代替我們調(diào)用了release
一切看上去都很好,不過既然編譯器知道了這么多信息,干嘛還要勞煩autorelease這個開銷不小的機制呢?于是乎,runtime使用了一些黑魔法將這個問題解決了,繼續(xù)往下看。
黑魔法之Thread Local Storage
Thread Local Storage(TLS)線程局部存儲,目的很簡單,將一塊內(nèi)存作為某個線程專有的存儲,以key-value的形式進行讀寫.
在返回值身上調(diào)用objc_autoreleaseReturnValue方法時,runtime將這個返回值object儲存在TLS中,然后直接返回這個object(不調(diào)用autorelease);同時,在外部接收這個返回值的objc_retainAutoreleasedReturnValue里,發(fā)現(xiàn)TLS中正好存了這個對象,那么直接返回這個object(不調(diào)用retain)。
于是乎,調(diào)用方和被調(diào)方利用TLS做中轉(zhuǎn),很有默契的免去了對返回值的內(nèi)存管理。這樣就省去了兩個操作:autorelease和外部的一次retain操作,對于性能提高很多。
自動釋放池用途
for (int i = 0; i < 100000000; i++)
{
@autoreleasepool
{
NSString* string = @"ab c";
NSArray* array = [string componentsSeparatedByString:string]; //放入自動釋放池中,從而避免大量局部變量要等待runloop將要休眠才釋放從而造成的內(nèi)存峰值。
}
}
參考文章:
http://m.itdecent.cn/p/8b011b844231
https://blog.sunnyxx.com/2014/10/15/behind-autorelease/
