AutoreleasePool是OC中的一種自動回收機制诗宣,在ARC的模式下已經(jīng)很少能看到autorelease了,它可以延遲變量release的時機耗跛。在OC的main.m中就有一個autoreleasepool杖刷,本篇結合runtime研究一下autoreleasepool的底層是如何實現(xiàn)的竹勉。
問答模式
問:什么時候需要使用自動釋放池裹虫?
官方解釋:基本分為如下三點
1肿嘲、當我們需要創(chuàng)建大量的臨時變量的時候,可以通過@autoreleasepool 來減少內(nèi)存峰值筑公。
2雳窟、創(chuàng)建了新的線程執(zhí)行Cocoa調(diào)用。
3匣屡、如果您的應用程序或線程是長期存在的封救,并且可能會生成大量自動釋放的對象,那么您應該定期清空并創(chuàng)建自動釋放池(就像UIKit在主線程上所做的那樣)捣作;否則誉结,自動釋放的對象會累積,內(nèi)存占用也會增加券躁。但是惩坑,如果創(chuàng)建的線程不進行Cocoa調(diào)用,則不需要創(chuàng)建自動釋放池也拜。
問:為什么會減少內(nèi)存峰值旭贬?
答:借用YYImage的代碼打個比方。
比如業(yè)務需要在一個代碼塊中需要創(chuàng)建大量臨時變量搪泳,或臨時變量足夠大,占用了很多內(nèi)存扼脐,可以在臨時變量使用完以后就立即釋放掉岸军,在ARC的環(huán)境下只能通過自動釋放池實現(xiàn)奋刽。
if ([UIDevice currentDevice].isSimulator) {
@autoreleasepool {
NSString *outPath = [NSString stringWithFormat:@"%@ermilio.gif.png",IMAGE_OUTPUT_DIR];
NSData *outData = UIImagePNGRepresentation([UIImage imageWithData:gif]);
[outData writeToFile:outPath atomically:YES];
[gif writeToFile:[NSString stringWithFormat:@"%@ermilio.gif",IMAGE_OUTPUT_DIR] atomically:YES];
}
@autoreleasepool {
NSString *outPath = [NSString stringWithFormat:@"%@ermilio.apng.png",IMAGE_OUTPUT_DIR];
NSData *outData = UIImagePNGRepresentation([UIImage imageWithData:apng]);
[outData writeToFile:outPath atomically:YES];
[apng writeToFile:[NSString stringWithFormat:@"%@ermilio.png",IMAGE_OUTPUT_DIR] atomically:YES];
}
@autoreleasepool {
NSString *outPath = [NSString stringWithFormat:@"%@ermilio_q85.webp.png",IMAGE_OUTPUT_DIR];
NSData *outData = UIImagePNGRepresentation([YYImageDecoder decodeImage:webp_q85 scale:1]);
[outData writeToFile:outPath atomically:YES];
[webp_q85 writeToFile:[NSString stringWithFormat:@"%@ermilio_q85.webp",IMAGE_OUTPUT_DIR] atomically:YES];
}
}
再比如在循環(huán)的場景下,如果創(chuàng)建大量的臨時變量艰赞,會使內(nèi)存峰值持續(xù)增加佣谐,加入自動釋放池以后,在每次循環(huán)結束時方妖,超出自動釋放池的作用域狭魂,使得內(nèi)部的大量臨時變量被釋放,從而大大降低了內(nèi)存的使用党觅。
for (int i = 0; i < count; i++) {
@autoreleasepool {
id imageSrc = _images[i];
NSDictionary *frameProperty = NULL;
if (_type == YYImageTypeGIF && count > 1) {
frameProperty = @{(NSString *)kCGImagePropertyGIFDictionary : @{(NSString *) kCGImagePropertyGIFDelayTime:_durations[i]}};
} else {
frameProperty = @{(id)kCGImageDestinationLossyCompressionQuality : @(_quality)};
}
}
上述這幾種情況如果沒必要就別這么寫雌澄,畢竟創(chuàng)建自動釋放池也需要耗費內(nèi)存。
自動釋放池的實現(xiàn)原理
在開始之前先看一下自動釋放池的大致結構圖
上圖就是自動釋放池的結構圖杯瞻,可能現(xiàn)在看不懂镐牺,這里先有個概況繼續(xù)往下看就明白了,不太會畫圖魁莉,反正意思表達出來了睬涧。
查看main.cpp
我們先在終端clang一下main.m,變成C++實現(xiàn)
clang -rewrite-objc main.m -o main.cpp
我們會得到一個main.cpp文件旗唁,打開這個文件翻到最底部會看到這個代碼
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vg_bngxds5x5q90wwst5gl1jq140000gn_T_main_1b100d_mi_0);
}
return 0;
}
發(fā)現(xiàn)原來autoreleasepool也是一個對象畦浓,我們在這個cpp文件中查找__AtAutoreleasePool
,找到如下的結構體
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;
};
結構體只提供了一個構造函數(shù)和一個析構函數(shù)检疫,里面分別調(diào)用了objc_autoreleasePoolPush
和objc_autoreleasePoolPop
讶请,這個objc前綴告訴我們,是不是能到runtime里面搜索一下电谣,在rumtime源碼中全局搜索objc_autoreleasePoolPush
秽梅,找到這個函數(shù)
void * objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
我們發(fā)現(xiàn)了正主,是一個類AutoreleasePoolPage
AutoreleasePoolPage
class AutoreleasePoolPage
{
···
//當自動釋放池為空時的一個占位符
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
//邊界符剿牺,用來區(qū)別每個AutoreleasePoolPage的邊界
# define POOL_BOUNDARY nil
//線程的key企垦,通過key值尋找線程下的AutoreleasePoolPage
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
//4096個字節(jié),表示每個page的大小晒来,因為虛擬內(nèi)存每個扇區(qū)4096個字節(jié)
PAGE_MAX_SIZE;
//一個page里面的對象數(shù)量
static size_t const COUNT = SIZE / sizeof(id);
//共需要占用56個字節(jié)
magic_t const magic; // 16字節(jié)钞诡,校驗完整性的變量
id *next; // 8字節(jié),指向下一個對象的指針
pthread_t const thread; // 8字節(jié)湃崩,所屬線程荧降,page和thread是一一對應關系
AutoreleasePoolPage * const parent; // 8字節(jié),父節(jié)點攒读,指向上一個page
AutoreleasePoolPage *child; // 8字節(jié)朵诫,子節(jié)點,指向下一個page
uint32_t const depth; // 4字節(jié)薄扁,表示鏈表一共有多少個節(jié)點
uint32_t hiwat; // 4字節(jié)剪返,high water marks表示自動釋放池中最多能存放的對象個數(shù)
···
}
從這個類中我們得到了以下內(nèi)容
- EMPTY_POOL_PLACEHOLDER:
當自動釋放池為空時的一個占位符废累,就是在第一次push
時,先用這個字段把AutoreleasePoolPage的位置占上脱盲。
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
- POOL_BOUNDARY :
邊界符邑滨,用來區(qū)別每個AutoreleasePoolPage的邊界,我們從創(chuàng)建page的時候可以得知
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);//設置活躍頁
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
- key:
線程的key钱反,通過key值尋找線程下的AutoreleasePoolPage - PAGE_MAX_SIZE:
4096個字節(jié)掖看,表示每個page的大小,因為虛擬內(nèi)存每個扇區(qū)4096個字節(jié) - COUNT:
一個page里面的對象數(shù)量 - magic:
校驗完整性的變量面哥,占用16字節(jié) - next:
指向下一個對象的指針哎壳,占用8字節(jié) - thread:
所屬線程,page和thread是一一對應關系幢竹,占用8字節(jié) - parent:
父節(jié)點耳峦,指向上一個page,占用8字節(jié)焕毫,看到這里我們發(fā)現(xiàn)這個自動釋放池其實是個雙向鏈表蹲坷,不過是以棧的形式存取的 - child:
子節(jié)點,指向下一個page邑飒,占用8字節(jié) - depth:
表示鏈表一共有多少個節(jié)點循签,占用4字節(jié) - hiwat:
high water marks表示自動釋放池中最多能存放的對象個數(shù),占用4字節(jié)
從上面的分析我們可以得知疙咸,page本身占用了56個字節(jié)县匠,而一個AutoreleasePoolPage一共4096個字節(jié),也就是說我們還剩下4040個字節(jié)可以用來放對象撒轮。接下來看看它的push和pop的過程乞旦。
1、Push
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
這里我們不看Debug题山,直接找autoreleaseFast函數(shù)
static inline id *autoreleaseFast(id obj)
{
//獲取當前活躍的page
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
從代碼中我們得知纹烹,先調(diào)用hotPage()
函數(shù)獲取page涮帘,當page不滿時谒兄,我們調(diào)用add()
函數(shù)赶促;當對象滿了時,調(diào)用了autoreleaseFullPage()
函數(shù)慨菱;當沒獲取到page時焰络,調(diào)用autoreleaseNoPage()
函數(shù)。接下來我們看看這幾個函數(shù)都做了什么
1.1符喝、hotPage()
static inline AutoreleasePoolPage *hotPage()
{
//從一個鍵值對中獲取當前page
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
從代碼中我們得知hotPage函數(shù)是從一個鍵值對中獲取當前活躍的page闪彼,而這個key就是上面我們看到的
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
1.2、page->add(obj)
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
從源碼中我們得知协饲,add()
是向鏈表中增加一個對象备蚓,簡單的改變了指針的指向课蔬,這不必細說。
1.3郊尝、autoreleaseFullPage(obj, page)
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;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
從源碼中我們得知,這個page滿了以后战惊,循環(huán)遍歷自動釋放池中的page流昏,直到找到一個page不滿時,我們把對象添加進去吞获。
1.4况凉、autoreleaseNoPage(obj)
id *autoreleaseNoPage(id obj)
{
bool pushExtraBoundary = false;
//判斷是否有空池占位符
if (haveEmptyPoolPlaceholder()) {
pushExtraBoundary = true;
} else if (obj != POOL_BOUNDARY && DebugMissingPools) {
//沒有可用pool
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
//當前page還沒有空池占位符,先加上占位符
return setEmptyPoolPlaceholder();
}
//如果執(zhí)行到這里各拷,表示目前沒有可有page刁绒,要新建一個
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);//設置激活頁
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
//如果有占位符,則這里加上邊界符
page->add(POOL_BOUNDARY);
}
//把autorelease對象添加進來
return page->add(obj);
}
1.5烤黍、Push總結
流程基本如上圖所示
2知市、Pop
在Pop時,會傳入當前的token速蕊,token就是
static inline void pop(void *token)
{
//token就是邊界符
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
if (hotPage()) {
pop(coldPage()->begin());
} else {
setHotPage(nil);
}
return;
}
//找到最上邊的page嫂丙,即當前的page
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
//講道理,如果token不等于POOL_BOUNDARY规哲,pageForPointer()計算過后跟啤,理論上是一定會進入這里的
} else {
//走這就出問題了
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
//更新當前自動釋放池最大存儲數(shù)
if (PrintPoolHiwat) printHiwat();
//清空token之前的autorelease對象
page->releaseUntil(stop);
//清空操作
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
從源碼中我們看到了Pop的過程分成了三步,
1唉锌、判斷token
是否等于EMPTY_POOL_PLACEHOLDER
首先我們要知道token
實際上是個邊界符隅肥,通常情況下等于POOL_BOUNDARY
,其次我們要記得上面說過自動釋放池其實是個雙向鏈表袄简,不過是以棧的形式存取的腥放,所以當執(zhí)行這個判斷條件時,實際上就是Pop到了最后一步了痘番。
2捉片、當token
不等于POOL_BOUNDARY時
這一步一般是不會進來的,只有在沒有自動釋放池且調(diào)用了autorelease時才會出現(xiàn)汞舱。但生活還是要繼續(xù)的...
在做接下來的操作前伍纫,先獲取最新的page,即當前page
page = pageForPointer(token);
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;//size就是4096昂芜,每個page最大size
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
下面這個操作知識為了確保這個token拿到的page沒問題莹规。
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
//講道理,如果token不等于POOL_BOUNDARY泌神,pageForPointer()計算過后良漱,理論上是一定會進入這里的
// 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 {
//走這就出問題了
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
3舞虱、最后一步釋放page里面的對象。
在釋放操作之前母市,更新當前自動釋放池最大存儲數(shù)矾兜。
if (PrintPoolHiwat) printHiwat();
static void printHiwat()
{
AutoreleasePoolPage *p = hotPage();
uint32_t mark = p->depth*COUNT + (uint32_t)(p->next - p->begin());
if (mark > p->hiwat && mark > 256) {
for( ; p; p = p->parent) {
p->unprotect();
p->hiwat = mark;
p->protect();
}
這一步操作釋放token之前的autorelease對象。
//釋放token之前的autorelease對象
page->releaseUntil(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) {
while (page->empty()) {
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);
}
}
kill操作患久,如果當前page小于當前page的一半時椅寺,則把當前頁的所有子節(jié)點都kill掉,否則從子節(jié)點的子節(jié)點開始kill蒋失。
//清空操作
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
到目前為止返帕,我們明白了autorelease對象的釋放是在autoreleasePool釋放之前。
參考資料
autorelease和autoreleasePoolPage--你真的了解么?
OC源碼 —— autoreleasepool
官方runtime源碼