背景
自從蘋果推出了ARC管理內(nèi)存后秽五,對(duì)于iOS開發(fā)這而言饥悴,內(nèi)存管理就變得so easy了西设,只要正確使用相關(guān)規(guī)則,再也不用擔(dān)心double release棠笑,野指針的等問題了禽绪,而ARC的背后,除了強(qiáng)大的編譯器之外循捺,還要得益于運(yùn)行時(shí)起作用的AutoReleasePool雄人。
研究AutoReleasePool
iOS的項(xiàng)目中础钠,除了特別需求外恰力,整個(gè)項(xiàng)目就一個(gè)地方明確寫了autoReleasePool的代碼了旗吁,就是main函數(shù):
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
autoreleasepool做了什么很钓?
我們知道oc代碼在編譯期間都會(huì)轉(zhuǎn)化為c/c++代碼,然后轉(zhuǎn)化為匯編回还,最終轉(zhuǎn)化為對(duì)應(yīng)的架構(gòu)的二進(jìn)制文件柠硕;也可以這么說,oc的底層實(shí)現(xiàn)就是c/c++闻葵,既然這樣癣丧,我們把他轉(zhuǎn)化為對(duì)應(yīng)的c/c++代碼應(yīng)該就可以窺探到其中的密碼了:
轉(zhuǎn)化為c/c++代碼胁编,Xcode有自帶的工具,打開命令行早直,輸入一下命令就可以:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
為了減少代碼量市框,重新建了一個(gè)macOS命令行項(xiàng)目:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
轉(zhuǎn)化為cpp文件枫振,看下對(duì)應(yīng)的代碼:
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++源碼可以發(fā)現(xiàn)@autoreleasepool {}最后變成了:
{ __AtAutoreleasePool __autoreleasepool;//定義了一個(gè)__AtAutoreleasePool結(jié)構(gòu)體變量
粪滤。。。
}
我們分析下流程:
1窍侧、進(jìn)入大括號(hào)转绷,定義了一個(gè)__AtAutoreleasePool的結(jié)構(gòu)體局部變量议经;
2、定義這個(gè)結(jié)構(gòu)體變量的時(shí)候咧织,會(huì)走結(jié)構(gòu)體的構(gòu)造方法籍救,間接的會(huì)調(diào)用objc_autoreleasePoolPush函數(shù):
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
3、當(dāng)走出大括號(hào)是闪萄,局部變量__autoreleasepool败去,會(huì)被銷毀,因此會(huì)走結(jié)構(gòu)體的析構(gòu)函數(shù)广鳍,間接就會(huì)調(diào)用objc_autoreleasePoolPop函數(shù):
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
從上面的分析搜锰,我們發(fā)現(xiàn)了兩個(gè)重要的函數(shù)objc_autoreleasePoolPush和objc_autoreleasePoolPop耿战,這兩個(gè)函數(shù)是全局函數(shù)剂陡,而且是以objc開頭的,應(yīng)該是在objc的源碼中歌馍,下載objc源碼的地址晕鹊,macOS 最新系統(tǒng)下面的objc4溅话。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
他們調(diào)用的是AutoreleasePoolPage類對(duì)應(yīng)的push跟pop兩個(gè)靜態(tài)函數(shù),那么我們就要研究下AutoreleasePoolPage這個(gè)類了砚哆。
研究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是一個(gè)雙向鏈表的結(jié)構(gòu)卵史,每個(gè)實(shí)例都會(huì)存在一個(gè)parent實(shí)例指針以躯,跟一個(gè)child實(shí)例指針。其他成員變量暫時(shí)不知道表示什么意思色鸳,只能繼續(xù)研究AutoreleasePoolPage的實(shí)現(xiàn)邏輯了命雀,還是從push跟pop函數(shù)入手:
AutoreleasePoolPage 的push函數(shù):
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page .debug模式新建一個(gè)page對(duì)象,實(shí)際不需要關(guān)注
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {//實(shí)際走這里
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
追根溯源autoreleaseFast:
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
//拿到當(dāng)前正在被使用的page撵儿,因?yàn)槊總€(gè)page都是有對(duì)象obj數(shù)量限
//制的淀歇,當(dāng)page放滿了匈织,就會(huì)創(chuàng)建一個(gè)child page來繼續(xù)放缀匕。
if (page && !page->full()) {//沒滿,直接添加到當(dāng)前的page上
return page->add(obj);//添加obj
} else if (page) {//full阔加,滿了
return autoreleaseFullPage(obj, page);//將obj添加到對(duì)應(yīng)的未滿的child page里面胜榔,并將其設(shè)置為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夭织,將當(dāng)前的指針指向child page
else page = new AutoreleasePoolPage(page);//new一個(gè)新的page牵辣,并將其賦值給child page纬向;
} while (page->full());//page是否滿了戴卜,沒滿,跳出
setHotPage(page);//將page設(shè)置為當(dāng)前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();//設(shè)置一個(gè)占位的空的page
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.第一次創(chuàng)建一個(gè)page
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);//并將它設(shè)置為hotpage
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);//添加哨兵對(duì)象
}
// 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);///一個(gè)page的大小是size,4096字節(jié)
}
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指向下一個(gè)位置
protect();
return ret;//返回前一個(gè)obj
}
總結(jié)一下:
- push操作從取出當(dāng)前的hotpage酌心,然后將一個(gè)哨兵對(duì)象(其實(shí)是nil)放入到page的next位置安券,并將next指向的位置向下+1;
- 取出的hotpage存在鹦筹,但是hotpage是一個(gè)fullpage(一個(gè)page址貌,PAGE_MAX_SIZE = 4096字節(jié)大小芳誓,除了存放內(nèi)部成員變量的值之外,其他的都用來存放autorelease對(duì)象的地址)匿值,這時(shí)就會(huì)循環(huán)查找他的child指向的page對(duì)象赂摆,知道找到?jīng)]使用完的child page烟号,如果沒有child page,則創(chuàng)建一個(gè)达传,將找到的child page并設(shè)置為hotpage宪赶,然后將一個(gè)哨兵對(duì)象添加進(jìn)去脯燃;
- 取出的當(dāng)前hotpage不存在辕棚,則通過autoreleaseNoPage創(chuàng)建一個(gè)新的page邓厕,并設(shè)置為hotpage详恼,然后將然后將一個(gè)哨兵對(duì)象添加進(jìn)去涤妒;
- 這樣做的結(jié)果她紫,除了第一層page(沒有parent page),每次push返回的obj的都是哨兵對(duì)象渐逃,最開始的push返回的是第一層page最開始的位置page.begin()民褂,后面的pop會(huì)用到這個(gè)返回值赊堪。
AutoreleasePoolPage 的pop函數(shù):
static inline void pop(void *token)
{//token就是對(duì)應(yīng)push返回值哭廉,上面提到過要么是哨兵對(duì)象,要么是第一層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的指針?biāo)傅膶?duì)象乌企,直到遇到哨兵對(duì)象或者全部釋放完成
// 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對(duì)象釋放掉
// 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) {//循環(huán)釋放對(duì)象加酵,直到遇到哨兵對(duì)象或者全部釋放完
// 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()) {//當(dāng)前page釋放完哭当,還沒遇到哨兵對(duì)象荣病,拿到parent page渗柿,并設(shè)置為hotpage,繼續(xù)釋放柴梆,直到遇到哨兵對(duì)象或者全部釋放完
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;//拿到obj對(duì)象终惑,并將next指針指向上一個(gè)位置
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));//清空next位置雹有,這里是設(shè)置為0x3A
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);//釋放掉對(duì)象
}
}
setHotPage(this);//釋放完后,將當(dāng)前page設(shè)置為hotpage
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
pop函數(shù)總結(jié)幾點(diǎn):
- 會(huì)拿到最近一次push函數(shù)(pop函數(shù)與push一一對(duì)應(yīng))返回的哨兵對(duì)象溜宽,作為pop函數(shù)的入?yún)ⅲ?/li>
- 遍歷hotpage的next指針指向的對(duì)象适揉,并釋放煤惩,直到遇到入?yún)⒌纳诒鴮?duì)象魄揉;
- 如果當(dāng)前page釋放完了,還沒遇到哨兵對(duì)象票彪,就會(huì)往parent page遍歷降铸,直到遇到哨兵對(duì)象摇零,一次類推驻仅;
- 最后釋放掉為空(empty)的page對(duì)象,但是需要注意的是:當(dāng)他的parent的使用空間超過了1/2毡泻,保留它對(duì)應(yīng)的child page粘优。
從上面的分析大致了解了AutoreleasePoolPage的工作流程,在程序運(yùn)行的時(shí)候是怎么樣工作的呢廊遍?
我們知道贩挣,在MRC時(shí)代王财,需要程序員手寫對(duì)象的retain和release,后面最智能的就是new一個(gè)對(duì)象的時(shí)候狭握,我們需要帶上autorelease代碼:
[[[NSObject alloc] init] autorelease];//MRC手動(dòng)管理內(nèi)存
到了ARC论颅,我們不需要這樣寫囱嫩,因?yàn)榫幾g器在編譯的時(shí)候墨闲,會(huì)自動(dòng)幫忙加上這些代碼,所以說不管是ARC還是MRC時(shí)期盾鳞,oc對(duì)象的內(nèi)存管理入口都是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);//調(diào)用的是AutoreleasePoolPage的autorelease函數(shù)
}
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);//調(diào)用autoreleaseFast腾仅,上面提到過
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
就是說推励,每個(gè)oc對(duì)象創(chuàng)建的時(shí)候(alloc/new/copy/mutableCopy)验辞,都是通過autorelease方法添加到page對(duì)象中的喊衫。pop那具體的調(diào)用時(shí)機(jī)又是什么呢族购?我們知道财著,項(xiàng)目只在main函數(shù)有一個(gè)autoreleasepool撑碴,其他的地方除非程序員自己手動(dòng)添加醉拓,就不會(huì)有了收苏,也就是說:我們暫且認(rèn)為編譯器幫忙添加的autorelease鹿霸,那也只是添加進(jìn)page懦鼠,pop函數(shù)還是只有一個(gè),而且是程序退出的時(shí)候調(diào)用街氢,如果是這種情況的話珊肃,程序整個(gè)運(yùn)行期間,內(nèi)存得不停的增長(zhǎng)馅笙,因?yàn)橹挥猩暾?qǐng)伦乔,沒有釋放。顯然這種做法是行不通的 董习。
RunLoop& AutoReleasePool關(guān)系幾點(diǎn)說明:
- App啟動(dòng)后蔗喂,蘋果在主線程 RunLoop 里注冊(cè)了兩個(gè) Observer,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()高帖。
- 第一個(gè) Observer 監(jiān)視的事件是 Entry(即將進(jìn)入Loop)缰儿,其回調(diào)內(nèi)會(huì)調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動(dòng)釋放池。其 order 是-2147483647散址,優(yōu)先級(jí)最高乖阵,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前宣赔。
- 第二個(gè) Observer 監(jiān)視了兩個(gè)事件: BeforeWaiting(準(zhǔn)備進(jìn)入休眠) 時(shí)調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時(shí)調(diào)用 _objc_autoreleasePoolPop() 來釋放自動(dòng)釋放池瞪浸。這個(gè) Observer 的 order 是 2147483647,優(yōu)先級(jí)最低钩蚊,保證其釋放池子發(fā)生在其他所有回調(diào)之后。
總結(jié)
通過上面的源碼及流程的分析,我們對(duì)于autoreleasepool的工作原理及流程有了充分的了解:
-
autoreleasepool底層的數(shù)據(jù)結(jié)構(gòu)是一個(gè)autoreleasepage的雙向鏈表,每個(gè)page的大小為4096字節(jié)赡茸,除了存儲(chǔ)成員變量的大小,其他的位置都用來存儲(chǔ)autorelease對(duì)象的地址,next變量永遠(yuǎn)指向下一個(gè)可以存放autorelease對(duì)象地址的地址空間叭喜,具體結(jié)構(gòu)如下:數(shù)據(jù)結(jié)構(gòu)
- 當(dāng)一個(gè)page1存儲(chǔ)空間用完后,會(huì)創(chuàng)建一個(gè)新的page2,新的page2的parent指針指向滿了的page1溉知,page1的child指針會(huì)指向page2;
- 程序啟動(dòng)就會(huì)創(chuàng)建一個(gè)autoreleasepool舌劳,會(huì)調(diào)用push,程序結(jié)束時(shí),最后會(huì)調(diào)用pop建邓,回收所有autorelease對(duì)象的內(nèi)存沸手;
- 程序運(yùn)行期間,同過監(jiān)聽runloop的休眠狀態(tài)诡渴,調(diào)用push/pop方法,管理autorelease對(duì)象妄辩;
- 每次push的時(shí)候,會(huì)往page里面添加一個(gè)哨兵對(duì)象眼耀,這個(gè)哨兵對(duì)象作為下次pop函數(shù)的入?yún)ⅲ龅缴诒鴮?duì)象哮伟,說明這次runloop循環(huán)添加到page的autorelease對(duì)象release完畢。