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
ARC
與MRC
下autoreleasepool
的區別
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
autoreleasepool
與runloop
的關係在斷點調試中,使用
po [NSRunLoop currentLoop]
數據結構
由上圖可知:自動釋放池在runloop
中註冊了兩個observer,分別都會以_wrapRunLoopWithAutoreleasePoolHandler
進行回調。不過兩個observer中的activities
和order
有些不一樣。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
};
複製代碼
第一個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, argv, __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進行存儲,這樣實現了線程和自動釋放池的關聯,不一樣線程的自動釋放池也是獨立的,互不干擾。
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節點,以便下一次能夠直接添加(而不須要遍歷查找)
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, page, 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
。
SCRIBBLE
,next
指針往前移。具體流程以下圖所示:
autoreleasepool
與NSThread
的關係二者的關聯主要涉及的有兩個點:
a.
autoreleasepool
依賴於當前線程的TLS
,這個上面也分析過了;b.
autoreleasepool
在不一樣線程中的建立和釋放,這裏主要探討這個問題
@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
進行建立和釋放。
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, argv, nil, appDelegateClassName);
}
複製代碼
咱們經過在id o = obj
位置設置斷點,而後添加觀察變量watchpoint set variable o
,再運行程序,會發現每次迭代結束後,都會調用自動釋放池的releaseUnitl
方法: