概念
AutoreleasePool(自動釋放池)是OC中的一種內(nèi)存自動回收機制,它可以延遲加入AutoreleasePool中的變量release的時機。在正常情況下,創(chuàng)建的變量會在超出其作用域的時候release,但是如果將變量加入AutoreleasePool,那么release將延遲執(zhí)行。

-
App啟動后,蘋果在主線程 RunLoop里注冊了兩個 Observer,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler(),從程序啟動到加載完成,主線程對應的runloop會處于休眠狀態(tài),等待用戶交互來喚醒runloop- 第一個
Observer監(jiān)視的事件是Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用_objc_autoreleasePoolPush()創(chuàng)建自動釋放池。優(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()來釋放自動釋放池。優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。
- 第一個
- 用戶的每一次交互都會啟動一次
runloop,用于處理用戶的所有點擊、觸摸事件等 -
runloop在監(jiān)聽到交互事件后,就會創(chuàng)建自動釋放池,并將所有延遲釋放的對象添加到自動釋放池中 -
主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會被RunLoop創(chuàng)建好的AutoreleasePool環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建Pool了。 - 在一次完整的runloop結束之前,會向自動釋放池中所有對象發(fā)送release消息,然后銷毀自動釋放池
clang 分析
int main(int argc, char * argv[]) {
@autoreleasepool {
}
}
通過clang編譯成cpp文件插看實現(xiàn):
xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m
int main(int argc, char * argv[]) {
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
}
}
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
通過代碼可以看出autoreleasepool在底層實際是調(diào)用__AtAutoreleasePool,而__AtAutoreleasePool本質(zhì)上是一個結構體,其內(nèi)部包含構造函數(shù)__AtAutoreleasePool()與析構函數(shù)~__AtAutoreleasePool(),在{}作用域結束后會自動調(diào)用析構函數(shù),以便及時創(chuàng)建銷毀
匯編分析
struct LGTest {
LGTest(){
printf("1123 - %s",__func__);
}
~LGTest(){
printf("5667 - %s",__func__);
}
};
int main(int argc, char * argv[]) {
LGTest LGTest;
}
在main函數(shù)中添加斷點查看匯編

可以看出跟clang編譯后一樣都是經(jīng)過objc_autoreleasePoolPush與objc_autoreleasePoolPop
底層原理
在objc源碼中是這樣注釋的
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.
每個指針都是要釋放的對象,或者是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.
池令牌是指向該池的POOL_BOUNDARY的指針。彈出池后,將釋放比哨點更熱的每個對象。
- The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
堆棧分為兩個雙向鏈接的頁面列表。根據(jù)需要添加和刪除頁面。
- Thread-local storage points to the hot page, where newly autoreleased objects are stored.
線程本地存儲指向熱頁面,該頁面存儲新自動釋放的對象。
查看源碼
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
通過代碼可以看出push與pop操作都是基于AutoreleasePoolPage,根據(jù)其定義看出自動釋放池是頁結構,每頁的大小為4096字節(jié)
//************宏定義************
#define PAGE_MIN_SIZE PAGE_SIZE
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096 /* bytes per 80386 page */
//************類定義************
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
//頁的大小
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MIN_SIZE; // size and alignment, power of 2
#endif
private:
...
//構造函數(shù)
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),//開始存儲的位置
objc_thread_self(),//傳的是當前線程,當前線程時通過tls獲取的
newParent,
newParent ? 1+newParent->depth : 0,//如果是第一頁深度為0,往后是前一個的深度+1
newParent ? newParent->hiwat : 0)
{...}
//析構函數(shù)
~AutoreleasePoolPage() {...}
...
//頁的開始位置
id * begin() {...}
//頁的結束位置
id * end() {...}
//頁是否為空
bool empty() {...}
//頁是否滿了
bool full() {...}
//頁的存儲是否少于一半
bool lessThanHalfFull() {...}
//添加釋放對象
id *add(id obj){...}
//釋放所有對象
void releaseAll() {...}
//釋放到stop位置之前的所有對象
void releaseUntil(id *stop) {...}
//殺掉
void kill() {...}
//釋放本地線程存儲空間
static void tls_dealloc(void *p) {...}
//獲取AutoreleasePoolPage
static AutoreleasePoolPage *pageForPointer(const void *p) {...}
static AutoreleasePoolPage *pageForPointer(uintptr_t p) {...}
//是否有空池占位符
static inline bool haveEmptyPoolPlaceholder() {...}
//設置空池占位符
static inline id* setEmptyPoolPlaceholder(){...}
//獲取當前操作頁
static inline AutoreleasePoolPage *hotPage(){...}
//設置當前操作頁
static inline void setHotPage(AutoreleasePoolPage *page) {...}
//獲取coldPage
static inline AutoreleasePoolPage *coldPage() {...}
//快速釋放
static inline id *autoreleaseFast(id obj){...}
//添加自動釋放對象,當頁滿的時候調(diào)用這個方法
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {...}
//添加自動釋放對象,當沒頁的時候使用這個方法
static __attribute__((noinline))
id *autoreleaseNoPage(id obj){...}
//創(chuàng)建新頁
static __attribute__((noinline))
id *autoreleaseNewPage(id obj) {...}
public:
//自動釋放
static inline id autorelease(id obj){...}
//入棧
static inline void *push() {...}
//兼容老的 SDK 出棧方法
__attribute__((noinline, cold))
static void badPop(void *token){...}
//出棧頁面
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop){...}
__attribute__((noinline, cold))
static void
popPageDebug(void *token, AutoreleasePoolPage *page, id *stop){...}
//出棧
static inline void
pop(void *token){...}
static void init(){...}
//打印
__attribute__((noinline, cold))
void print(){...}
//打印所有
__attribute__((noinline, cold))
static void printAll(){...}
//打印Hiwat
__attribute__((noinline, cold))
static void printHiwat(){...}
根據(jù)代碼可以看出AutoreleasePoolPage繼承與AutoreleasePoolPageData
struct AutoreleasePoolPageData
{
magic_t const magic; // 內(nèi)存大小為m[4];所占內(nèi)存(即4*4=16字節(jié))
__unsafe_unretained id *next;// 8字節(jié)
pthread_t const thread;// 8字節(jié)
AutoreleasePoolPage * const parent;// 8字節(jié)
AutoreleasePoolPage *child;// 8字節(jié)
uint32_t const depth;// 4字節(jié)
uint32_t hiwat;// 4字節(jié)
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)
{
}
};
-
magic用來校驗AutoreleasePoolPage的結構是否完整; -
next指向最新添加的autoreleased對象的下一個位置,初始化時指向
begin(); -
thread指向當前線程; -
parent指向父結點,第一個結點的parent值為nil; -
child指向子結點,最后一個結點的child值為nil; -
depth代表深度,從0開始,往后遞增1; -
hiwat代表high water mark最大入棧數(shù)量標記
根據(jù)變量看出其中包含parent與child,兩者相互存在關系,可以得出是一個雙向鏈表結構
push
//入棧
static inline void *push()
{
id *dest;
//判斷是否有pool
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page.自動釋放池從新池頁面開始
//如果沒有,則創(chuàng)建
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
//壓棧一個POOL_BOUNDARY,即壓棧哨兵
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
通過代碼可以看出,通判斷是否存在pool,如果存在直接壓棧,如果沒有則需要創(chuàng)建
- 創(chuàng)建方法
//創(chuàng)建新頁
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
//獲取當前操作頁
AutoreleasePoolPage *page = hotPage();
//如果存在,則壓棧對象
if (page) return autoreleaseFullPage(obj, page);
//如果不存在,則創(chuàng)建頁
else return autoreleaseNoPage(obj);
}
//獲取當前操作頁
static inline AutoreleasePoolPage *hotPage()
{
//獲取當前頁
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
//如果是一個空池,則返回nil,否則,返回當前線程的自動釋放池
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
//******** autoreleaseNoPage方法 ********
static __attribute__((noinline))
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;
//判斷是否是空占位符,如果是,則壓棧哨兵標識符置為YES
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;
}
//如果對象不是哨兵對象,且沒有Pool,則報錯
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",
objc_thread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
//如果對象是哨兵對象,且沒有申請自動釋放池內(nèi)存,則設置一個空占位符存儲在tls中,其目的是為了節(jié)省內(nèi)存
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {//如果傳入?yún)?shù)為哨兵
// 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();//設置空的占位符
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
//初始化第一頁
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
//設置page為當前聚焦頁
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
//壓棧哨兵的標識符為YES,則壓棧哨兵對象
if (pushExtraBoundary) {
//壓棧哨兵
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
//壓棧對象
return page->add(obj);
}
通過代碼得知邏輯為:
- 通過
hotPage獲取當前操作頁- 如果存在,則通過
autoreleaseFullPage直接將對象進行壓棧 - 如果不存在,則通過
autoreleaseNoPage創(chuàng)建頁
- 如果存在,則通過
//********begin()********
//頁的開始位置
id * begin() {
//等于 首地址+56(AutoreleasePoolPage類所占內(nèi)存大?。? return (id *) ((uint8_t *)this+sizeof(*this));
}
根據(jù)代碼得知begin()是頁的起始位置即存儲對象的起始位置,由于AutoreleasePoolPageData是一個結構體,存儲對象的話需要將AutoreleasePoolPageData的地址進行平移結構體的大小才能開始存儲,上面分析其屬性時可以得出共56字節(jié),故而begin()地址為首地址+56
驗證
由于在ARC模式下,是無法手動調(diào)用autorelease,所以將Demo切換至MRC模式(Build Settings -> Objectice-C Automatic Reference Counting設置為NO)

//************打印自動釋放池結構************
extern void _objc_autoreleasePoolPrint(void);
//************運行代碼************
int main(int argc, const char * argv[]) {
@autoreleasepool {
//循環(huán)創(chuàng)建對象,并加入自動釋放池
for (int i = 0; i < 5; i++) {
NSObject *objc = [[NSObject alloc] sutorelease];
}
//調(diào)用
_objc_autoreleasePoolPrint();
}
}

根據(jù)代碼可以看出,本質(zhì)上應該只打印5個對象,但實際上打印內(nèi)容多出一個POOL,其就表示哨兵,為了防止越界
并且查看對應地址時,可以看出起始位置為0x100817000,但是哨兵的位置為0x100817038,根據(jù)十六進制計算得出38=3*16+8 = 56,這樣也可以驗證begin()起始位置是經(jīng)過內(nèi)存平移56個字節(jié)
通過更改i的大小可以發(fā)現(xiàn),當i為504時正好一頁存儲,而當i為505時,就需要兩頁,但是第二頁中只有對象,并沒有哨兵,這樣就可以得知,哨兵在自動釋放池中只存在一個,且在第一頁,每頁存儲的數(shù)據(jù)為505個,第一頁為504個對象+哨兵,其他頁為505個對象,因為每頁內(nèi)存大小為4096字節(jié),就可以得出:505* 8 = 4040 + 56 = 4096

壓棧
根據(jù)上面push代碼可以得知,當沒有頁時是創(chuàng)建頁,而當有頁時,直接壓棧
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);
}
}
壓棧時,首先判斷當前頁是否滿了,如果未滿直接壓棧,如果滿了則需創(chuàng)建新頁面
//添加自動釋放對象,當頁滿的時候調(diào)用這個方法
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-while遍歷循環(huán)查找界面是否滿了
do {
//如果子頁面存在,則將頁面替換為子頁面
if (page->child) page = page->child;
//如果子頁面不存在,則新建頁面
else page = new AutoreleasePoolPage(page);
} while (page->full());
//設置為當前操作頁面
setHotPage(page);
//對象壓棧
return page->add(obj);
}
- 添加釋放對象: 底層是實現(xiàn)是通過
next指針存儲釋放對象,并將next指針遞增,表示下一個釋放對象存儲的位置。從這里可以看出頁是通過棧結構存儲
//添加釋放對象
id *add(id obj)
{
ASSERT(!full());
unprotect();
//傳入對象存儲的位置
id *ret = next; // faster than `return next-1` because of aliasing
//將obj壓棧到next指針位置,然后next進行++,即下一個對象存儲的位置
*next++ = obj;
protect();
return ret;
}
pop
在objc_autoreleasePoolPop方法中有個參數(shù),在clang分析時,發(fā)現(xiàn)傳入的參數(shù)是push壓棧后返回的哨兵對象,即ctxt,其目的是避免出?;靵y,防止將別的對象出棧
//出棧
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
//判斷對象是否是空占位符
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
//如果當是空占位符
// Popping the top-level placeholder pool.
//獲取當前頁
page = hotPage();
if (!page) {
// Pool was never used. Clear the placeholder.
//如果當前頁不存在,則清除空占位符
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
//如果當前頁存在,則將當前頁設置為coldPage,token設置為coldPage的開始位置
page = coldPage();
token = page->begin();
} else {
//獲取token所在的頁
page = pageForPointer(token);
}
stop = (id *)token;
//判斷最后一個位置,是否是哨兵
if (*stop != POOL_BOUNDARY) {
//最后一個位置不是哨兵,即最后一個位置是一個對象
if (stop == page->begin() && !page->parent) {
//如果是第一個位置,且沒有父節(jié)點,什么也不做
// 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 {
//如果是第一個位置,且有父節(jié)點,則出現(xiàn)了混亂
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
//出棧頁
return popPage<false>(token, page, stop);
}
傳入的allowDebug為false,則通過releaseUntil出棧當前頁stop位置之前的所有對象,即向棧中的對象發(fā)送release消息,直到遇到傳入的哨兵對象
//出棧頁面
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
if (allowDebug && PrintPoolHiwat) printHiwat();
//出棧當前操作頁面對象
page->releaseUntil(stop);
// memory: delete empty children 刪除空子項
if (allowDebug && DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
//調(diào)試期間刪除每個特殊情況下的所有池
//獲取當前頁面的父節(jié)點
AutoreleasePoolPage *parent = page->parent;
//將當前頁面殺掉
page->kill();
//設置操作頁面為父節(jié)點頁面
setHotPage(parent);
}
else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
//特殊情況:調(diào)試丟失的自動釋放池時刪除pop(top)的所有內(nèi)容
page->kill();
setHotPage(nil);
}
else if (page->child) {
// 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();
}
}
}
進入releaseUntil實現(xiàn),主要是通過循環(huán)遍歷,判斷對象是否等于stop,其目的是釋放stop之前的所有的對象,
首先通過獲取page的next釋放對象(即page的最后一個對象),并對next進行遞減,獲取上一個對象
判斷是否是哨兵對象,如果不是則自動調(diào)用objc_release釋放
//釋放到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
//判斷下一個對象是否等于stop,如果不等于,則進入while循環(huán)
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects 每次從hotPage()重新啟動,以防-release自動釋放更多對象
//獲取當前操作頁面,即hot頁面
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
//如果當前頁是空的
while (page->empty()) {
//將page賦值為父節(jié)點頁
page = page->parent;
//并設置當前頁為父節(jié)點頁
setHotPage(page);
}
page->unprotect();
//next進行--操作,即出棧
id obj = *--page->next;
//將頁索引位置置為SCRIBBLE,表示已經(jīng)被釋放
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
}
進入kill實現(xiàn),主要是銷毀當前頁,將當前頁賦值為父節(jié)點頁,并將父節(jié)點頁的child對象指針置為nil
//銷毀
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
AutoreleasePoolPage *page = this;
//獲取最后一個頁
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
//子節(jié)點 變成 父節(jié)點
page = page->parent;
if (page) {
page->unprotect();
//子節(jié)點為nil
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
總結
-
在自動釋放池的
壓棧(即push)操作中
當沒有pool,即只有空占位符(存儲在tls中)時,則創(chuàng)建頁,壓棧哨兵對象
在頁中壓棧普通對象主要是通過next指針遞增進行的,
當頁滿了時,需要設置頁的child對象為新建頁
2251862-45e9a6ccea6bf412.png -
在自動釋放池的出棧(即
pop)操作中
在頁中出棧普通對象主要是通過next指針遞減進行的,
當頁空了時,需要賦值頁的parent對象為當前頁
2251862-cf9d759e28bf646e-2.png
圖片來源月月

