iOS 內存管理--自動釋放池底層原理

前言

在內存管理的學習中自動釋放池的原理學習是必須的,作為一個合格的iOS開發者,必須要明白自動釋放池的操作原理,這篇文章的目的就是探索自動釋放池的底層原理。

準備工作

1. 自動釋放池

1.1 相關概念

  • 如果在函數、方法的開始處將對象的引用計數加1,在函數、方法不需要該對象的時候將其引用計數減1,這思想基本OK
  • 有些函數、方法需要返回一個對象,而系統可能在該對象被返回之前,就已經銷毀了對象。那么為了保證函數、方法返回的對象在被返回之前不被銷毀,我們就要使用自動釋放池進行延遲銷毀(NSAutoreleasePool)。
  • 所謂自動釋放池,是指它是一個存放對象的容器(集合),而自動釋放池會保證延遲銷毀該池中所有的對象。出于自動釋放池的考慮,所有的對象都應該添加到自動釋放池中,這樣可以讓自動釋放池在銷毀之前,先銷毀池中的所有對象。
  • autorelease方法。該方法不會改變對象的引用計數,只是將該對象添加到自動釋放池中。該方法會返回調用該方法的對象本身
  • 當程序在自動釋放池上下文中調用某個對象的autorelease方法時,該方法只是將對象添加到自動釋放池中,當該自動釋放池釋放時,自動釋放池會讓池中所有的對象執行release方法。
  • 自動釋放池的銷毀和其他普通對象相同,只要其引用計數為0,系統就會自動銷毀自動釋放池對象。系統會在調用NSAotoreleasePooldealloc方法時回收該池中的所有對象
  • NSAutoreleasePool還提供了一個drain方法來銷毀自動釋放池中的對象。與release不同,release會使自動釋放池自身的引用計數變為0,從而讓系統回收NSAutoreleasePool對象,在回收NSAutoreleasePool對象之前,系統會回收該池中的所有對象。而drain方法則只是回收池中的所有對象,并不會銷毀自動釋放池

1.2 運行邏輯

AutoReleasePoolOC內存自動回收機制,將加入到AutoReleasePool中的變量release時機延遲。在正常情況下,創建的變量會在超出其作用域的時候release,但是如果將變量加入AutoreleasePool,那么release將延遲執行,即使超出作用域也不會立即釋放,直到runloop休眠或者超出AutoReleasePool作用域才會釋放

運行邏輯

自動釋放池的運行機制:

  • 程序啟動到加載完成,主線程對應的Runloop處于休眠狀態,直到用戶點擊交互喚醒Runloop
  • 用戶每次交互都會啟動一次Runloop用來處理用戶的點擊、交互事件
  • Runloop被喚醒后,會自動創建AutoReleasePool,并將所有延遲釋放的對象添加到AutoReleasePool
  • 在一次完整的Runloop執行結束前,會自動向AutoReleasePool中的對象發送release消息,然后銷毀AutoReleasePool

注意:AutoreleasePoolRunloop的運行機制和關系,在后面講解Runloop時會詳細說明。

1.3 使用效果分析

下面通過一個案例來說明自動釋放池的作用。我們常用以下兩種方式創建字符串

    // 方式1
    NSString * string1  = [[NSString alloc] initWithFormat:@"hello world..."];
    // 方式2
    NSString * string2  = [NSString stringWithFormat:@"hello world auto relase..."];

那么以上兩種方式有什么不同呢?還是老規矩,查看匯編!!!

  • 方式1
NSString * string1  = [[NSString alloc] initWithFormat:@"hello world..."];

打開匯編斷點查看流程如下:

方式1匯編流程

這個流程有點熟悉,就是標準的對象創建過程嘛。使用alloc出來的方式,字符串在調用release的時候被回收(假設該字符串沒有被其他對象引用變量會在超出其作用域的時候release)。

  • 方式2
    NSString * string2  = [NSString stringWithFormat:@"hello world auto relase..."];

打開匯編斷點查看流程如下:

方式2匯編流程

使用stringWith的方式,字符串在api內部會被設置成autorelease,不用手動釋放,系統會回收,因此將會在最近的一個自動釋放池drainrelease時被回收。

下面通過一個案例來深入了解自動釋放池的作用。案例中,使用兩種方式創建了字符串,并且把字符串賦值給__weak修飾的成員變量。

  • 案例1
__weak NSString *weakSrting;
__weak NSString *weakSrtingAutoRelease;

@implementation ViewController

- (void)createStringFunc {
        // 方式1
        NSString * string1  = [[NSString alloc] initWithFormat:@"hello world..."];
        weakSrting = string1;
        
        // 方式2
        NSString * string2  = [NSString stringWithFormat:@"hello world auto relase..."];
        weakSrtingAutoRelease = string2;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    [self createStringFunc];
    NSLog(@"weakSrting: %@", weakSrting);
    NSLog(@"weakSrtingAutoRelease: %@", weakSrtingAutoRelease);
}

- (void)viewWillAppear:(BOOL)animated
{
    NSLog(@"view will appear weakSrting: %@", weakSrting);
    NSLog(@"view will appear weakSrtingAutoRelease: %@", weakSrtingAutoRelease);
}

- (void) viewDidAppear:(BOOL)animated
{
    NSLog(@"view did appear weakSrting: %@", weakSrting);
    NSLog(@"view did appear weakSrtingAutoRelease: %@", weakSrtingAutoRelease);
}

查看運行結果,如下:


運行結果

結果分析如下:

  • 使用方式1創建的字符串weakSrting,在createStringFunc方法執行完成后就會釋放作用域結束),弱引用weakSrting也會釋放掉。所以weakSrting打印結果都是空。
  • 使用方式2創建的對象weakSrtingAutoRelease,這個對象被系統自動添加到了當前的autoreleasepool中,起到了延遲釋放的效果。這個對象是一個autoreleased對象,autoreleased對象是被添加到了當前最近的autoreleasepool中,只有當這個autoreleasepool自身drain的時候,autoreleasepool中的autoreleased對象才會被release

對象weakSrtingAutoRelease,在viewWillAppear中打印這個對象的時候,能夠輸出,說明此時對象還沒有被釋放。但是在viewDidAppear中打印這個對象的時候就成了null了,那么這個對象一定是在viewWillAppearviewDidAppear方法之間的某個時候被釋放了,并且是由于它所在的autoreleasepoolrelease的時候釋放的。我們可以在lldb調試中設置觀察點(watchpoint set v weakSrtingAutoRelease),來查看對象的釋放過程,如下:

對象的釋放過程

在運行棧中可以發現,weakSrtingAutoRelease對象在自動釋放池釋放時完成了釋放

  • 案例2
    案例1看起來不夠直接,那么我們來一個直接點的案例。代碼中手動添加了一個@autoreleasepool,在自動釋放池內,weakSrtingAutoRelease一直不會釋放,而出了自動釋放池就會釋放。如下:
    案例2

    明顯看出使用方式2創建的對象weakSrtingAutoRelease在自動釋放池內都能夠正常使用,出了自動釋放池就會被釋放,起到延遲釋放的效果。

但是使用方式1創建的字符串weakSrting,為什么在自動釋放池內就釋放了呢?他不會加入到自動釋放池嗎?帶著疑問繼續往下走!!

2. 自動釋放池原理分析

2.1 原理初探

通過clang查看自動釋放池的實現原理,如下:

main.cpp

@autoreleasepool在編譯后變成了以下代碼:

    __AtAutoreleasePool __autoreleasepool;

main.cpp文件中全局搜索__AtAutoreleasePool的定義,找到__AtAutoreleasePool結構體的定義,如下:

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

該結構體提供了一個構造函數objc_autoreleasePoolPush和一個析構函數objc_autoreleasePoolPop。所以自動釋放池在底層其實是一個結構體,其通過objc_autoreleasePoolPush完成自動釋放池的創建objc_autoreleasePoolPop釋放自動釋放池

設置objc_autoreleasePoolPush的符號斷點,的確能夠進入,匯編如下:

objc_autoreleasePoolPush符號斷點

可以確定自動釋放池其實現源碼在我們最熟悉的libobjc.A.dylib庫。那就非常的nice了!

2.2 結構分析

下面通過源碼進行分析。跟蹤objc_autoreleasePoolPush的方法實現,如下:

objc_autoreleasePoolPush

其調用了objc_autoreleasePoolPush()方法,繼續跟蹤代碼:
objc_autoreleasePoolPush()

在該方法的實現中,其調用了AutoreleasePoolPagepush方法。那么AutoreleasePoolPage的結構是怎么的呢?如下:
AutoreleasePoolPage

通過AutoreleasePoolPage類的注釋可以得到以下關鍵信息:

  • 一個線程的自動釋放池是一堆指針
  • 每個指針要么是一個要釋放的對象,要么是POOL_BOUNDARY(自動釋放池邊界-哨兵對象)
  • 堆棧被分成一個雙向鏈接頁面列表, 頁面已添加對象并根據需要刪除
  • 線程本地存儲指向新自動釋放的熱點頁面對象被存儲

AutoreleasePoolPage繼承于AutoreleasePoolPageData,那么查看AutoreleasePoolPageData的結構如下:

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    struct AutoreleasePoolEntry {
        uintptr_t ptr: 48;
        uintptr_t count: 16;

        static const uintptr_t maxCount = 65535; // 2^16 - 1
    };
    static_assert((AutoreleasePoolEntry){ .ptr = MACH_VM_MAX_ADDRESS }.ptr == MACH_VM_MAX_ADDRESS, "MACH_VM_MAX_ADDRESS doesn't fit into AutoreleasePoolEntry::ptr!");
#endif

    magic_t const magic; // 16
    __unsafe_unretained id *next; // 8
    pthread_t const thread; // 8
    AutoreleasePoolPage * const parent; // 8
    AutoreleasePoolPage *child; // 8
    uint32_t const depth; // 4
    uint32_t hiwat;  // 4

    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

屬性相關說明:

  • magic?來校驗AutoreleasePoolPage的結構是否完整
  • next 指向最新添加的autoreleased對象的下?個位置初始化時指向begin()
  • thread 指向當前線程
  • parent指向?結點,第?個結點的parent值為nil
  • child 指向?結點,最后?個結點的child值為nil
  • depth 代表深度,從0開始,往后遞增1
  • hiwat代表high water mark最??棧數量標記

2.3 源碼實現

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

那么我們繼續跟蹤push()方法,其源碼實現如下:

    static inline void *push() 
    {
        id *dest;
        if (slowpath(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方法,并傳入邊界對象哨兵對象)。查看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);
        }
    }
  • 首先獲取當前hotPage,如果不為空且沒有滿,則會向該頁中添加obj
  • 如果該頁已滿,則調用autoreleaseFullPage方法;
  • 如果當前hotPage不存在,也就是沒有page,則調用autoreleaseNoPage方法。autoreleaseNoPage實現源碼如下:
    autoreleaseNoPage

    在完成AutoreleasePoolPage創建后,首先添加哨兵對象,然后在加入obj。 首先查看AutoreleasePoolPage構造函數,如下:
    AutoreleasePoolPage

    通過調用AutoreleasePoolPageData的構造函數實現初始化,并確定頁之間的鏈表關系。通過上面的結構我們可以確定AutoreleasePoolPageData屬性占56個字節。見下圖:
    AutoreleasePoolPageData屬性

    因為頁中next字段用于設置存儲obj的位置,那么因為每個頁自身有一些屬性需要占用一部分空間,所以next的起始值是page首地址平移56個字節,也就是構造函數中begin()方法所確定下來的值。
 id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

斷點調試如下:

begin()斷點調試

如果頁滿時,就會調用上面的autoreleaseFullPage方法,見下面實現源碼:

 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;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }

do..while循環找到最后一個page,如果page沒有滿,就將page設置為hotPage。如果page已滿,則會新建一個page,也將page設置為hotPage。最后往page中添加obj

綜合上面的數據結構和源碼實現,我們可以得出以下結論:

  • Autoreleasepool是由多個AutoreleasePoolPage雙向鏈表的形式連接起來的
  • Autoreleasepool的基本原理:在自動釋放池創建的時候,會在當前的AutoreleasePoolPage中設置一個標記位邊界),在此期間,當有對象調用autorelease時,會把對象添加到AutoreleasePoolPage
  • 如果當前頁加滿了,會初始化一個新頁,然后用雙向鏈表鏈接起來,并把初始化的一頁設置為hotPage,當自動釋放池pop時,從最下面依次往上pop,調用每個對象的release方法,直到遇到標志位

2.4 滿頁臨界值

自動釋放池一頁能夠存儲多少個對象呢?如果能夠打印輸出自動釋放池的數據,會更便于我們對自動釋放池的了解。在源碼中也提供了相關的打印數據結構的方法,如下:

void 
_objc_autoreleasePoolPrint(void)
{
    AutoreleasePoolPage::printAll();
}

printAll()方法如下:

    __attribute__((noinline, cold))
    static void printAll()
    {
        _objc_inform("##############");
        _objc_inform("AUTORELEASE POOLS for thread %p", objc_thread_self());

        AutoreleasePoolPage *page;
        ptrdiff_t objects = 0;
        for (page = coldPage(); page; page = page->child) {
            objects += page->next - page->begin();
        }
        _objc_inform("%llu releases pending.", (unsigned long long)objects);

        if (haveEmptyPoolPlaceholder()) {
            _objc_inform("[%p]  ................  PAGE (placeholder)", 
                         EMPTY_POOL_PLACEHOLDER);
            _objc_inform("[%p]  ################  POOL (placeholder)", 
                         EMPTY_POOL_PLACEHOLDER);
        }
        else {
            for (page = coldPage(); page; page = page->child) {
                page->print();
            }
        }

        _objc_inform("##############");
    }

創建一個案例查看其內部存儲結構,如下:

案例分析

通過上面的輸出可以發現,該自動釋放池的起始頁是0x10380a000,地址平移56個字節后放入的是哨兵對象,哨兵對象地址為0x10380a038,緊接著放入4個對象。那么一頁能放多少呢?源碼中也有定義,如下:

static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif

#define PAGE_MIN_SHIFT          12
#define PAGE_MIN_SIZE           (1 << PAGE_MIN_SHIFT)

通過以上的源碼定義發現其大小為1<<12,也即是4096,而每頁自身屬性的占用56個字節,同時第一頁需要一個哨兵對象8個字節,所以首頁最多可以放(4096 - 56 - 8) / 8 = 504個對象。驗證一下:

案例分析

案例分析

通過輸出自動釋放池的數據結構可以發現,當放入505個對象時,會新開辟一頁,并且第二頁中只有一個對象。(哨兵對象只會放在第一頁)所以第一頁最多可以放504個對象,之后每頁可以存儲505個對象。

3. 自動釋放池注意點

3.1 對象release而非銷毀

引入案例,如下:

案例分析

當自動釋放池結束的時候,僅僅是對存儲在自動釋放池中的對象發送1release消息,而不是銷毀對象

3.2 自動釋放池的嵌套

引入案例,如下:

案例分析

通過該案例可以發現,自動釋放池嵌套并不會影響數據結構,只是多插入一個哨兵對象

3.3 哪些對象可以放入自動釋放池

引入案例,如下:

  • MRC環境
    MRC環境
  • ARC環境
    ARC環境

    總結:
  • 主動調用autorelase方法的,用alloc, init,copy等方法創建的對象,這些我們自己持有的,我們想讓他延遲釋放,就調用autorelase方法,這樣在自動釋放池出棧的時候,對象就會釋放掉
  • 對于那種stringWithFormt這種從名字來看,沒有被調用者持有的情況,要么是自動加到自動釋放池里的,要么是常量字符串不用引用計數來管理
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容