1. 程式人生 > IOS開發 >自動釋放池AutoreleasePool的探究

自動釋放池AutoreleasePool的探究

一、autoreleasepool概念

autoreleasepool本質是自動延遲物件的釋放,即物件使用完之後,它不會立即釋放,而是加入到釋放池,等到某個合適的時刻,對釋放池中的物件進行統一釋放。

官方檔案對主執行緒的自動釋放池有這麼一段描述

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop,and drains it at the end,thereby releasing any autoreleased objects generated while processing an event.

二、ARCMRCautoreleasepool的區別

MRC下需要手動管理自動釋放池的建立和釋放,ARC下只需要使用@autoreleasepool將對應的程式碼包含起來即可。


- (void)MRCTest {

    Person *person;
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    person = [[Person alloc] initWithName:@"jam" age:24];
    [person autorelease];
    NSLog(@"before pool release person: %@"
,person); [pool release]; NSLog(@"after pool release person: %@",person); //crash } 輸出結果: before pool release person: name:jam,age:24 crash ... - (void)ARCTest { Person *person; @autoreleasepool { person = [[Person alloc] initWithName:@"jam" age:24]; NSLog(@"before end release pool person: %@"
,person); } NSLog(@"after end release pool person: %@",person); } 輸出結果: before end release pool person: name:jam,age:24 after end release pool person: name:jam,age:24 複製程式碼

根據日誌輸出得知:MRC下呼叫自動釋放池release方法後,會對在autorelease物件進行釋放,因此,此後訪問的person變數為野指標,再去訪問自然會導致crash。而ARC下,@autoreleasepool並不會立即在結束括號符後,立即釋放person變數,而是會在一個合適的時間點。具體是在什麼時候,下面會講解到。

ps:x-code下對特定檔案設定使用MRC的方式:-fno-objc-arc

三、autoreleasepoolrunloop的關係

在斷點除錯中,使用po [NSRunLoop currentLoop]

由上圖可知:自動釋放池在runloop中註冊了兩個observer,分別都會以_wrapRunLoopWithAutoreleasePoolHandler進行回撥。不過兩個observer中的activitiesorder有些不同。

a. 首先看activities的區別:

typedef CF_OPTIONS(CFOptionFlags,CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),kCFRunLoopBeforeTimers = (1UL << 1),kCFRunLoopBeforeSources = (1UL << 2),kCFRunLoopBeforeWaiting = (1UL << 5),kCFRunLoopAfterWaiting = (1UL << 6),kCFRunLoopExit = (1UL << 7),kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製程式碼

第一個observeractivities0x01,即kCFRunLoopEntry,第二個observeractivities0xa0(轉換為二進位制為10100000),即kCFRunLoopBeforeWaiting | kCFRunLoopExit

b. 兩者order的區別,這裡的order表示的是runloop執行事件的優先順序。

order = -2147483647
order = 2147483647

int32 max: 2147483647
int32 min: -2147483648
複製程式碼

根據上面activitiesorder的對比,得知:

第一個observerrunloop監聽kCFRunLoopEntry時的優先順序為-2147483647(優先順序最高),即保證該observer回撥會發生在其他事件回撥之前。

第二個observerrunloop監聽kCFRunLoopBeforeWaiting | kCFRunLoopExit時的優先順序為2147483647,即保證該observer回撥會發生在其他事件回撥之後

這兩個observer分別在回撥時對自動釋放池進行了什麼操作呢?我們通過一個小例子來看看

Person *p;
//此處打斷點
p = [[Person alloc] initWithName:@"jam" age:24];
NSLog(@"p: %@",p);
複製程式碼

我們先在宣告臨時變數p處設定一個斷點,然後使用watchpoint set variable p命令監測變數p的變化,然後繼續執行程式,會不斷觸發到斷點,其中會在某個時刻分別顯示這麼兩段內容:

CoreFoundation`objc_autoreleasePoolPush:
-> 0x107e6a2fc <+0>: jmpq *0x1e88d6(%rip) ; (void *)0x000000010a9bd50f: objc_autoreleasePoolPush

CoreFoundation`objc_autoreleasePoolPop:
-> 0x107e6a2f6 <+0>: jmpq *0x1e88d4(%rip) ; (void *)0x000000010a9bd5b3: objc_autoreleasePoolPop
複製程式碼

很明顯這兩段內容是跟自動釋放池相關,分別對應釋放池的pushpop操作,而這兩個操作其實就是通過上面兩個observer的回撥之後的相關呼叫。(這兩者的關聯的確沒有什麼很好的證據證明,只能說是根據上面的例子推測而來)

因此,當runloop進入kCFRunLoopEntry時,自動釋放池會進行push操作,當runloop進入kCFRunLoopBeforeWaiting | kCFRunLoopExit狀態時,自動釋放池會進行pop操作。即系統在每一個runloop迭代中都加入了自動釋放池push和pop

四、@autoreleasepool的原理

通過使用clang編譯器對main.m檔案進行重新改寫為cpp檔案來一探究竟。

clang -rewrite-objc main.m
複製程式碼

執行後,發現會出錯,提示fatal error: 'UIKit/UIKit.h' file not found,此時,可以通過下面的命令來解決:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
複製程式碼

其實這裡主要是通過-isysroot選項指定了編譯所使用的的SDK目錄,即x-code下的SDK目錄。

//.m
int main(int argc,char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
    // Setup code that might create autoreleased objects goes here.
    appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc,argv,nil,appDelegateClassName);
}

//.cpp
int main(int argc,char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    appDelegateClassName = NSStringFromClass(((Class (*)(id,SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"),sel_registerName("class")));
    }
    return UIApplicationMain(argc,__null,appDelegateClassName);
}
複製程式碼

可以看到,生成後的cpp檔案中,新增了一個__AtAutoreleasePool結構體的變數

struct __AtAutoreleasePool {
    __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
    void * atautoreleasepoolobj;
};
複製程式碼

根據這個結構體的定義,可以看出在初始化時,會呼叫objc_autoreleasePoolPush()方法,在其解構函式,即該結構體例項銷燬時,會呼叫objc_autoreleasePoolPop(atautoreleasepoolobj)方法。

五、objc_autoreleasePoolPushobjc_autoreleasePoolPop的原理

在上面runloop@autorelesepool的探究過程中,最後都會停留到這兩個方法中,接下來,我們通過檢視原始碼來探究下這兩個方法具體做了哪些工作。(ps:可以在這裡下載可編譯的runtime原始碼)

void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
複製程式碼

根據上面的程式碼,可以看到pushpop操作分別呼叫了AutoreleasePoolPage的類方法。我們先看下AutoreleasePoolPage的定義:

class AutoreleasePoolPage : private AutoreleasePoolPageData
{...}

struct AutoreleasePoolPageData
{
magic_t const magic; //檢查完整性的校驗
__unsafe_unretained id *next; 
pthread_t const thread; //當前執行緒
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
複製程式碼

這裡比較值得關注的有:

a. parentchild變數構成雙向連結串列

b. next變數作為指向新新增autorelease物件的下一個位置,用於以棧的形式儲存

自動釋放池資料結構如上所示:雙連結串列+棧

瞭解完AutoreleasePoolPage的結構後,我們來分別細看下pushpop操作

push操作

static inline void *push() 
{
    id *dest;
    if (slowpath(DebugPoolAllocation)) { //debug模式下會直接生成一個新的page
    // 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;
}

#define POOL_BOUNDARY nil
複製程式碼

這裡會根據是否為debug模式,來進行不同的處理,這裡可以暫時忽略debug模式下的處理,即呼叫autoreleaseFast方法,並傳入一個nil物件,最後返回dest物件作為push方法的返回值。


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);
    }
}
複製程式碼

a. 首先它通過hotPage方法獲取到當前的page,若page存在且空間未滿,則將obj新增到page中。

b. 若page存在但空間已經滿了,則需要新建一個子page來儲存obj

c. 若page不存在,則建立一個新page來儲存obj

  • 當前page的獲取和儲存(這裡的當前page指的是AutoreleasePoolPage連結串列中當前所處於的節點page)
//獲取page
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;
}

//設定page
static inline void setHotPage(AutoreleasePoolPage *page) 
{
    if (page) page->fastcheck();
    tls_set_direct(key,(void *)page);
}

//AutoreleasePoolPage宣告內
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
複製程式碼

可以看到兩者分別呼叫tls_get_directtls_set_direct方法對page分別進行讀取和儲存。


static inline void *tls_get_direct(tls_key_t k)
{ 
    ASSERT(is_valid_direct_key(k));
    if (_pthread_has_direct_tsd()) {
    return _pthread_getspecific_direct(k);
    } else {
    return pthread_getspecific(k);
    }
}

static inline void tls_set_direct(tls_key_t k,void *value) 
{ 
    ASSERT(is_valid_direct_key(k));
    if (_pthread_has_direct_tsd()) {
    _pthread_setspecific_direct(k,value);
    } else {
    pthread_setspecific(k,value);
    }
}
複製程式碼

這裡使用了TLS(Thread Local Storage)執行緒區域性變數進行儲存,也就是說使用當前執行緒的區域性儲存空間對page進行儲存,這樣實現了執行緒和自動釋放池的關聯,不同執行緒的自動釋放池也是獨立的,互不幹擾

  • page空間不足的處理
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;
    else page = new AutoreleasePoolPage(page);
    } while (page->full());
    setHotPage(page);
    return page->add(obj);
}

複製程式碼

如上,若當前page空間不足,則不斷往後遍歷,直到找到有空間的page,若找到最後也沒有,則建立一個子page,並更新當前page節點,以便下一次可以直接新增(而不需要遍歷查詢)

  • page不存在的情況
static __attribute__((noinline))
    ....
    // 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);
    }
    // Push the requested object or pool.
    return page->add(obj);
}
複製程式碼

如上,page不存在的情況,會建立一個新page(作為連結串列的頭部節點),並更新到TLS中。

  • add操作:不管上面哪種情況,最後都會呼叫add方法將物件新增到對應的page
id *add(id obj)
{
    ASSERT(!full());
    unprotect();
    id *ret = next; // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect();
    return ret;
}
複製程式碼

上面提到過*next為新新增物件的位置,所以這裡將*next的賦值為當前物件,並移動到下一個位置。

  • autoreleaseFast方法的呼叫

a. AutoreleasePoolPage:push方法,傳入POOL_BOUNDARY(nil)物件

當呼叫push方法時,都會傳入一個nil物件,作為“哨兵物件”,以便標識每次pushpop之間新增的物件區間,這樣當執行pop操作時,就能準確釋放對應的物件(直到“哨兵”位置)。

如上,當進行pop操作時,會將obj2-5的物件進行釋放。

b. AutoreleasePoolPage:autorelease方法,傳入實際的obj物件

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;
}
複製程式碼

在ARC下,編譯器會在適當的位置插入autorelease方法。因此,會將物件自動新增到自動釋放池中。

pop操作

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.
        page = coldPage();
        token = page->begin();
    } else {
        page = pageForPointer(token);
    }
    stop = (id *)token;
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin() && !page->parent) {
        // 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);
        }
    }
    
    if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
        return popPageDebug(token,page,stop);
    }   
    return popPage<false>(token,stop);
}
複製程式碼
  1. 這裡傳入的引數token為上面push操作返回的,即push操作後,返回的"哨兵"物件的指標。

  2. EMPTY_POOL_PLACEHOLDER是對只有1個pool情況下的優化,可以先不考慮該細節。

  3. 通過pageForPointer方法獲取當前到page

  4. if (*stop != POOL_BOUNDARY),根據上面的第一點,可以知道,token應該為p操作完後,返回的“哨兵”物件,若不是,則進行異常處理。

  • 獲取到“哨兵”物件所在的page
static AutoreleasePoolPage *pageForPointer(const void *p) 
{
    return pageForPointer((uintptr_t)p);
}

static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE;
    ASSERT(offset >= sizeof(AutoreleasePoolPage));
    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();
    return result;
}
複製程式碼

因為每一個page的大小是固定的,所以可以通過p % SIZE的方法獲取到偏移量,然後通過p - offset獲取到page的起始地址。

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
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (allowDebug && 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) {
        // 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方法進行釋放物件,釋放後,會根據page的空間進行調整,前兩個if判斷都是debug模式下,可以先不用管,最後一個else if其實就是對剩餘的空閒空間進行回收。

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) {
    // 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()) {
        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);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
    ASSERT(page->empty());
}
#endif
}
複製程式碼

這裡用while迴圈從當前page的不斷遍歷,直到next指向了stop

  1. 獲取到當前page,因為如果包含多個page,會順著連結串列往前遍歷page
  2. 當前page為空,則往前遍歷,並更新當前page
  3. 獲取到當前需要釋放的物件,然後將該位置設定為SCRIBBLEnext指標往前移。
  4. 最後,若當前物件不為“哨兵”物件,則對該物件進行釋放

具體流程如下圖所示:

六、autoreleasepoolNSThread的關係

兩者的關聯主要涉及的有兩個點:

a. autoreleasepool依賴於當前執行緒的TLS,這個上面也分析過了;

b. autoreleasepool在不同執行緒中的建立和釋放,這裡主要探討這個問題

  • 主執行緒中,系統已經在main.m中通過@autoreleasepool建立了自動釋放池,所以我們無需額外去建立和釋放了
int main(int argc,char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc,appDelegateClassName);
}
複製程式碼
  • 那麼在子執行緒中,我們是否需要像主執行緒一樣,使用@autoreleasepool方法進行建立和釋放呢?

在ARC中,我們知道編譯器會在合適的位置自動插入autorelease方法,而我們上面分析push操作的時候提到過autoreleaseFast方法也會在autorelease方法的時候呼叫。因此,不管我們有沒手動建立自動釋放池,它都會新增到autoreleasepool中。

NSObject *obj = [[NSObject alloc] init];

//編譯後:
NSObject *obj = [[NSObject alloc] init];
[obj autorelease];

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;
}
複製程式碼

自動釋放池的建立清楚了,再來看看它的釋放操作。我們知道主執行緒中的@autoreleasepool會通過objc_autoreleasePoolPop方法進行釋放。而在子執行緒中並沒有呼叫這樣的方法,那又要如何進行釋放呢?我們先看個例子:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil];
    [thread start];
}

- (void)threadRun {
    Person *p = [[Person alloc] initWithName:@"jam" age:24 date:[NSDate date]];
    self.person = p; //此處打斷點
    NSLog(@"run in %@",[NSThread currentThread]);
}
複製程式碼

self.person = p的位置打斷點,然後設定觀察物件watchpoint set variable p,再不斷執行,直到執行緒執行完,找到對應執行緒的斷點,可以看到:

點進去看,可以看到起呼叫過程:

這裡有個_pthread_tsd_cleanup函式的呼叫

void
_pthread_tsd_cleanup(pthread_t self)
{
	int i,j;
	void *param;
	for (j = 0;  j < PTHREAD_DESTRUCTOR_ITERATIONS;  j++)
	{
		for (i = 0;  i < _POSIX_THREAD_KEYS_MAX;  i++)
		{
			if (_pthread_keys[i].created && (param = self->tsd[i]))
			{
				self->tsd[i] = (void *)NULL;
				if (_pthread_keys[i].destructor)
				{
					(_pthread_keys[i].destructor)(param);
				}
			}
		}
	}
}
複製程式碼

很明顯,該函式會對當前執行緒的TLS的資源進行清除,遍歷所有pthread_key_t,呼叫其解構函式。我們知道autoreleasepool線上程中有對應的pthread_key_t

static pthread_key_t const key = AUTORELEASE_POOL_KEY;

static void init()
{
    int r __unused = pthread_key_init_np(AutoreleasePoolPage::key,AutoreleasePoolPage::tls_dealloc);
    ASSERT(r == 0);
}

static void tls_dealloc(void *p) 
{
    if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
        // No objects or pool pages to clean up here.
        return;
    }

    // reinstate TLS value while we work
    setHotPage((AutoreleasePoolPage *)p);

    if (AutoreleasePoolPage *page = coldPage()) {
        if (!page->empty()) objc_autoreleasePoolPop(page->begin());  // pop all of the pools
        if (slowpath(DebugMissingPools || DebugPoolAllocation)) {
            // pop() killed the pages already
        } else {
            page->kill();  // free all of the pages
        }
    }
    
    // clear TLS value so TLS destruction doesn't loop
    setHotPage(nil);
}
複製程式碼

因此,子執行緒中自動釋放池的建立和釋放都無需我們進行額外的操作。當然,在某些場景下,也可以手動通過@autoreleasepool進行建立和釋放。

七、autoreleasepoolenumerateObjectsUsingBlock

enumerateObjectsUsingBlock方法會自動在內部新增一個@autoreleasepool,以保證下一次迭代前清除臨時物件,從而降低記憶體峰值。

int main(int argc,char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        NSArray *arr = @[@"str1",@"str2",@"str3"];
        [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj,NSUInteger idx,BOOL * _Nonnull stop) {
            id o = obj; //此處設定斷點
            NSLog(@"obj: %@",o);
        }];
    }
    return UIApplicationMain(argc,nil,appDelegateClassName);
}
複製程式碼

我們通過在id o = obj位置設定斷點,然後新增觀察變數watchpoint set variable o,再執行程式,會發現每次迭代結束後,都會呼叫自動釋放池的releaseUnitl方法:

相關資料