iOS 自動釋放池autoreleasepool(一)

前言

在前面幾篇文章,說了關於OC中的內存佈局、內存管理方案、以及MRC狀況下的retainCount、retain、release,可是MRC也已是過去式了,此次來講說ARC。 從MRC到ARC的變化 就取決於@autoreleasepool。c++

@autoreleasepool 自動釋放池: 管理內存的池,把不須要的對象放在自動釋放池中,自動釋放(延遲釋放)這個池子內的對象。bash

@autoreleasepool的應用場景:函數

  1. 存在大量臨時變量的時候
  2. 非UI操做,如:命令行
  3. 本身建立輔助線程

從哪開始

AutoreleasePool建立和釋放

咱們能夠在工程中隨便找個地方打個斷點 po [NSRunLoop currentRunLoop]oop

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

$ cd main.m所在文件夾spa

$ 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

相關文章
相關標籤/搜索