1. 程式人生 > IOS開發 >iOS 自動釋放池autoreleasepool(一)

iOS 自動釋放池autoreleasepool(一)

前言

在前面幾篇文章,說了關於OC中的記憶體佈局、記憶體管理方案、以及MRC情況下的retainCount、retain、release,但是MRC也已經是過去式了,這次來說說ARC。 從MRC到ARC的變化 就取決於@autoreleasepool。

@autoreleasepool 自動釋放池: 管理記憶體的池,把不需要的物件放在自動釋放池中,自動釋放(延遲釋放)這個池子內的物件。

@autoreleasepool的應用場景:

  1. 存在大量臨時變數的時候
  2. 非UI操作,如:命令列
  3. 自己建立輔助執行緒

從哪開始

AutoreleasePool建立和釋放

我們可以在工程中隨便找個地方打個斷點 po [NSRunLoop currentRunLoop]

image.png

  • App啟動後,蘋果在主執行緒 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 了。

也就是說AutoreleasePool建立是在一個RunLoop事件開始之前(push),AutoreleasePool釋放是在一個RunLoop事件即將結束之前(pop)。 AutoreleasePool裡的Autorelease物件的加入是在RunLoop事件中,AutoreleasePool裡的Autorelease物件的釋放是在AutoreleasePool釋放時。 以上內容參考自這位老哥

檢視@autoreleasepool{ }編譯成C++程式碼

使用編譯器clang編譯main.m轉化成main.cpp檔案(在終端:clang -rewrite-objc main.m)

$ cd main.m所在資料夾

$ clang -rewrite-objc main.m -o main.cpp

image.png

會在原有路徑下生成一個main.cpp,點開之後我們可以找到如下程式碼:

image.png
可知:main函式在c++中被編譯成了上圖中的模樣,核心重點一看就知道是這個__AtAutoreleasePool,在當前程式碼直接搜尋,很容易就能找到下面這些程式碼

image.png

注意:這裡的~是C++的解構函式(destructor) 與建構函式相反,當物件脫離其作用域時(例如物件所在的函式已呼叫完畢),系統自動執行解構函式。

從上圖我們可以找到兩個東西,從名字也很淺顯,一個進池子一個出池子
objc_autoreleasePoolPush
objc_autoreleasePoolPop
結合上面的注意點,我覺得可以先這麼總結一下

@autoreleasepool,就是把在它作用域(就是"{}")中的程式碼,先push進去,然後等這些程式碼都幹完活了,再把他們pop出去。

走進原始碼

我們先看一下這個 objc_autoreleasePoolPush

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
複製程式碼

這裡出現了一個AutoreleasePoolPage,且語法是C++的語法。我們這裡先看看這個AutoreleasePoolPage是何方神聖。

AutoreleasePoolPage

我們先看看變數宣告

class AutoreleasePoolPage 
{
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)
#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // 4096位元組 size and alignment,power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic; // 4*4位元組 用來校驗 AutoreleasePoolPage 的結構是否完整;
    id *next; // 8位元組 指向最新新增的 autoreleased 物件的下一個位置,初始化時指向 begin() ;
    pthread_t const thread; // 8位元組 指向當前執行緒;
    AutoreleasePoolPage * const parent; // 8位元組,指向父結點,第一個結點的 parent 值為 nil ;雙向連結串列上一個節點
    AutoreleasePoolPage *child; //8位元組,指向子結點,最後一個結點的 child 值為 nil ;雙向連結串列下一個節點
    uint32_t const depth; // 4位元組,代表深度,從 0 開始,往後遞增 1;
    uint32_t hiwat; // 4位元組 , 代表 high water mark 。
    //這幾個加起來是56位元組  
    ...
}
複製程式碼

image.png
由上圖可以看到AutoreleasePoolPage的大小為4096位元組,其中自身變數佔用56個位元組。 這裡可以來驗證一下。 這裡先介紹一下這個函式,這個可以列印當前AutoreleasePool中Page的狀態

void
_objc_autoreleasePoolPrint(void); 
複製程式碼

已知AutoreleasePoolPage size = 4096,自身佔用56,那麼剩餘空間為4096-56=4040。 一個NSObject的大小為8位元組,就是說一個AutoreleasePoolPage的剩餘空間還可以容納4040/8=505個NSObject。 所以就來一波這種操作

void
_objc_autoreleasePoolPrint(void);
int main(int argc,const char * argv[]) {
    @autoreleasepool {
        for (int i = 0 ; i<505; i++) {
            NSObject *obj = [[NSObject new]autorelease];
        }
        _objc_autoreleasePoolPrint();
     }
    return 0;
}
複製程式碼

然後看一下列印資訊

image.png

中間省略幾百個NSObject...

image.png
可以看到上面兩張圖合起來的話,一共出現了2個PAGE。

  • 第一個PAGE標記了(full)(cold)第一個詞很容易理解,就是滿了,第二個詞大意就是“涼了”可以理解成不活躍了。
  • 第二個PAGE標記了(hot)就是(cold)的反義詞咯,那就理解成活躍的意思。 總而言之,就是第一個PAGE滿了且被標記成非活躍狀態,第二個PAGE沒滿且是活躍狀態。

那麼問題來了,按照我們先前的計算,505個不是應該剛剛好裝滿一個PAGE嗎?這第二頁是什麼鬼?明顯就是多出了一個什麼奇怪的東西。 我們觀察一下這三個地址

objc[32613]: [0x102002000] ................ PAGE (full) (cold) objc[32613]: [0x102002038] ################ POOL 0x102002038 objc[32613]: [0x102002040] 0x100f8d780 NSObject

前兩個地址的區別就在於一個38,這裡是16進位制, 所以38在十進位制中為 3*16+8 = 56,所以這一系列的#號為變數進入pool的起點 第三個地址就是從38變成了40,38要變成40,在16機制中就是需要38中的8再加上8,讓3變成4。 所以就是這 0x102002038 裡面的東西在作怪。那這個又是什麼呢? 這裡我先貼一下部分程式碼

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

        // 翻譯:pushExtraBoundary 推入一個額外邊界
        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            //當push一個新頁或者第一頁的時候,在push之前需要先push一個poolboundary(邊界)
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // 異常情況,拋錯
            _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();
        }
        // 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);
    }
複製程式碼

這裡解讀一下這段程式碼 當我們push一個物件進來時,沒有page或者page滿了,需要到新的一頁的時候,他就會將pushExtraBoundary設定為true,在底下會判斷pushExtraBoundary,如果為true,就會先push一個POOL_BOUNDARY進入page中。

所以這個POOL_BOUNDARY就是我們上文中的“奇怪的東西”,從字面翻譯我們可以理解為這是一個“邊界”或者叫“邊界符”

總結:

  1. autoreleasepool由許許多多的AutoreleasePoolPage組成
  2. 當一個AutoreleasePoolPage裝滿之後,就會建立新的AutoreleasePoolPage,兩個Page之間用parent/child互相關聯,從而證明雙向連結串列的說法
  3. 在AutoreleasePoolPage自身變數的56個位元組之後,當push物件進page時,會先push一個邊界符進去POOL_BOUNDARY。這個邊界符也佔8個位元組

這裡給一張圖,可以更好的理解,加深印象

image.png

這篇文章先到這裡,下一篇autoreleasepool將會解析push跟pop