Runtime原始碼 autoreleasepool

Ly夢k發表於2018-11-20

前言

在iOS開發中,由於ARC的普遍使用,記憶體管理的問題好像不那麼常見了,但瞭解Objective-C的記憶體管理機制依然是非常必要的,今天我們來看看autoreleasepool的一些細節,在ARC時代幾乎很少看到autoreleasepool的身影了,唯一常見的應該就是在main函式中了:

int main(int argc, char * argv[]) { 
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

}
}複製程式碼

這裡可以看到整個 iOS 的應用都是包含在一個自動釋放池 block 中的。那麼這個autoreleasepool到底是什麼呢?接下來我們來一窺究竟。

結構

使用clang -rewrite-objc main.m將main函式所在的檔案,轉化為C++程式碼:

int main(int argc, char * argv[]) { 
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));

}
}複製程式碼

可以看到autoreleasepool其實是型別為__AtAutoreleasePool的結構體,那麼它又是什麼呢?我們來看看他的定義:

struct __AtAutoreleasePool { 
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();

} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);

} void * atautoreleasepoolobj;

}複製程式碼

由此,我們可以知道:當使用autoreleasepool時,實際上是:

/* @autoreleasepool */ { 
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
...// 自己的程式碼,接收到 autorelease 訊息的物件會被新增到這個 autoreleasepool中 objc_autoreleasePoolPop(atautoreleasepoolobj);

}複製程式碼

它其實是呼叫了objc_autoreleasePoolPush,這又是什麼?怎麼越看越多?別急,馬上到了,繼續吧,開啟runtime原始碼工程,在NSObject.mm檔案中我們看到,其實他是:

void *objc_autoreleasePoolPush(void){ 
return AutoreleasePoolPage::push();

}複製程式碼

最後,再來看看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;
...
}複製程式碼

終於看到他的結構了,總的來說,其實每一個自動釋放池都是由一系列的 AutoreleasePoolPage 組成的,並且每一個 AutoreleasePoolPage 的大小都是 4096 位元組(16 進位制 0x1000)根據註釋The stack is divided into a doubly-linked list of pages. Pages are added可以知道,他是以雙連結串列的形式儲存的,也對應了其結構中的parent和child,接下里我們看看他其他欄位:

  • magic
    magic是magic_t型別的結構體,它用來校驗 AutoreleasePoolPage結構的完整性
  • *next
    指向最新新增的 autoreleased 物件的下一個位置,初始化時指向 begin()
  • thread
    當前執行緒
  • parent
    指向父結點,第一個結點的 parent 值為 nil ;
  • child
    指向子結點,最後一個結點的 child 值為 nil ;
  • depth
    代表深度,從 0 開始,往後遞增 1;
  • hiwat
    代表 high water mark 。

objc_autoreleasePoolPush()

上面我們看到了objc_autoreleasePoolPush()接下來,看看push的具體操作:

static inline void *push()     { 
id *dest;
if (DebugPoolAllocation) {
// 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;

}複製程式碼

這裡有一個debug環境的判斷,我們忽略,直接看到autoreleaseFast():

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);

}
}複製程式碼
  • 當前 page 存在且沒有滿時:
    直接將物件新增到當前 page 中,即 next 指向的位置;
  • 當前 page 存在且已滿時:
    建立一個新的 page ,並將物件新增到新建立的 page 中;
  • 當前 page 不存在時即還沒有 page 時:
    建立第一個 page ,並將物件新增到新建立的 page 中。

每呼叫一次 push 操作就會建立一個新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一個POOL_BOUNDARY,並且返回插入的 POOL_BOUNDARY的記憶體地址。

從定義# define POOL_BOUNDARY nil可知:這裡的POOL_BOUNDARY其實就是nil的別名

autorelease

接下來我們看看autorelease的實現,追溯原始碼發現它的函式呼叫棧很深,依次為:

  1. -(id) autorelease
  2. _objc_rootAutorelease(id obj)
  3. objc_object::rootAutorelease()
  4. objc_object::rootAutorelease2()
  5. static inline id autorelease(id obj)
  6. static inline id *autoreleaseFast(id obj)

它以obj為引數,通過層層呼叫,最終還是呼叫到了autoreleaseFast方法,
也就是說:它和 push 操作的實現基本一致,只是不過 push 操作插入的是一個 POOL_BOUNDARY ,而 autorelease 操作插入的是一個具體的 autoreleased 物件。

objc_autoreleasePoolPop()

他其實是呼叫了:

objc_autoreleasePoolPop(void *ctxt){ 
AutoreleasePoolPage::pop(ctxt);

}複製程式碼

pop呼叫到了releaseUntil裡面有這樣的操作:

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);

}
}
複製程式碼

pop可以說是:以上面說到的POOL_BOUNDARY的記憶體地址為入參,根據傳入的POOL_BOUNDARY,找到這個push所在的位置,對之前入棧的每個antorealse物件都傳送一次- release訊息,並移動next指標,當指標的地址移動到POOL_BOUNDARY的位置時,這個自動釋放池內的物件釋放完畢。

應用

  1. 當使用迴圈時,使用@autoreleasePool能降低記憶體峰值(詳見《Effective Objective-C 2.0》第34條)
  2. 遍歷時儘量使用系統的enumerateObjectsUsingBlock,因為其內部有自動釋放池,而for或者for-in沒有

總結

  • 自動釋放池是由 AutoreleasePoolPage 以雙向連結串列的方式實現的。
  • 呼叫AutoreleasePoolPage::push向棧插入一個nil作為標記,並返回這個地址。
  • 當物件呼叫 autorelease 方法時,會將物件加入 AutoreleasePoolPage 的棧中(由於後加,所以他們一定在nil所在位置之上)。
  • 呼叫 AutoreleasePoolPage::pop 方法,傳遞push函式得到的地址,依次向棧中的物件傳送 release 訊息,並下移指標,直到push的nil所在位置,即完成了自動釋放。

參考資料:
NSAutoreleasePool
黑幕背後的Autorelease
Objective-C Autorelease Pool 的實現原理

來源:https://juejin.im/post/5bf35e99e51d4552ee42482f

相關文章