重新仔細(xì)讀了下源碼记盒,通過留些筆記的方式加深理解憎蛤。由于我的C++水平有限,可能有些地方理解的不到位,甚至有錯(cuò)誤俩檬,希望有朋友幫我指正萎胰。
一、@autoreleasepool {}是什么棚辽?
創(chuàng)建個(gè)新工程技竟,簡(jiǎn)單起見,把main.m改成這樣:
int main(int argc, char * argv[]) {
@autoreleasepool { }
return 0;
}
打開終端屈藐,cd到main.m的目錄下榔组,用命令“clang -rewrite-objc main.m”,看看C++的實(shí)現(xiàn)联逻。報(bào)錯(cuò)的話搓扯,根據(jù)錯(cuò)誤信息修改。打開同目錄下新生成的main.cpp遣妥,搜索找到main函數(shù)的實(shí)現(xiàn)擅编,如下:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; }
return 0;
}
初步可見,@autoreleasepool{}是通過__AtAutoreleasePool類型的局部變量__autoreleasepool來實(shí)現(xiàn)的箫踩。再搜索下爱态,會(huì)發(fā)現(xiàn)__AtAutoreleasePool是如下的結(jié)構(gòu)體:
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
因?yàn)榫植孔兞柯暶鲿r(shí)調(diào)用構(gòu)造函數(shù),離開作用域時(shí)調(diào)用析構(gòu)函數(shù)境钟,那么:
// 假如我們寫:
@autoreleasepool{
NSString *str = [NSString stringWithFormat:@"Hello, World! "];
}
// 實(shí)際就相當(dāng)于:
/* @autoreleasepool */ {
atautoreleasepoolobj = objc_autoreleasePoolPush();
// 暫先不用理會(huì)這行代碼c++的實(shí)現(xiàn)是什么樣子
NSString *str = [NSString stringWithFormat:@"Hello, World! "];
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
接下來的任務(wù)是看objc_autoreleasePoolPush()和objc_autoreleasePoolPop()做了什么锦担。顯然要看runtime的源碼。
二慨削、Autorelease Pool的數(shù)據(jù)結(jié)構(gòu)
在繼續(xù)看源碼之前洞渔,我想先根據(jù)我個(gè)人的理解說明下autorelease pool的數(shù)據(jù)結(jié)構(gòu)(如果理解有誤,希望有同學(xué)指正)缚态,帶著這個(gè)總體概念有助于繼續(xù)閱讀源碼磁椒。
首先,每個(gè)線程都可以有自己的autorelease pool玫芦,系統(tǒng)會(huì)默認(rèn)為主線程創(chuàng)建一個(gè)浆熔,而子線程默認(rèn)是沒有的。這點(diǎn)后面會(huì)再進(jìn)一步說明桥帆。
然后医增,線程可以有0到多個(gè)pool,1到多個(gè)pool形成一個(gè)棧老虫,棧中存儲(chǔ)需要autorelease的對(duì)象指針或者名叫POOL_BOUNDARY的占位符(用來區(qū)隔不同的pool)叶骨。這個(gè)棧又由不同的page組成,每個(gè)page有固定的size祈匙,還有parent和child兩根指針忽刽。多個(gè)page之間通過指針相連,這樣就形成了一種類似鏈棧的結(jié)構(gòu)。
三缔恳、runtime源碼中的實(shí)現(xiàn)
runtime源碼(這里用objc4-781)里很容找到一中提到的兩個(gè)函數(shù)的實(shí)現(xiàn):
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
繼續(xù)向下找AutoreleasePoolPage宝剖。
- AutoreleasePoolPage
首先我們會(huì)找到下面的代碼和注釋:
/***********************************************************************
Autorelease pool implementation
A thread's autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is
an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When
the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.
**********************************************************************/
class AutoreleasePoolPage : private AutoreleasePoolPageData
可以結(jié)合二中提到的內(nèi)容看看這些注釋:
// 一個(gè)線程的autorelease pool是一個(gè)指針棧洁闰。
// 棧中存放的指針指向需要release的對(duì)象或者POOL_BOUNDARY(作用如二中所提)歉甚。
// 每個(gè)pool會(huì)有一個(gè)token指針,指向pool的POOL_BOUNDARY(哨兵)扑眉。當(dāng)這個(gè)pool執(zhí)行出棧操作時(shí)纸泄,每一個(gè)比這個(gè)哨兵后入棧的對(duì)象都會(huì)被released。
// 這個(gè)棧是由一個(gè)以pages為節(jié)點(diǎn)的雙向鏈表組成腰素,pages會(huì)根據(jù)需求進(jìn)行增減聘裁。
// 線程局部變量(Thread-local storage / tls)存儲(chǔ)了指向最新page(the hot page)的指針,最近被autoreleased的對(duì)象會(huì)被存儲(chǔ)在這個(gè)page里弓千。
- AutoreleasePoolPageData
這里我們可以看到上面所謂雙向鏈表的節(jié)點(diǎn)的結(jié)構(gòu):
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
magic_t const magic; // 用于結(jié)構(gòu)的完整性校驗(yàn)
__unsafe_unretained id *next; // 當(dāng)前page的棧頂指針
pthread_t const thread; // 所屬線程
AutoreleasePoolPage * const parent; // 父節(jié)點(diǎn)
AutoreleasePoolPage *child; // 子節(jié)點(diǎn)
uint32_t const depth; // 鏈表結(jié)點(diǎn)數(shù)
uint32_t hiwat; // 當(dāng)前鏈表儲(chǔ)存指針的個(gè)數(shù)衡便?
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)
{
}
};
節(jié)點(diǎn)結(jié)構(gòu)清楚了,來看看push操作洋访。
- AutoreleasePoolPage::push()
直接看源碼加注釋:
static inline void *push()
{
id *dest;
if (slowpath(DebugPoolAllocation)) {
// debug模式下镣陕,每一個(gè)autorelease pool都從一個(gè)新的page開始。
// 注意這里操作的對(duì)象是:POOL_BOUNDARY
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
// 返回POOL_BOUNDARY存入的位置
return dest;
}
static inline id *autoreleaseFast(id obj)
{
// hotPage可以理解為當(dāng)前工作的page姻政,也就是最近被autoreleased的對(duì)象指針會(huì)被加入的page呆抑,下同
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 如果hotPage沒滿,存入obj
return page->add(obj);
} else if (page) {
// 如果滿了汁展,方法內(nèi)部向下尋找沒滿的子節(jié)點(diǎn)鹊碍,并存入obj;如果沒有食绿,就新建一個(gè)節(jié)點(diǎn)
return autoreleaseFullPage(obj, page);
} else {
// 新建一個(gè)節(jié)點(diǎn)并存入obj
return autoreleaseNoPage(obj);
}
}
// 邏輯大致如上侈咕,只是沒有判斷hotPage是否滿了
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page) return autoreleaseFullPage(obj, page);
else return autoreleaseNoPage(obj);
}
push操作內(nèi)部會(huì)有一些邏輯分支,條件如上面代碼注釋器紧,最終的目的都是找到或創(chuàng)建一個(gè)page將指針存進(jìn)去耀销。既然要存新指針,autoreleaseFullPage(obj, page)和autoreleaseNoPage(obj)內(nèi)部最終也都調(diào)用了page->add(obj)方法品洛。add方法很簡(jiǎn)單树姨,指針存入hotPage頂部(next),頂部加1:
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
// next指向的空間存入obj桥状,然后加1
*next++ = obj;
protect();
// 返回存入的位置
return ret;
}
顯然存儲(chǔ)的目的就是為了在[pool drain]漓骚,也就是pop操作的時(shí)候向被存入的對(duì)象發(fā)送release消息。
- AutoreleasePoolPage::pop(ctxt)
pop的代碼有點(diǎn)多耐亏,這里就不貼出來了龟糕。
概括來講就是要找到ctxt對(duì)應(yīng)的page,并將比ctxt晚入棧的對(duì)象全部出棧,并調(diào)用方法:objc_release(obj)查邢,也就是[obj release]蔗崎。可以對(duì)照三.1中的這段注釋來理解:
// 每個(gè)pool會(huì)有一個(gè)token指針扰藕,指向pool的POOL_BOUNDARY(哨兵)缓苛。當(dāng)這個(gè)pool執(zhí)行出棧操作時(shí),每一個(gè)比這個(gè)哨兵后入棧的對(duì)象都會(huì)被released邓深。
- AutoreleasePoolPage:: autorelease(obj)
上面1中提到未桥,autorelease pool棧中存放的指針有兩種:指向需要release的對(duì)象或者POOL_BOUNDARY。
上面的push和pop操作針對(duì)的就是POOL_BOUNDARY芥备,而這里要講的方法的操作對(duì)象就是pool drain時(shí)需要release的對(duì)象冬耿。
其方法實(shí)現(xiàn)很簡(jiǎn)單,內(nèi)部調(diào)用的autoreleaseFast方法上面也講過了萌壳。
我們知道亦镶,當(dāng)通過遍歷構(gòu)造器,比如“NSString *str = [NSString stringWithFormat:@"%@", xxx]”創(chuàng)建對(duì)象的時(shí)候袱瓮,方法內(nèi)部執(zhí)行了“[str autorelease]”缤骨。而這時(shí)底層就通過調(diào)用“AutoreleasePoolPage:: autorelease(str)”,將str加入自動(dòng)釋放池懂讯。
static inline id autorelease(id obj)
{
ASSERT(obj);
ASSERT(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
源碼的部分先到這荷憋,主要的結(jié)構(gòu)和方法都提到了,還有很多細(xì)節(jié)和值得深挖的地方褐望,留給你自己勒庄。
四、Autorelease Pool的使用場(chǎng)景
autorelease pool是為引用計(jì)數(shù)機(jī)制服務(wù)的瘫里,我們創(chuàng)建的所有需要autorelease的對(duì)象实蔽,都要在一個(gè)pool中進(jìn)行。
- 主線程在每一個(gè)runloop的circle開始時(shí)都會(huì)自動(dòng)創(chuàng)建一個(gè)pool谨读,然后在結(jié)束時(shí)自動(dòng)調(diào)用[pool drain]局装。所以我們?cè)谥骶€程中如果沒有特殊需求不需要手動(dòng)創(chuàng)建autorelease pool。有特殊需求的情況見3劳殖。
- 在子線程中铐尚,如果我們調(diào)用了諸如“NSString *str = [NSString stringWithFormat:@"%@", xxx]這類的方法,就需要自動(dòng)釋放池哆姻,這時(shí)我們必須手動(dòng)創(chuàng)建autorelease pool宣增,否則會(huì)導(dǎo)致leak。但是如果我們只用[[MyClass alloc] init]方法來創(chuàng)建對(duì)象矛缨,因?yàn)樗鼈兂隽俗饔糜蚓蜁?huì)被自動(dòng)銷毀爹脾,則不需要我們手動(dòng)創(chuàng)建池子了帖旨。
- 另一個(gè)被廣泛討論的場(chǎng)景就是在主線程(子線程中如果我們開啟了runloop,當(dāng)然也一樣)的一個(gè)循環(huán)中短時(shí)間創(chuàng)建大量對(duì)象灵妨,如果我們不想等到當(dāng)前runloop的circle結(jié)束時(shí)才釋放這些對(duì)象(這時(shí)內(nèi)存可能已經(jīng)暴漲很多了)解阅,就可以在循環(huán)體中手動(dòng)創(chuàng)建一個(gè)pool,使得每次循環(huán)結(jié)束時(shí)都將本次循環(huán)中產(chǎn)生的臨時(shí)變量及時(shí)釋放掉泌霍。
五货抄、Swift中的Autorelease Pool
- Swift工程的程序入口標(biāo)記為@UIApplicationMain,沒有了main.m烹吵,也就看不到@autoreleasepool {}了碉熄。
- Swift中還需不需要使用autorelease pool?
Swift 1.0時(shí)還有各種對(duì)應(yīng)OC中遍歷構(gòu)造器的創(chuàng)建對(duì)象的方法肋拔,比如“String.stringWithFormat”。而從Swift 1.1開始這類方法都被廢棄了呀酸,我們基本都只能使用init方法來創(chuàng)建對(duì)象凉蜂,這種情況下就不需要autorelease pool了。(這點(diǎn)是從喵神15年的博客看來的...那時(shí)候我還沒開始學(xué)習(xí)Swift性誉。)
不過窿吩,我發(fā)現(xiàn)現(xiàn)在還有一些類似的api存在,比如:
public class func localizedStringWithFormat(_ format: NSString, _ args: CVarArg...) -> Self
如果使用這類方法的話错览,應(yīng)該還需要autorelease pool吧纫雁。?
- Swift中autorelease pool的使用形式不再是@autoreleasepool {}倾哺,去掉了“@”符號(hào)轧邪,直接是autoreleasepool {},其實(shí)是一個(gè)函數(shù)羞海,還有返回值忌愚。
public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result