自動釋放池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.
二、ARC
與MRC
下autoreleasepool
的區別
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
三、autoreleasepool
與runloop
的關係
在斷點除錯中,使用
po [NSRunLoop currentLoop]
由上圖可知:自動釋放池在runloop
中註冊了兩個observer,分別都會以_wrapRunLoopWithAutoreleasePoolHandler
進行回撥。不過兩個observer中的activities
和order
有些不同。
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
};
複製程式碼
第一個observer
的activities
為0x01
,即kCFRunLoopEntry
,第二個observer
的activities
為0xa0
(轉換為二進位制為10100000
),即kCFRunLoopBeforeWaiting | kCFRunLoopExit
。
b. 兩者order
的區別,這裡的order
表示的是runloop
執行事件的優先順序。
order = -2147483647
order = 2147483647
int32 max: 2147483647
int32 min: -2147483648
複製程式碼
根據上面activities
和order
的對比,得知:
第一個observer
在runloop
監聽kCFRunLoopEntry
時的優先順序為-2147483647
(優先順序最高),即保證該observer回撥會發生在其他事件回撥之前。
第二個observer
在runloop
監聽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
複製程式碼
很明顯這兩段內容是跟自動釋放池相關,分別對應釋放池的push
和pop
操作,而這兩個操作其實就是通過上面兩個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_autoreleasePoolPush
和objc_autoreleasePoolPop
的原理
在上面
runloop
和@autorelesepool
的探究過程中,最後都會停留到這兩個方法中,接下來,我們通過檢視原始碼來探究下這兩個方法具體做了哪些工作。(ps:可以在這裡下載可編譯的runtime
原始碼)
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
複製程式碼
根據上面的程式碼,可以看到push
和pop
操作分別呼叫了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. parent
和child
變數構成雙向連結串列
b. next
變數作為指向新新增autorelease
物件的下一個位置,用於以棧的形式儲存
自動釋放池資料結構如上所示:雙連結串列+棧
瞭解完AutoreleasePoolPage
的結構後,我們來分別細看下push
和pop
操作
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_direct
和tls_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物件,作為“哨兵物件”,以便標識每次
push
和pop
之間新增的物件區間,這樣當執行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);
}
複製程式碼
-
這裡傳入的引數
token
為上面push
操作返回的,即push
操作後,返回的"哨兵"物件的指標。 -
EMPTY_POOL_PLACEHOLDER
是對只有1個pool情況下的優化,可以先不考慮該細節。 -
通過
pageForPointer
方法獲取當前到page -
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
。
- 獲取到當前page,因為如果包含多個page,會順著連結串列往前遍歷page
- 當前page為空,則往前遍歷,並更新當前page
- 獲取到當前需要釋放的物件,然後將該位置設定為
SCRIBBLE
,next
指標往前移。 - 最後,若當前物件不為“哨兵”物件,則對該物件進行釋放
具體流程如下圖所示:
六、autoreleasepool
與NSThread
的關係
兩者的關聯主要涉及的有兩個點:
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
進行建立和釋放。
七、autoreleasepool
與enumerateObjectsUsingBlock
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
方法: