原文:https://zhuanlan.zhihu.com/p/520047056
在原文基礎(chǔ)上增加了一些注釋。
說明:源碼在runtime源碼中,不同runloop源碼中。
使用
使用場景
在ARC下,AutoreleasePool主要應(yīng)用在大量創(chuàng)建臨時對象的場景,通過AutoreleasePool控制內(nèi)存峰值,是一個很好的選擇。
NSAutoreleasePool
在MRC可以調(diào)用NSAutoreleasePool使對象延遲釋放,在ARC下這個API已經(jīng)被禁用。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// ...
[pool release];
@autoreleasepool
除了NSAutoreleasePool還可以使用@autoreleasepool,并且蘋果推薦使用@autoreleasepool,因為這個API性能更好,在ARC下依然可以使用@autoreleasepool。
無論是MRC還是ARC,autorelease最大的作用,是在大量創(chuàng)建對象的同時,通過修飾讓內(nèi)存得到提前釋放,從而降低內(nèi)存峰值。
for (size_t i = 0; i < frameCount; i++) {
@autoreleasepool {
[bitmapRep setProperty:NSImageCurrentFrame withValue:@(i)];
float frameDuration = [[bitmapRep valueForProperty:NSImageCurrentFrameDuration] floatValue];
NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapRep.CGImage size:CGSizeZero];
SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:frameImage duration:frameDuration];
[frames addObject:frame];
}
}
__autoreleasing
在ARC下,需要被自動釋放的對象,可以用__autoreleasing修飾,讓對象延遲釋放。
+ (NSArray *)parseString:(NSString *)originalM3U8Str m3u8Host:(NSString *)m3u8url error:(NSError *__autoreleasing *)errorPtr;
源碼分析
__AtAutoreleasePool結(jié)構(gòu)體
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
//在創(chuàng)建的時候會執(zhí)行objc_autoreleasePoolPush函數(shù)
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
//在釋放的時候會執(zhí)行析構(gòu)函數(shù),并執(zhí)行objc_autoreleasePoolPop函數(shù)
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
@autoreleasepool本質(zhì)上會被系統(tǒng)轉(zhuǎn)換成C++的__AtAutoreleasePool結(jié)構(gòu)體,@autoreleasepool的大括號開始,對應(yīng)著objc_autoreleasePoolPush函數(shù)。大括號結(jié)束,對應(yīng)著objc_autoreleasePoolPop函數(shù)。通過clang命令將OC代碼轉(zhuǎn)成C++代碼,可以看到有一個__AtAutoreleasePool結(jié)構(gòu)體。
__AtAutoreleasePool結(jié)構(gòu)體在創(chuàng)建的時候會執(zhí)行objc_autoreleasePoolPush函數(shù),在釋放的時候會執(zhí)行析構(gòu)函數(shù),并執(zhí)行objc_autoreleasePoolPop函數(shù)。在這兩個函數(shù)內(nèi)部,會調(diào)用AutoreleasePoolPage的push和pop函數(shù)。
AutoreleasePoolPage
在運行時代碼中,objc_autoreleasePoolPop和objc_autoreleasePoolPush,都調(diào)用了AutoreleasePoolPage類的實現(xiàn)。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
說明:AutoreleasePoolPage的實現(xiàn)在runtime的源碼里。
在AutoreleasePoolPage的定義中,可以看到有parent和child的定義,當(dāng)page中對象太多存儲不下時,會創(chuàng)建其他的page對象來存儲,AutoreleasePoolPage的結(jié)構(gòu)是一個雙向鏈表。在插入新的autorelease對象時,也會從鏈表頭向后查找,直到找到未滿的page。
class AutoreleasePoolPage
{
magic_t const magic; // 校驗page的結(jié)構(gòu)是否完整
id *next; // 指向下一個可以存放autorelease對象的地址
pthread_t const thread; // 當(dāng)前所在的線程
AutoreleasePoolPage * const parent; // 當(dāng)前page的父節(jié)點
AutoreleasePoolPage *child; // 當(dāng)前page的子節(jié)點
uint32_t const depth; // page的深度
uint32_t hiwat;
}
AutoreleasePoolPage是一個C++的類,每個page占4096個字節(jié),也就是16進制的0x1000,也就是4kb的空間。這些空間中,其自身的成員變量只占56個字節(jié),也就是下面七個成員變量,每個占8字節(jié),總共56個字節(jié)。其他的四千多個字節(jié),都用來存放被autorelease修飾的對象內(nèi)存地址。
POOL_BOUNDARY

POOL_BOUNDARY的作用是,區(qū)分不同的自動釋放池,也就是不同的@autoreleasepool。調(diào)用push時,會傳入POOL_BOUNDARY并返回一個地址例如0x1038,0x1038是不存儲@autorelease對象的地址的,起到一個標(biāo)識作用,用來分割不同的@autoreleasepool。
調(diào)用pop時,會傳入end的地址,并從后到前調(diào)用對象的release方法,直到POOL_BOUNDARY為止。如果存在多個page,會從child的page的最末尾開始調(diào)用,直到POOL_BOUNDARY。page的結(jié)構(gòu)是一個棧結(jié)構(gòu),釋放的時候也是從棧頂開始釋放。
next指針指向棧頂,是棧里面很常見的一個設(shè)計。AutoreleasePoolPage和POOL_BOUNDARY的區(qū)別在于,AutoreleasePoolPage負(fù)責(zé)維護存儲區(qū)域,而POOL_BOUNDARY則負(fù)責(zé)分割存儲在page中的對象地址,以@autoreleasepool為單位進行分割。
多層嵌套
@autoreleasepool {
NSObject *p1 = [[NSObject alloc] init];
NSObject *p2 = [[NSObject alloc] init];
@autoreleasepool {
NSObject *p3 = [[NSObject alloc] init];
@autoreleasepool {
NSObject *p4 = [[NSObject alloc] init];
}
}
}
如果是多層@autoreleasepool的嵌套,會用同一個AutoreleasePoolPage對象。以下面的三個嵌套為例,在同一個page中的順序是下圖這樣。不同的@autoreleasepool以POOL_BOUNDARY做分割。

push
創(chuàng)建一個autoreleasePool之后,就會調(diào)用push函數(shù)。在push函數(shù)中會判斷是否調(diào)試模式下,如果調(diào)試模式會每次生成一個新的page。debug環(huán)境代碼可以直接忽略,只保留autoreleaseFast函數(shù)。
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
return dest;
}
autoreleaseFast
在函數(shù)內(nèi)部,會通過hotPage獲取當(dāng)前的page,hotPage函數(shù)內(nèi)部本質(zhì)上是一個page和key的映射。
- 如果
page不為空并且有空間,則調(diào)用page的add函數(shù)將對象添加到page中,并將POOL_BOUNDARY添加在當(dāng)前的位置。 - 如果
page已經(jīng)被創(chuàng)建但沒有空間,會調(diào)用autoreleaseFullPage函數(shù)創(chuàng)建新的page,并且將鏈表的末尾指向新創(chuàng)建的page。 - 如果
沒有創(chuàng)建page,則調(diào)用autoreleaseNoPage函數(shù)創(chuàng)建一個新的page,并且將當(dāng)前線程的hotPage設(shè)置為新創(chuàng)建的page。
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
autoreleaseFullPage
- 在autoreleaseFullPage函數(shù)中,會從
page的鏈表中,從前往后找到末尾的節(jié)點。 - 創(chuàng)建一個新的page,在創(chuàng)建函數(shù)AutoreleasePoolPage中會處理parent和child指針的問題,返回的page可以直接用。
- 調(diào)用
setHotPage將page設(shè)置到哈希表中,并且調(diào)用page的add函數(shù)將autorelease修飾的對象,添加到page中。
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
疑惑點:autorelease修飾的對象是指哪里
參考文章:http://www.itdecent.cn/p/d0558e4b0d21
說明:當(dāng)一個對象發(fā)送了autorelease消息,就是將當(dāng)前這個對象加入到AutoreleasePoolPage的棧頂next指向的位置
示例代碼如下:
// MRC
NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool drain];
// ARC
@autoreleasepool {
id obj = [NSObject alloc] init];
}
autoreleaseNoPage
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
return page->add(obj);
}
autoreleaseNoPage函數(shù)的核心代碼比較簡單,就是創(chuàng)建一個新的page,隨后設(shè)置POOL_BOUNDARY標(biāo)志,并且把對象添加進去。在函數(shù)中需要留意POOL_BOUNDARY標(biāo)志,很多地方都用來做page是否為空的判斷。
add
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next;
*next++ = obj;
protect();
return ret;
}
add函數(shù)比較簡單,核心邏輯就是將obj放入next指針的位置,并且對next指針進行++,指向下一個位置。*next++表示先用后加,先將obj存入next的地址,隨后+1。
pop
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
// 1.
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
page = hotPage();
if (!page) {
return setHotPage(nil);
}
page = coldPage();
token = page->begin();
} else {
page = pageForPointer(token);
}
// 2.
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
} else {
return badPop(token);
}
}
// 3.
return popPage<false>(token, page, stop);
}
調(diào)用pop函數(shù)時,有三步處理。
- 判斷autoreleasepool是否為空,通過EMPTY_POOL_PLACEHOLDER占位符判斷,為空則清空這個page。
- 傳入的stop是否不等于POOL_BOUNDARY標(biāo)識,如果不等于則可能是一個有問題的page。
- 調(diào)用popPage方法,釋放對象。
popPage
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
page->releaseUntil(stop);
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
- popPage函數(shù)核心代碼就是調(diào)用releaseUntil函數(shù),在最開始會調(diào)用releaseUntil函數(shù)去完成釋放操作。
- 按照page達到一半就擴容的原則,后面的if語句會判斷執(zhí)行pop后page鏈表的狀態(tài)。
2.1 如果少于半滿,就將子節(jié)點刪除。
2.2 如果大于半滿,則保留子節(jié)點,并刪除后面的節(jié)點。
releaseUntil
void releaseUntil(id *stop)
{
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();//獲取當(dāng)前的page
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);
}
}
setHotPage(this);
}
在releaseUntil函數(shù)內(nèi)部,核心邏輯是從當(dāng)前page,從后到前調(diào)用objc_release,釋放被autorelease修飾的對象。
獲取當(dāng)前的hotPage。
判斷page是否為空,如果為空則表示里面的對象被釋放完,則將page的父節(jié)點page設(shè)置為hotPage。
獲得上一個節(jié)點,->的算數(shù)優(yōu)先級比--要高,所以是先通過next獲取當(dāng)前節(jié)點地址,這是一個為空的待存入節(jié)點,隨后執(zhí)行--操作獲取上一個對象地址。
通過memset將上一個節(jié)點釋放。
判斷上一個節(jié)點是否占位符號POOL_BOUNDARY,如果不是則調(diào)用objc_release釋放對象。
在while循環(huán)結(jié)束后,將當(dāng)前page設(shè)置為hotPage。
autorelease
static inline id autorelease(id obj)
{
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
return obj;
}
對象調(diào)用autorelease方法會被編譯器轉(zhuǎn)換為objc_autoreleaseReturnValue方法,并且經(jīng)過多層調(diào)用,會來到底層的autorelease函數(shù)。
在這個函數(shù)中會判斷傳入的對象是否tagged pointer,因為tagged pointer沒有引用計數(shù)的概念。隨后會調(diào)用autoreleaseFast函數(shù),函數(shù)內(nèi)部調(diào)用add函數(shù)將obj對象加入到page中,并且會判斷是否需要創(chuàng)建新的page。
hotPage、coldPage
hotPage
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;
}
hotPage可以被理解為,page鏈表的末尾,也就是調(diào)用push函數(shù)被插入的位置。執(zhí)行hotPage函數(shù)獲取,以及調(diào)用setHotPage設(shè)置,都是操作鏈表的末尾page。
AutoreleasePoolPage對象和線程一一對應(yīng),并且都被存儲在tls的哈希表中。通過tls_get_direct函數(shù)并傳入key可以獲取到對應(yīng)的自動釋放池。
hotPage函數(shù)中的判斷是下面的定義,這個標(biāo)示意思是當(dāng)前page為空,也就是從未存儲過任何對象。是一個標(biāo)志位,下面是標(biāo)志位的定義。
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
coldPage
static inline AutoreleasePoolPage *coldPage()
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
coldPage只有獲取函數(shù),沒有設(shè)置函數(shù)。這是因為coldPage函數(shù)本質(zhì)上,就是尋找page鏈表的根節(jié)點,從源碼中的while循環(huán)可以看到。
調(diào)試
_objc_autoreleasePoolPrint
如果想調(diào)試自動釋放池,可以通過_objc_autoreleasePoolPrint私有API來進行。將項目改為MRC,并且在命令行項目中增加下面這些調(diào)試代碼。
int main(int argc, const char * argv[]) {
_objc_autoreleasePoolPrint(); // print1
@autoreleasepool {
_objc_autoreleasePoolPrint(); // print2
Person *p1 = [[[Person alloc] init] autorelease];
Person *p2 = [[[Person alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print3
}
_objc_autoreleasePoolPrint(); // print4
return 0;
}
打印結(jié)果如下,可以看到POOL_BOUNDARY在page中也占了一個位置。
objc[68122]: ############## (print1)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 0 releases pending. // 當(dāng)前自動釋放池中沒有任何對象
objc[68122]: [0x102802000] ................ PAGE (hot) (cold)
objc[68122]: ##############
objc[68122]: ############## (print2)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 1 releases pending. // 當(dāng)前自動釋放池中有1個對象,這個對象為POOL_BOUNDARY
objc[68122]: [0x102802000] ................ PAGE (hot) (cold)
objc[68122]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68122]: ##############
objc[68122]: ############## (print3)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 3 releases pending. // 當(dāng)前自動釋放池中有3個對象
objc[68122]: [0x102802000] ................ PAGE (hot) (cold)
objc[68122]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68122]: [0x102802040] 0x100704a10 HTPerson //p1
objc[68122]: [0x102802048] 0x10075cc30 HTPerson //p2
objc[68122]: ##############
objc[68156]: ############## (print4)
objc[68156]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68156]: 0 releases pending. // 當(dāng)前自動釋放池中沒有任何對象,因為@autoreleasepool作用域結(jié)束,調(diào)用pop方法釋放了對象
objc[68156]: [0x100810000] ................ PAGE (hot) (cold)
objc[68156]: ##############
UIApplicationMain
項目中經(jīng)常會看到下面的代碼,很多人的解釋是“這個autoreleasepool是為了釋放主線程的autorelease對象的”。但是,這個說法是錯誤的。autoreleasepool只負(fù)責(zé)自己作用域中添加的對象,而主線程在運行過程中,也會隱式創(chuàng)建autoreleasepool對象,這個pool是包含在main函數(shù)的pool里面的。
所以,主線程runloop每次執(zhí)行循環(huán)后,釋放的對象是主線程的。而main函數(shù)的autoreleasepool釋放的,是main函數(shù)中直接創(chuàng)建的對象。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
釋放時機
區(qū)分
如果是在viewDidLoad方法中創(chuàng)建一個autorelease對象,并不是在這個方法結(jié)束后釋放對象,這個說法是錯誤的。即便執(zhí)行到viewDidAppear,依然不會釋放對象。
被autorelease修飾的對象,釋放時機有兩種。
- 如果通過代碼添加一個autoreleasepool,在作用域結(jié)束時,隨著pool的釋放,就會釋放pool中的對象。這種情況是及時釋放的,并不依賴于runloop。
- 另一種就是由系統(tǒng)自動進行釋放,系統(tǒng)會在runloop開始的時候創(chuàng)建一個pool,結(jié)束的時候會對pool中的對象執(zhí)行release操作。
runloop
如果是系統(tǒng)創(chuàng)建的pool,需要手動開啟runloop,主線程默認(rèn)已經(jīng)開啟并運行,子線程需要調(diào)用currentRunLoop方法開啟并運行runloop,子線程中系統(tǒng)創(chuàng)建pool的流程才會正常工作。

包括主線程在內(nèi)的每個線程,如果在線程中使用到了AutoreleasePool,則會創(chuàng)建兩個Observer并添加到當(dāng)前線程的Runloop中,通過這兩個Observer進行對象的自動內(nèi)存管理。
// activities = 0x1,kCFRunLoopEntry
<CFRunLoopObserver 0x60000012f000 [0x1135c2bb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}
// activities = 0xa0,kCFRunLoopBeforeWaiting | kCFRunLoopExit
<CFRunLoopObserver 0x60000012ef60 [0x1135c2bb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}
首先會創(chuàng)建一個Observer并監(jiān)聽kCFRunLoopEntry消息,時機是在進入Runloop前,此Observer的優(yōu)先級設(shè)置為-2147483647的最高優(yōu)先級,以保證回調(diào)發(fā)生在Runloop其他事件前。
然后創(chuàng)建另一個Observer,并監(jiān)聽kCFRunLoopBeforeWaiting和kCFRunLoopExit消息,時機分別在進入Runloop休眠和退出Runloop時,將Observer的優(yōu)先級設(shè)置為2147483647,以保證回調(diào)發(fā)生在Runloop其他事件之后。
兩個Observer都有相同的回調(diào)函數(shù)_wrapRunLoopWithAutoreleasePoolHandler,在第一次回調(diào)時會在內(nèi)部調(diào)用_objc_autoreleasePoolPush函數(shù),創(chuàng)建自動釋放池。
在kCFRunLoopBeforeWaiting將要進入休眠前,調(diào)用_objc_autoreleasePoolPop函數(shù)釋放自動釋放池中的對象,并調(diào)用_objc_autoreleasePoolPush函數(shù)創(chuàng)建一個新的釋放池。在kCFRunLoopExit將要退出Runloop時調(diào)用_objc_autoreleasePoolPop函數(shù),釋放自動釋放池中的對象。
