AutoreleasePool
(下面稱爲緩存池)是 iOS 開發中的一種內存管理的機制,對象調用 autorelease
方法後會被放到緩存池中延遲釋放,當緩存池須要清除時,會向這些 Autoreleased
對象發送 release
消息。c++
新建一個 Xcode
項目,將項目調整成 MRC
:objective-c
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
對象在出了自動緩存池的做用域後被自動釋放。bash
不是全部狀況都是出了做用域後自動釋放,後面詳解。app
經過 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 命令將 main.m 轉成 C++ 代碼。less
會發現 @autoreleasepool
被轉成一個成員變量:iphone
__AtAutoreleasePool __autoreleasepool;
複製代碼
__AtAutoreleasePool
結構體的實現:函數
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
複製代碼
這裏有一個 C++
的語法,__AtAutoreleasePool()
是構造函數,建立結構體時調用,~__AtAutoreleasePool()
是析構函數,在結構體銷燬時調用,因此上面的代碼就能夠理解爲:oop
int main(int argc, const char * argv[]) {
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// @autoreleasepool 括號裏面的代碼
objc_autoreleasePoolPop(atautoreleasepoolobj);
return 0;
}
複製代碼
這也解釋了上面說的爲何並非全部狀況都是出了 @autoreleasepool
做用域後自動釋放,由於這只是一個語法糖,本質是調用了上面的 Push&PoP
方法。源碼分析
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
的說明:
page
存在且沒有滿時,直接將對象添加到當前 page
中。page
存在且已滿時,建立一個新的 page
,並將對象添加到新建立的 page
中,而後將這兩個鏈表節點連接。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);
}
複製代碼
autoreleased
對象,直到遍歷到 stop
這個 POOL_BOUNDARY
。hatPage
沒有 POOL_BOUNDARY
,將 hatPage
設置爲父節點。autoreleased
對象發送 release
消息。hatPage
。再來看看 autorelease
的實現,這裏直接定位到 page
裏面的 autorelease
:
// 簡化後
static inline id autorelease(id obj)
{
id *dest __unused = autoreleaseFast(obj);
return obj;
}
複製代碼
和上面的 push
操做中調用的同一函數 autoreleaseFast
,沒什麼說的。
這裏從源碼層面上就瞭解了自動緩存池道理是怎麼一回事。
這裏須要 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
對象的內存問題等等。