自動釋放池AutoreleasePool的探究

1、autoreleasepool概念

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

官方文檔對主線程的自動釋放池有這麼一段描述c++

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.git

2、ARCMRCautoreleasepool的區別

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

- (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變量,而是會在一個合適的時間點。具體是在何時,下面會講解到。objective-c

ps:x-code下對特定文件設置使用MRC的方式:-fno-objc-arc bash

3、autoreleasepoolrunloop的關係

在斷點調試中,使用po [NSRunLoop currentLoop]數據結構

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

a. 首先看activities的區別:less

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

4、@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, argv, __null, appDelegateClassName);
}
複製代碼

能夠看到,生成後的cpp文件中,新增了一個__AtAutoreleasePool結構體的變量

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

根據這個結構體的定義,能夠看出在初始化時,會調用objc_autoreleasePoolPush()方法,在其析構函數,即該結構體實例銷燬時,會調用objc_autoreleasePoolPop(atautoreleasepoolobj)方法。

5、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, page, 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. 最後,若當前對象不爲「哨兵」對象,則對該對象進行釋放

具體流程以下圖所示:

6、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, argv, nil, 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進行建立和釋放。

7、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, argv, nil, appDelegateClassName);
}
複製代碼

咱們經過在id o = obj位置設置斷點,而後添加觀察變量watchpoint set variable o,再運行程序,會發現每次迭代結束後,都會調用自動釋放池的releaseUnitl方法:

相關資料

相關文章
相關標籤/搜索