iOS 深入探究 AutoreleasePool
AutoreleasePool 是什麼
AutoreleasePool
(下面稱為快取池)是 iOS 開發中的一種記憶體管理的機制,物件呼叫 autorelease
方法後會被放到快取池中延遲釋放,當快取池需要清除時,會向這些 Autoreleased
物件傳送 release
訊息。
新建一個 Xcode
專案,將專案調整成 MRC
:
MRC
中,需要使用
retain/release/autorelease
手動管理記憶體,如下程式碼:
int main(int argc, const char * argv[]) {
NSLog(@"-A-");
Coder *coder = [[Coder alloc] init];
[coder release];
NSLog (@"-B-");
return 0;
}
// log
-A-
Coder dealloc
-B-
複製程式碼
這裡用 alloc
建立了 coder
物件,讓它的引用計數增加,然後呼叫 release
方法完成釋放。如果使用 autorelease
,就需要用到自動快取池了,程式碼如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"-A-");
Coder *coder = [[[Coder alloc] init] autorelease];
NSLog (@"-B-");
}
NSLog(@"-C-");
return 0;
}
// log
-A-
-B-
Coder dealloc
-C-
複製程式碼
這裡的 coder
物件在出了自動快取池的作用域後被自動釋放。
不是所有情況都是出了作用域後自動釋放,後面詳解。
@autoreleasepool 幹了什麼
通過 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 命令將 main.m 轉成 C++ 程式碼。
會發現 @autoreleasepool
被轉成一個成員變數:
__AtAutoreleasePool __autoreleasepool;
複製程式碼
__AtAutoreleasePool
結構體的實現:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
複製程式碼
這裡有一個 C++
的語法,__AtAutoreleasePool()
是建構函式,建立結構體時呼叫,~__AtAutoreleasePool()
是解構函式,在結構體銷燬時呼叫,所以上面的程式碼就可以理解為:
int main(int argc, const char * argv[]) {
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// @autoreleasepool 括號裡面的程式碼
objc_autoreleasePoolPop(atautoreleasepoolobj);
return 0;
}
複製程式碼
這也解釋了上面說的為什麼並不是所有情況都是出了 @autoreleasepool
作用域後自動釋放,因為這只是一個語法糖,本質是呼叫了上面的 Push&PoP
方法。
AutoreleasePoolPage
runtime原始碼地址,這裡使用的 objc4-723
在原始碼中查詢上面的 Push&Pop
函式:
void *objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
複製程式碼
這裡呼叫了 AutoreleasePoolPage
這個類的 Push&Pop
函式,關於 Push&Pop
這裡先打住。先來看看 AutoreleasePoolPage
是怎樣的結構,這裡只有成員變數:
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
// ...
}
複製程式碼
這裡的 next
指標指向的是最新被新增進來的 autorelease
物件的下一個位置。 單看這個是不好理解的,所以這裡直接先說 AutoreleasePoolPage
。
自動釋放池實際上是封裝的 AutoreleasePoolPage
這個 C++
類,以雙向連結串列的形式構成。每個 AutoreleasePoolPage
物件會開闢 4096
位元組記憶體(也就是虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來以棧的方式儲存 autorelease
物件。AutoreleasePoolPage
空間被佔滿時,會以連結串列的形式新建連結一個 AutoreleasePoolPage
物件,然後將 autorelease
物件的地址存在裡面。如圖所示:
原始碼分析
回到最初的 C++
程式碼:
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
objc_autoreleasePoolPop(atautoreleasepoolobj);
複製程式碼
呼叫 Push
函式後,會獲得一個返回值,這個返回值作為 Pop
函式的引數被傳入了,下面來看看裡面具體的原理是什麼。直接來看 Push
函式的原始碼:
// 簡化後
static inline void *push()
{
id *dest;
dest = autoreleaseFast(POOL_BOUNDARY);
return dest;
}
複製程式碼
這裡有個 POOL_BOUNDARY
值得我們注意,不過檢視它的定義會發現它其實是等價 nil
的巨集定義:
# define POOL_BOUNDARY nil
複製程式碼
也就是說,POOL_BOUNDARY
僅僅只是一個哨兵值。進入 autoreleaseFast(...)
函式:
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 1.
return page->add(obj);
} else if (page) {
// 2.
return autoreleaseFullPage(obj, page);
} else {
// 3.
return autoreleaseNoPage(obj);
}
}
複製程式碼
hotPage 指當前使用的
AutoreleasePoolPage
節點,coldPage 指已經被裝滿的連結串列節點。
這裡的判斷邏輯完全符合前面關於 AutoreleasePoolPage
的說明:
- 1.當前
page
存在且沒有滿時,直接將物件新增到當前page
中。 - 2.當前
page
存在且已滿時,建立一個新的page
,並將物件新增到新建立的page
中,然後將這兩個連結串列節點連結。 - 3.當前
page
不存在時,建立第一個page
,並將物件新增到新建立的page
中。
Push
後,都會先新增一個
POOL_BOUNDARY
來佔位,是為了對應一次
Pop
的釋放,例如圖中的
page
就需要兩次
Pop
然後完全的釋放。也就是程式碼中巢狀的情況:
@autoreleasepool {
@autoreleasepool {
}
}
複製程式碼
這裡還需要強調的是,這裡使用的是雙鏈表來實現,只有在當前 page
空間使用完後,才會建立新的 page
,並不是每個 @autoreleasepool
對應一個 AutoreleasePoolPage
物件。
接下來看 Pop
的原始碼:
// 簡化後
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
// 1.根據 token,也就是上文的佔位 POOL_BOUNDARY 釋放 `autoreleased` 物件
page->releaseUntil(stop);
// hysteresis: keep one empty child if page is more than half full
// 2.釋放 `Autoreleased` 物件後,銷燬多餘的 page。
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
複製程式碼
這裡沒什麼說的,來到 releaseUntil(...)
內部:
// 簡化後
void releaseUntil(id *stop)
{
// 1.
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
// 2.
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
// 3.
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
// 4.
setHotPage(this);
}
複製程式碼
- 1.外部迴圈挨個遍歷
autoreleased
物件,直到遍歷到stop
這個POOL_BOUNDARY
。 - 2.如果當前
hatPage
沒有POOL_BOUNDARY
,將hatPage
設定為父節點。 - 3.給當前
autoreleased
物件傳送release
訊息。 - 4.再次配置
hatPage
。
再來看看 autorelease
的實現,這裡直接定位到 page
裡面的 autorelease
:
// 簡化後
static inline id autorelease(id obj)
{
id *dest __unused = autoreleaseFast(obj);
return obj;
}
複製程式碼
和上面的 push
操作中呼叫的同一函式 autoreleaseFast
,沒什麼說的。
這裡從原始碼層面上就瞭解了自動快取池道理是怎麼一回事。
AutoreleasePool 和 runloop
這裡需要 runloop 的知識,可以看我前面的文章 iOS 淺談 Runloop
App
啟動後,蘋果在主執行緒 RunLoop
裡註冊了兩個 Observer
,回撥都是 _wrapRunLoopWithAutoreleasePoolHandler
,用來處理自動快取池。
列印主執行緒的 runloop
進行確認。
print(RunLoop.main)
複製程式碼
注意觀察圖中
Observer
觀察的狀態,上面的是
activities = 0x1
,下面的是
activities = 0xa0
。這裡需要一點
runloop
的知識,下面就是
runloop
可以被監聽的狀態列舉。
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 1
kCFRunLoopBeforeTimers = (1UL << 1), // 2
kCFRunLoopBeforeSources = (1UL << 2), // 4
kCFRunLoopBeforeWaiting = (1UL << 5), // 32
kCFRunLoopAfterWaiting = (1UL << 6), // 64
kCFRunLoopExit = (1UL << 7), // 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製程式碼
0x1
(等於1)對應的是 kCFRunLoopEntry
,第一個 Observer
監視的即將進入Loop時,,其回撥內會呼叫 _objc_autoreleasePoolPush()
建立一個自動釋放池。其 order
是 -2147483647
,優先順序最高,保證建立快取池發生在其他所有回撥之前。
0xa0
(16進位制等於160,等於32+128) 對應的是 kCFRunLoopBeforeWaiting&kCFRunLoopExit
,第二個 Observer
監視了兩個事件: 準備進入休眠時呼叫 _objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
釋放舊的池並建立新池;即將退出Loop時呼叫 _objc_autoreleasePoolPop()
來釋放自動釋放池。這個 Observer
的 order
是 2147483647
,優先順序最低,保證其釋放快取池發生在其他所有回撥之後。
所以對於我們應用來說,autoreleased
物件更多的是在 runloop
的休眠時進行釋放的。
參考
關於 AutoreleasePool
還有一些實際使用中的技巧,例如解決迴圈中 autoreleased
物件的記憶體問題等等。