iOS 內存管理之AutoReleasePool

背景

自從蘋果推出了ARC管理內存后,對于iOS開發這而言,內存管理就變得so easy了,只要正確使用相關規則,再也不用擔心double release,野指針的等問題了,而ARC的背后,除了強大的編譯器之外,還要得益于運行時起作用的AutoReleasePool。

研究AutoReleasePool

iOS的項目中,除了特別需求外,整個項目就一個地方明確寫了autoReleasePool的代碼了,就是main函數:

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

autoreleasepool做了什么?

我們知道oc代碼在編譯期間都會轉化為c/c++代碼,然后轉化為匯編,最終轉化為對應的架構的二進制文件;也可以這么說,oc的底層實現就是c/c++,既然這樣,我們把他轉化為對應的c/c++代碼應該就可以窺探到其中的密碼了:
轉化為c/c++代碼,Xcode有自帶的工具,打開命令行,輸入一下命令就可以:

xcrun -sdk iphoneos   clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

為了減少代碼量,重新建了一個macOS命令行項目:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

轉化為cpp文件,看下對應的代碼:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_k5_h0x40m15075dxn_z956dk2500000gn_T_main_6e2ecd_mi_0);
    }
    return 0;
}

從上面的C++源碼可以發現@autoreleasepool {}最后變成了:

{ __AtAutoreleasePool __autoreleasepool;//定義了一個__AtAutoreleasePool結構體變量
。。。
 }

我們分析下流程:
1、進入大括號,定義了一個__AtAutoreleasePool的結構體局部變量;
2、定義這個結構體變量的時候,會走結構體的構造方法,間接的會調用objc_autoreleasePoolPush函數:

__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}

3、當走出大括號是,局部變量__autoreleasepool,會被銷毀,因此會走結構體的析構函數,間接就會調用objc_autoreleasePoolPop函數:

  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}

從上面的分析,我們發現了兩個重要的函數objc_autoreleasePoolPush和objc_autoreleasePoolPop,這兩個函數是全局函數,而且是以objc開頭的,應該是在objc的源碼中,下載objc源碼的地址,macOS 最新系統下面的objc4。

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

他們調用的是AutoreleasePoolPage類對應的push跟pop兩個靜態函數,那么我們就要研究下AutoreleasePoolPage這個類了。

研究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是一個雙向鏈表的結構,每個實例都會存在一個parent實例指針,跟一個child實例指針。其他成員變量暫時不知道表示什么意思,只能繼續研究AutoreleasePoolPage的實現邏輯了,還是從push跟pop函數入手:

AutoreleasePoolPage 的push函數:

  static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page .debug模式新建一個page對象,實際不需要關注
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {//實際走這里
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

追根溯源autoreleaseFast:

static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
//拿到當前正在被使用的page,因為每個page都是有對象obj數量限
//制的,當page放滿了,就會創建一個child page來繼續放。
        if (page && !page->full()) {//沒滿,直接添加到當前的page上
            return page->add(obj);//添加obj
        } else if (page) {//full,滿了
            return autoreleaseFullPage(obj, page);//將obj添加到對應的未滿的child page里面,并將其設置為hot page
        } else {//沒有page
            return autoreleaseNoPage(obj);
        }
    }
 static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }
 static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // The hot page is full. 
        // Step to the next non-full page, adding a new page if necessary.
        // Then add the object to that page.
        assert(page == hotPage());
        assert(page->full()  ||  DebugPoolAllocation);

        do {
            if (page->child) page = page->child;//若有child page,將當前的指針指向child page
            else page = new AutoreleasePoolPage(page);//new一個新的page,并將其賦值給child page;
        } while (page->full());//page是否滿了,沒滿,跳出

        setHotPage(page);//將page設置為當前hotpage
        return page->add(obj);//添加obj到page
    }
    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());

        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            // We are pushing a second pool over the empty placeholder pool
            // or pushing the first object into the empty placeholder pool.
            // Before doing that, push a pool boundary on behalf of the pool 
            // that is currently represented by the empty placeholder.
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // We are pushing an object with no pool in place, 
            // and no-pool debugging was requested by environment.
            _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();//設置一個占位的空的page
        }

        // We are pushing an object or a non-placeholder'd pool.

        // Install the first page.第一次創建一個page
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);//并將它設置為hotpage
        
        // 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);//添加obj
    }
   id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));//page的起始地址+成員變量的大小
    }

    id * end() {
        return (id *) ((uint8_t *)this+SIZE);///一個page的大小是size,4096字節
    }

    bool empty() {
        return next == begin();
    }

    bool full() { 
        return next == end();
    }
  id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;//將obj的指針賦值給next所在的位置,然后將next指向下一個位置
        protect();
        return ret;//返回前一個obj
    }

總結一下:

  1. push操作從取出當前的hotpage,然后將一個哨兵對象(其實是nil)放入到page的next位置,并將next指向的位置向下+1;
  2. 取出的hotpage存在,但是hotpage是一個fullpage(一個page,PAGE_MAX_SIZE = 4096字節大小,除了存放內部成員變量的值之外,其他的都用來存放autorelease對象的地址),這時就會循環查找他的child指向的page對象,知道找到沒使用完的child page,如果沒有child page,則創建一個,將找到的child page并設置為hotpage,然后將一個哨兵對象添加進去;
  3. 取出的當前hotpage不存在,則通過autoreleaseNoPage創建一個新的page,并設置為hotpage,然后將然后將一個哨兵對象添加進去;
  4. 這樣做的結果,除了第一層page(沒有parent page),每次push返回的obj的都是哨兵對象,最開始的push返回的是第一層page最開始的位置page.begin(),后面的pop會用到這個返回值。

AutoreleasePoolPage 的pop函數:

    static inline void pop(void *token) 
    {//token就是對應push返回值,上面提到過要么是哨兵對象,要么是第一層page最開始的位置
        AutoreleasePoolPage *page;
        id *stop;

        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            if (hotPage()) {
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                pop(coldPage()->begin());
            } else {
                // Pool was never used. Clear the placeholder.
                setHotPage(nil);
            }
            return;
        }

        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                //要么是第一層page最開始的位置
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {//其他情況不存在,壞的page
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);//釋放存放在page的指針所指的對象,直到遇到哨兵對象或者全部釋放完成

        // memory: delete empty children
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } 
        else if (page->child) {//將page對象釋放掉
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }
    void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        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釋放完,還沒遇到哨兵對象,拿到parent page,并設置為hotpage,繼續釋放,直到遇到哨兵對象或者全部釋放完
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;//拿到obj對象,并將next指針指向上一個位置
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));//清空next位置,這里是設置為0x3A
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);//釋放掉對象
            }
        }

        setHotPage(this);//釋放完后,將當前page設置為hotpage

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            assert(page->empty());
        }
#endif
    }

pop函數總結幾點:

  1. 會拿到最近一次push函數(pop函數與push一一對應)返回的哨兵對象,作為pop函數的入參;
  2. 遍歷hotpage的next指針指向的對象,并釋放,直到遇到入參的哨兵對象;
  3. 如果當前page釋放完了,還沒遇到哨兵對象,就會往parent page遍歷,直到遇到哨兵對象,一次類推;
  4. 最后釋放掉為空(empty)的page對象,但是需要注意的是:當他的parent的使用空間超過了1/2,保留它對應的child page。

從上面的分析大致了解了AutoreleasePoolPage的工作流程,在程序運行的時候是怎么樣工作的呢?
我們知道,在MRC時代,需要程序員手寫對象的retain和release,后面最智能的就是new一個對象的時候,我們需要帶上autorelease代碼:

[[[NSObject alloc] init] autorelease];//MRC手動管理內存

到了ARC,我們不需要這樣寫,因為編譯器在編譯的時候,會自動幫忙加上這些代碼,所以說不管是ARC還是MRC時期,oc對象的內存管理入口都是autorelease方法:

autorelease方法的研究:

- (id)autorelease {
    return ((id)self)->rootAutorelease();
}
inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;//tagPointer 不需要
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}
__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);//調用的是AutoreleasePoolPage的autorelease函數
}
static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);//調用autoreleaseFast,上面提到過
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }

就是說,每個oc對象創建的時候(alloc/new/copy/mutableCopy),都是通過autorelease方法添加到page對象中的。pop那具體的調用時機又是什么呢?我們知道,項目只在main函數有一個autoreleasepool,其他的地方除非程序員自己手動添加,就不會有了,也就是說:我們暫且認為編譯器幫忙添加的autorelease,那也只是添加進page,pop函數還是只有一個,而且是程序退出的時候調用,如果是這種情況的話,程序整個運行期間,內存得不停的增長,因為只有申請,沒有釋放。顯然這種做法是行不通的 。

那么系統是怎么做的呢?直接說結論:我們知道程序運行期間是通過runloop維持的,而runloop就是不停的監聽事件和timer,處理事件和timer,沒有事件和timer的時候就進入休眠,系統會在runloop里面添加了兩個autorelease相關的observers:
autorelease相關的observers.png
RunLoop& AutoReleasePool關系幾點說明:
  1. App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。
  2. 第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。
  3. 第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之后。

總結

通過上面的源碼及流程的分析,我們對于autoreleasepool的工作原理及流程有了充分的了解:

  1. autoreleasepool底層的數據結構是一個autoreleasepage的雙向鏈表,每個page的大小為4096字節,除了存儲成員變量的大小,其他的位置都用來存儲autorelease對象的地址,next變量永遠指向下一個可以存放autorelease對象地址的地址空間,具體結構如下:
    數據結構
  2. 當一個page1存儲空間用完后,會創建一個新的page2,新的page2的parent指針指向滿了的page1,page1的child指針會指向page2;
  3. 程序啟動就會創建一個autoreleasepool,會調用push,程序結束時,最后會調用pop,回收所有autorelease對象的內存;
  4. 程序運行期間,同過監聽runloop的休眠狀態,調用push/pop方法,管理autorelease對象;
  5. 每次push的時候,會往page里面添加一個哨兵對象,這個哨兵對象作為下次pop函數的入參,遇到哨兵對象,說明這次runloop循環添加到page的autorelease對象release完畢。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379