為什么需要 AutoreleasePool
1. 延長對象生命周期
我們都知道,系統內存是有限的,要想系統一直正常高效運行著,就需要我們合理地管理內存,不需要的內存就應該及時釋放。在 Objective-C 早期年代采用的是 MRC 來管理內存,需要我們自己在合適的位置申請和釋放內存。在 LLVM 3.0 開始,Objective-C 引入 ARC(自動引用計數),就無需要我們手動管理內存了,系統會自動管理內存:
- (void)run {
id __strong obj = [[NSObject alloc] init]; //生成并持有對象,retainCount = 1
//... 使用 obj
} //obj 超出作用域,強引用失效,retainCount = 0
正常情況下,使用 release 都能幫助我們及時回收內存,防止內存泄漏。我們接著看下一種情況:
- (NSObject *)getObj {
id __strong obj = [[NSObject alloc] init]; //生成并持有對象,retainCount = 1
return obj;
}
- (void)run {
id __strong obj = [self getObj]; //持有對象, retainCount = 2
/*
使用 obj
*/
}//obj 超出作用域,強引用失效,retainCount = 1
我們發現到最后 obj 的 retainCount = 1 并沒有被銷毀,那么 release 能解決這個問題嗎?
- (NSObject *)getObj {
NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
[obj release]; // 此處加入 release,導致 obj 提前釋放
return obj;
[obj release]; // 這兒根本沒什么作用...
}
所以,我們需要擴大 obj 的生命周期,而 AutoreleasePool 正好可以解決這個問題:被加入 AutoreleasePool 的對象,不會立即釋放,而是在 AutoreleasePool 結束時調用 [obj release]
來保證對象在超出指定生存范圍時能夠自動并正確釋放。所以上述方法正確的實現應該是:
- (NSObject *)getObj {
NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
[obj autorelease]; //系統自動插入,先將 obj 放入最近的 AutoreleasePool,稍后釋放(retainCount - 1)
return obj;
}
2. 降低內存峰值
先看一個常見的面試題:
for (int i = 0; i < 100000000; i++) {
NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
str = [str stringByAppendingString:@" - world"];
}
上述代碼能正常運行嗎?如不能,請解釋原因并給出優化意見。
熟悉內存管理的同學應該知道,stringWithFormat:
返回的是一個 Autorelease 對象,所以每次循環生成的 str 都不會立即釋放,而是放入最近的 AutoreleasePool,當前 AutoreleasePool 需要等待當前線程 RunLoop 來釋放,當前線程 RunLoop 又一直在處理 for 循環等事件一直處于活躍狀態并不會釋放 AutoreleasePool,這樣就會導致 AutoreleasePool 里面的對象越來越多,內存占用成直線上升:
那怎么樣才能避免這種情況出現呢?
及時釋放內存。我們可以換個初始化方法,將 stringWithFormat:
換成了 alloc + initWithFormat:
,這樣對象就能立即釋放。另一個比較好的方案是加入局部 AutoreleasePool ,這樣也能避免在復雜情況下,去一個一個方法去確認是否返回 Autorelease 對象,手動加入的 AutoreleasePool,在作用域過后就立即清空池內所有對象。
for (int i = 0; i < 100000000; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
str = [str stringByAppendingString:@" - world"];
}
}
alloc
new
copy
mutableCopy
等方法會生成并持有對象,而其他類似[NSMutableArray array]
的方法會生成對象但不持有,返回的是 Autorelease 對象。
AutoreleasePool 原理
前面我們知道了 AutoreleasePool 的實踐應用,那它究竟是怎樣工作的呢,首先我們出它的結構說起,通過使用 clang -rewrite-objc
命令將下面的 Objective-C 代碼重寫成 C++ 代碼我們可以得知:
@autoreleasepool {
//...
}
實際上相當于
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
//...
objc_autoreleasePoolPop(atautoreleasepoolobj);
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
所以 AutoreleasePool 實際上是以 AutoreleasePoolPage 的形式在工作。我們來看看 AutoreleasePoolPage 在 NSObject.mm 中的定義:
class AutoreleasePoolPage {
magic_t const magic; // 完整性校驗
id *next; // 指向下一個內存為空的地址
pthread_t const thread; // 當前線程
AutoreleasePoolPage * const parent; // 構造雙向鏈表的指針
AutoreleasePoolPage *child; // 構造雙向鏈表的指針
uint32_t const depth;
uint32_t hiwat;
};
AutoreleasePool 并沒有單獨的結構,是由若干個 AutoreleasePoolPage 以雙向鏈表的形式組合而成的。
AutoreleasePoolPage 每個實例對象都會開辟 4096 bytes 內存(一頁虛擬內存的大小),除開底部用于存儲 AutoreleasePoolPage 的成員變量的空間,其余都用來儲存加入到自動釋放池的對象。
加入 AutoreleasePool
當你對一個對象 obj 發送 autorelease
消息時,如果當前線程不存在 AutoreleasePool,則會先生成 AutoreleasePool 對象:void *atautoreleasepoolobj = objc_autoreleasePoolPush();
,每當執行一次 objc_autoreleasePoolPush
, runtime 就向當前的 AutoreleasePoolPage 中 add 進一個哨兵對象(POOL_SENTINEL), atautoreleasepoolobj 即為返回的哨兵對象(POOL_SENTINEL)。接著 obj 就會被放入 next 指針所指的地址,然后 next 指向下一個內存為空的地址,如果當前 AutoreleasePoolPage 被占滿,就會生成新的 AutoreleasePoolPage 來存放 obj,并連接鏈表。
釋放 AutoreleasePool
從最新加入的對象一直向前清理,給每個對象發送 release
消息,直到哨兵對象(POOL_SENTINEL)位置,并回移 next
指針到正確的位置。
AutoreleasePool 對象何時釋放
我們從給對象 obj 發送 autorelease
消息開始說起:
-(id) autorelease
{
return _objc_rootAutorelease(self);
}
_objc_rootAutorelease(id obj)
{
assert(obj);
return obj->rootAutorelease();
}
可以看到這個方法里只是簡單的調了一下 _objc_rootAutorelease()
,繼續跟進:
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
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;
}
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);
}
}
可以看到:[obj autorelease]
實際上是調用了 autoreleaseFast(obj)
,在 autoreleaseFast()
里面會判斷當前是否存在 AutoreleasePoolPage ,如果不存在則調用 autoreleaseNoPage(obj)
,我們接著看這個方法:
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;
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;
}
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",
pthread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// 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);
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);
}
可以發現,經過一系列條件篩選,當不存在 AutoreleasePoolPage 時會生成新的 AutoreleasePoolPage 對象: AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
,這就表明:
當我們對一個對象發送 autorelease 消息時,都會被加入到最近的 AutoreleasePool ,不存在則先創建
- 對于手動添加的
@autoreleasepool { }
,里面的對象會在}
之后接收到release
消息 - 對于系統自動創建的
autoreleasepool
, 里面的對象與當前線程的 RunLoop 有關- 當前線程的 RunLoop 處于未開啟狀態時,
autoreleasepool
會在線程銷毀時一并清空 - 當前線程的 RunLoop 處于開啟狀態時(主線程的 RunLoop 會自動開啟,其他需要手動),RunLoop 會在合適的時機管理(push,pop)
autoreleasepool
- 當前線程的 RunLoop 處于未開啟狀態時,
對于主線程,系統會幫我們自動開啟 RunLoop ,并注冊了兩個 Observer,其回調都是
_wrapRunLoopWithAutoreleasePoolHandler()
,第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用_objc_autoreleasePoolPush()
創建自動釋放池。其 order 是 -2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
釋放舊的池并創建新池;Exit(即將退出Loop)時調用_objc_autoreleasePoolPop()
來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之后。在主線程執行的代碼,通常是寫在諸如事件回調、Timer 回調內的。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了。
Swift 中的 AutoreleasePool
實踐上,Swift 中的 AutoreleasePool 是橋接于 Objective-C,我們在 Swift 中使用 autoreleasepool { }
其實就是 Objective-C 中的那個。在過去這段時間,Swift 對 ARC 做過很多優化,好像并沒有了 AutoreleasePool 存在的必要性。然而真的不需要了嗎?我們看下述代碼:
guard let file = Bundle.main.path(forResource: "bigImage", ofType: "png") else {
return
}
for i in 0..<1000000 {
let url = URL(fileURLWithPath: file)
let imageData = try! Data(contentsOf: url)
}
還會引起內存的問題嗎?答案是:會。因為 Data(contentsOf: url)
實際上是橋接于 [NSData dataWithContentsOfURL]
,不幸的,還是會返回 autorelease 對象,同樣適用 autoreleasepool { }
能解決這個問題:
autoreleasepool {
let url = URL(fileURLWithPath: file)
let imageData = try! Data(contentsOf: url)
}
所以,AutoreleasePool 在 Swift 開發中仍然有用,因為在 UIKit 和 Foundation 中仍然存在遺留的 Objective-C 類 autorelease,但是由于 ARC 的優化,你在處理 Swift 類時可能不需要擔心它。
總結
- Autorelease 是通過推遲對對象發送
release
消息來延長對象生命周期的 - 每一個接收到
autorelease
消息的對象都會被加入最近的 AutoreleasePool(如果沒有就創建),然后會在當前線程銷毀時或者當前 Runloop 切換狀態(準備進入休眠和即將退出)時釋放,所以無需擔心autorelease
對象的內存問題 - AutoreleasePool 并沒有單獨的結構,是由若干個 AutoreleasePoolPage 以雙向鏈表的形式組合而成的
- AutoreleasePool 是通過哨兵對象(POOL_SENTINEL)來完成清空的,嵌套的 AutoreleasePool 相當于多個哨兵對象(POOL_SENTINEL)
參考
深入理解RunLoop
自動釋放池的前世今生
黑幕背后的Autorelease
帶著問題看源碼----子線程AutoRelease對象何時釋放
各個線程 Autorelease 對象的內存管理
does NSThread create autoreleasepool automatically now?
@autoreleasepool uses in 2019 Swift