自動釋放池源碼分析

ARC下,我們使用@autoreleasepool{}來使用一個AutoreleasePool

void main(int argc, char * argv[]) {

? ? ? ?@autoreleasepool {

? ? ? ?}

}

隨后編譯器將其改寫成下面的樣子(clang -rewrite-objc main.m):

報錯 main.m:9:9: fatal error: 'UIKit/UIKit.h' file not found,

#import <UIKit/UIKit.h>

尷尬!換個命令 xcrun -sdk iphonesimulator clang -rewrite-objc main.m(指定模擬器/SDK)

如果報錯 xcodebuild[3632:229732] [MT] PluginLoading: Required plug-in compatibility UUID E0A62D1F-3C18-4D74-BFE5-A4167D643966 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/CocoaPods.xcplugin' not present in DVTPlugInCompatibilityUUIDs

則Finder?前往這個地址,顯示包內存在Info.plist文件的DVTPlugInCompatibilityUUIDs數組中添加一item,值為E0A62D1F-3C18-4D74-BFE5-A4167D643966

~>

int main(int argc, char * argv[]) {

/* @autoreleasepool */???{ __AtAutoreleasePool __autoreleasepool;

? ? ?}

}

我們寫的@autoreleasepool{}變成了 @autoreleasepool??與 {}兩部分,第一部分@autorelease被注釋,很明顯它的作用對于編譯器只是一個標示。第二部分{}


__AtAutoreleasePool 是一結構體

struct __AtAutoreleasePool {

? ? ?__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}

? ? ?~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}

? ? ? void * atautoreleasepoolobj;

};

__AtAutoreleasePool __autoreleasepool;??這一句聲明,多少有點C++11 中的trivial的身影(可是看到了結構體中的構造和析構函數,一臉懵逼)

構造函數與類名相同,沒有返回值。析構函數與類名相同,前面加上~,沒有返回值和參數,對象銷毀時自動調用。

未定義構造函數,那么編譯器會添加有一個無參的構造函數,而如果定義了構造函數,不會默認生成,如果你還想允許無參構造,就必須顯式的聲明一個

盡管有點詭異,但是構造函數和析構函數還在那擺著呢。以上代碼等價于

void main(int argc, const char * argv[]) {

{

? ? ? void * autoreleasepoolobj = objc_autoreleasePoolPush();

? ? ? objc_autoreleasePoolPop(autoreleasepoolobj);

}

結構體會在初始化時調用 objc_autoreleasePoolPush() 方法,會在析構時(出作用域)調用 objc_autoreleasePoolPop 方法


@autoreleasepool 只是幫助我們少寫了這兩行代碼而已,讓代碼看起來更美觀。我們下面就要依據這兩個方法,展開

void *objc_autoreleasePoolPush(void) {

? ? ? return AutoreleasePoolPage::push();

}

void objc_autoreleasePoolPop(void *ctxt) {

? ? ? AutoreleasePoolPage::pop(ctxt);

}

AutoreleasePoolPage 的靜態方法push 和 pop,兩個函數都是對AutoreleasePoolPage的簡單封裝

AutoreleasePoolPage is who?

AutoreleasePoolPage是一個C++實現的類

class AutoreleasePoolPage {

? ? ? magic_t const magic;

? ? ? id *next;

? ? ? pthread_t const thread;

? ? ? AutoreleasePoolPage * const parent;

? ? ? AutoreleasePoolPage *child;

? ? ? uint32_t const depth;

? ? ? uint32_t hiwat;

};

1 magic 用于對當前 AutoreleasePoolPage 完整性的校驗

2 AutoreleasePool是按線程一一對應的(結構中的thread指針指向當前線程)

3 AutoreleasePool(自動釋放池)并沒有單獨的結構,而是由若干個AutoreleasePoolPage組合。每一個 AutoreleasePoolPage 的大小都是4096 字節, 前56 bit 用于存儲 AutoreleasePoolPage 的成員變量,剩下的用來存儲加入到自動釋放池中的對象(autorelease對象的地址)

4 上面的next指針作為游標,指向棧頂最新add進來的autorelease對象的下一個位置

next 指向了下一個為空的內存地址,如果 next 指向的地址加入一個 object,它就會如下圖所示移動到下一個為空的內存地址中


5 一個AutoreleasePoolPage的空間被占滿時,會新建一個AutoreleasePoolPage對象,連接鏈表,后來的autorelease對象在新的page加入

6 AutoreleasePoolPage 是雙向鏈表的形式連接起來的(parent 和 child 用來構造雙向鏈表的指針)

7 begin() 和 end() 這兩個類的實例方法獲取一個AutoreleasePoolPage的存儲autorelease對象地址的區域邊界


到了這里,你可能想要知道 POOL_SENTINEL 到底是什么,還有它為什么在棧中?

#define POOL_SENTINEL nil

POOL_SENTINEL 只是 nil 的別名,是一個哨兵對象。

static inline void *push() {

? ? ? ?return autoreleaseFast(POOL_SENTINEL);

}

static inline id *autoreleaseFast(id obj)

{

? ? ?AutoreleasePoolPage *page = hotPage();

? ? ?//有 hotPage 并且 hotPage 不滿

? ? ?if (page && !page->full()) { ?

? ? ? ? ? //調用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中

? ? ? ? ? return page->add(obj);

? ? ? } else if (page) {//有 hotPage 并且hotPage 已滿

? ? ? ? ? ? ?return autoreleaseFullPage(obj, page);

? ? ? } else {

? ? ? ? ?//無 hotPage

? ? ? ? ? ? ? return autoreleaseNoPage(obj); ??

? ? ? }

}

1 objc_autoreleasePoolPush -> AutoreleasePoolPage::push --> autoreleaseFast(POOL_SENTINEL)

因此,調用objc_autoreleasePoolPush,最終調用autoreleaseFast 方法,并傳入哨兵對象 POOL_SENTINEL

2 該函數分三種情況選擇不同的代碼執行(hotPage 可以理解為當前正在使用的 AutoreleasePoolPage)

將哨兵對象添加到hotPage中(page->add 添加對象),對函數整理簡化

id *add(id obj) {

? ? ?id *ret = next;

? ? ?*next = obj;

? ? ? next++;

? ? ? return ret;

}

這個函數其實就是一個壓棧的操作,將對象加入 AutoreleasePoolPage 然后移動棧頂的指針。


autoreleaseFast(POOL_SENTINEL),函數返回的是哨兵對象的地址



然后,順便看下autoreleaseFullPage()與autoreleaseNoPage()函數(page->child指向下一頁page)

static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {

? ? ? ?do {?

? ? ? ? ? ? ?if (page->child) page = page->child;

? ? ? ? ? ? ? else page = new AutoreleasePoolPage(page);

? ? ? ? } while (page->full());

? ? ? ?setHotPage(page);

? ? ? ?return page->add(obj);

}

會從傳入的 page 開始遍歷整個雙向鏈表,直到查找到一個未滿的 AutoreleasePoolPage,否則使用構造器傳入 parent 創建一個新的 AutoreleasePoolPage。得到一個可用的 AutoreleasePoolPage 之后,會將該頁面標記成 hotPage,然后調動上面分析過的 page->add 方法添加對象


如果不存在 hotPage,就會調用 autoreleaseNoPage 方法初始化一個 AutoreleasePoolPage,從頭開始構建雙向鏈表,也就是說,新的 AutoreleasePoolPage 是沒有 parent 指針的

static id *autoreleaseNoPage(id obj) {

? ? ? AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);

? ? ? setHotPage(page);

? ? ? if (obj != POOL_SENTINEL) {

//可見成員變量之上必然是哨兵對象,無論傳入與否。來確保在 pop 調用的時候,不會出現異常

? ? ? ? ? ? ?page->add(POOL_SENTINEL);

? ? ? ?}

? ? ? ?return page->add(obj);

}

綜上,autoreleaseFast()將哨兵對象添加到hotpage中,并返回哨兵對象的地址


3 ?objc_autoreleasePoolPush之后,autorelease對象必然執行類似操作進棧,紛紛添加到page中,直到objc_autoreleasePoolPop()(參數對應push返回值,哨兵對象的地址)

static inline void pop(void *token) {

? ? //獲取哨兵對象所在page(首地址)

? ? AutoreleasePoolPage *page = pageForPointer(token);

? ? id *stop = (id *)token;

? ? //釋放本次push進的對象

? ? ?page->releaseUntil(stop);

? ? //根據當前頁的不同狀態 kill 掉不同 child 的頁面

? ? if (page->child) {

? ? ? ? if (page->lessThanHalfFull()) {

? ? ? ? ? ? ? ?page->child->kill();

? ? ? ? } else if (page->child->child) { ??

? ? ? ? ? ? ? ?page->child->child->kill();

?? ? ? ? }

? ? ?}

}

pageForPointer 方法主要是通過內存地址的操作,獲取當前指針所在頁的首地址

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;

}

將指針地址與頁面的大小(如4096)取模,得到指針在page中的偏移量(因為所有的 AutoreleasePoolPage 在內存中都是對齊的)減去偏移量得到page的首地址,最后調用fastCheck() 用來檢查此地址是不是一個 AutoreleasePoolPage(通過檢查成員magic)

回到?pop函數,然后 page->releaseUntil(stop); 哨兵對象所在頁page調用releaseUntil,參數是哨兵對象的地址

void releaseUntil(id *stop) {

? ? ? while (this->next != stop) {

? ? ? ? ? ? ? ?AutoreleasePoolPage *page = hotPage();?

? ? ? ? ? ? ? ? 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_SENTINEL) {

? ? ? ? ? ? ? ? ? ? ? ? ? ? objc_release(obj); ?}

? ? ? ?}

? ? ? ?setHotPage(this);

}

這里,需要明白一點:

可能此次push進page的對象(指針)比較多,哨兵對象所在的page已不是hotpage

但是,由autoreleaseFullPage()可以看出,hotpage必然是哨兵對象所在的page的子節點(子子節點~)

找到當前hotpage,如果已空,通過parent指針遍歷雙向鏈表,找到不空的page作為hotpage(這個就是對應上面的情況,將hotpage清空,然后找到父節點繼續,直到來到哨兵對象地址)

通過page的next指針, memset將內存的內容設置成 SCRIBBLE,然后使用 objc_release 釋放對象,next指針必然前移。繼續循環釋放對象

每次進棧,add位置依據next指針指向,之后next指向后移。進棧依據next指針,出棧也是依據next指針。出棧之后,之后next指向前移。push可能用到多頁page,釋放必然也可能釋放多頁page,因此push進棧涉及到hotpage的變換,pop出棧也涉及到hotpage的切換。

這是一個不斷出棧的過程,只要哨兵對象所在頁page的next指針不指向此哨兵對象,循環繼續


以上,就是源碼分析。我們應該知道:

1 如果存在多個自動釋放池,那么它們在page之間的界限就是哨兵對象

2 自動釋放池的嵌套

@autoreleasepool {//pool 1

? ? ? obj1 = xxx autorelease;

? ? ? ?@autoreleasepool {//pool 2

? ? ? ? ? ? ? ? __autoreleasing obj2 = xxx;

? ? ? ? ? ? ? ? ? ? ? ?@autoreleasepool {//pool 3

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?obj3 = alloc;

? ? ? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? }

? ? ?}

它們在page中的位置,如下(假設同一個page)


next指針指向

pool 3哨兵對象 nil

obj2地址

pool 2哨兵對象 nil

obj1地址

pool 1哨兵對象 nil


加入釋放池,也是依據代碼順序,加入到對應的哨兵對象之后。釋放池廢棄時,也是先依據代碼順序,先釋放最內測的釋放池。當然這僅僅是以一個page為例

我們之前說過,RunLoop每次循環會創建自動釋放池,這個自動釋放必然是很外側的自動釋放池,而我們手動創建的相對于該自動釋放池是相對內側的釋放池,該自動釋放池釋放時,內側的自動釋放池s必然一同釋放(循環)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容