iOS -- Autorelease & AutoreleasePool

前言

內存管理一直是Objective-C 的重點,在MRC環境下,經過調用[obj autorelease]來延遲內存的釋放,在如今ARC環境下,咱們都知道編譯器會在合適的地方插入release/autorelease內存釋放語句,咱們甚至能夠不須要知道Autorelease就能很好的管理內存。雖然如今已經幾乎用不到MRC,可是瞭解 Objective-C 的內存管理機制仍然是十分必要的,看看編譯器幫助咱們怎麼來管理內存。本文僅僅是記錄本身的學習筆記。緩存

AutoreleasePool簡介

1.什麼是AutoreleasePool

AutoreleasePool:自動釋放池是 Objective-C 開發中的一種自動內存回收管理的機制,爲了替代開發人員手動管理內存,實質上是使用編譯器在適當的位置插入releaseautorelease等內存釋放操做。當對象調用 autorelease方法後會被放到自動釋放池中延遲釋放時機,當緩存池須要清除dealloc時,會向這些 Autoreleased對象作 release 釋放操做。bash

2.對象何時釋放(ARC規則)

通常的說法是對象會在當前做用域大括號結束時釋放, 有這樣一個ARC環境下簡單的例子🌰:首先建立一個ZHPerson類:app

//// ZHPerson.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface ZHPerson : NSObject

+(instancetype)object;
@end

////ZHPerson.m

#import "ZHPerson.h"
@implementation ZHPerson

-(void)dealloc
{
    NSLog(@"ZHPerson dealloc");
}
+(instancetype)object
{
    return [[ZHPerson alloc] init];
}
@end
複製代碼

而後在ViewController.m導入頭文件ZHPerson.h,而後在寫一段這樣的代碼:框架

__weak id temp = nil;
{
    ZHPerson *person = [[ZHPerson alloc] init];
    temp = person;
}
NSLog(@"temp = %@",temp);
複製代碼

解釋一下這個代碼:先聲明瞭一個 __weak 變量temp,由於 __weak 變量有一個特性就是它不會影響所指向對象的生命週期,而後讓變量temp指向建立的person對象,輸出以下: less

這裏超出了 person的做用域,它就被釋放了,看來是正常的。

把上面的建立對象的方法,變一變寫法:函數

__weak id temp = nil;
{
    ZHPerson *person = [ZHPerson object];
    temp = person;
}
NSLog(@"temp = %@",temp);
複製代碼

輸出以下: oop

這裏 person對象超出了其做用域仍是存在的,被延遲釋放了,也就是說其內部調用了 autorelease方法。

小總結學習

查詢得知:以 alloc, copy, ,mutableCopynew這些方法會被默認標記爲 __attribute((ns_returns_retained)) ,以這些方法建立的對象,編譯器在會在調用方法外圍要加上內存管理代碼retain/release,因此其在做用域結束的時候就會釋放,而不以這些關鍵字開頭的方法,會被默認標記爲__attribute((ns_returns_not_retained)),編譯器會在方法內部自動加上autorelease方法,這時建立的對象就會被註冊到自動釋放池中,同時其釋放會延遲,等到自動釋放池銷燬的時候才釋放。測試

3.AutoreleasePool的顯示建立

1.MRC下的建立

//1.生成一個NSAutoreleasePool對象
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//2.建立對象
id object = [[NSObject alloc] init];
//3.對象調用autorelease方法
[object autorelease];
//4.廢棄NSAutoreleasePool對象,會對釋放池中的object發送release消息
[pool drain];
複製代碼

2.ARC下的建立

@autoreleasepool {
    //LLVM會在內部插入autorelease方法
    id object = [[NSObject alloc] init];
    }
複製代碼

AutoreleasePool 的做用前面有提到過,每當一個對象調用 autorelease方法時,其實是將該對象放入當前 AutoreleasePool 中,當前AutoreleasePool 釋放時,會對添加進該 AutoreleasePool 中的對象逐一調用 release 方法。在ARC環境下,並不須要特別的去關注Autoreleasepool的使用,由於系統已經作了處理。ui

AutoreleasePool探索學習

爲了看一下AutoreleasePool到底作了什麼,先來建立一個main.m文件(Xcode -> File -> New Project -> macOS -> Command Line Tool -> main.m);

#import <Foundation/Foundation.h>

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

而後,使用編譯器clang編譯main.m轉化成main.cpp文件(在終端使用命令:clang -rewrite-objc main.m),滑到main.cpp文件的最後,有這樣一段代碼:

這個代碼是把 @autoreleasePool轉換成一個 __AtAutoreleasePool類型的局部私有變量 __AtAutoreleasePool __autoreleasepool;

接着在 main.cpp文件中查詢__AtAutoreleasePool,來看一下它具體的實現:

能夠看到 __AtAutoreleasePool是結構體類型,而且實現了兩個函數:構造函數 __AtAutoreleasePool()和析構函數 ~__AtAutoreleasePool()

也就是說在聲明 __autoreleasepool 變量時,構造函數 __AtAutoreleasePool() 被調用,即執行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;當出了當前做用域時,析構函數 ~__AtAutoreleasePool()被調用,即執行 objc_autoreleasePoolPop(atautoreleasepoolobj); 那麼上面的main.m中的代碼能夠用這種形式代替:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
   // @autoreleasepool
    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        // insert code here...
        NSLog(@"Hello, World!");
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}
複製代碼

接下來看一下析構函數和構造函數分別實現了什麼內容?這裏須要一份objc_runtime的源碼(源碼地址),這裏使用的是objc4-756.2.tar.gz:

這裏兩個函數本質上就是分別調用了 AutoreleasePoolPagepush方法和 pop方法(這裏::是 C++調用方法的形式,相似於點語法)。

1.AutoreleasePoolPage

AutoreleasePoolPage是一個C++實現的類,它的具體實現代碼是:

class AutoreleasePoolPage 
{ 
# define POOL_BOUNDARY nil //哨兵對象(能夠看作是一個邊界)
    static size_t const COUNT = SIZE / sizeof(id);    // 對象數量

    magic_t const magic;    //用來校驗 `AutoreleasePoolPage`的結構是否完整;
    id *next;    //指向最新添加的 `autoreleased` 對象的下一個位置,初始化時指向 `begin()` ;
    pthread_t const thread;    //指向當前線程;
    AutoreleasePoolPage * const parent;    //指向父結點,第一個結點的 `parent` 值爲 `nil` ;
    AutoreleasePoolPage *child;    //指向子結點,最後一個結點的 `child` 值爲 `nil` ;
    uint32_t const depth;    //表明深度,從 `0` 開始,日後遞增 `1`;
    uint32_t hiwat;    //表明 `high water mark` ;
//剩下代碼省略......
}
複製代碼

經過源碼能夠知道這是一個典型的雙向列表結構,因此 AutoreleasePool是由若干個 AutoreleasePoolPage以雙向鏈表的形式組合而成。

AutoreleasePoolPage每一個對象會開闢4096字節內存(虛擬內存一頁的大小),除了上面的實例變量所佔空間,剩下的空間所有用來儲存autorelease對象的地址,AutoreleasepoolPage 經過壓棧的方式來存儲每一個autorelease的對象(從低地址到高地址)。其中next指針做爲遊標指向棧頂最新add進來的autorelease對象的下一個位置,當 next指針指向begin時,表示 AutoreleasePoolPage爲空;當 next指針指向end時,表示 AutoreleasePoolPage 已滿,此時會新建一個AutoreleasePoolPage對象,鏈接鏈表,後來的autorelease對象在新的AutoreleasePoolPage插入,一樣的新AutoreleasePoolPagenext指針被初始化在棧底(指向begin的位置)。

2.AutoreleasePoolPage::push()

既然已經知道了autorelease的對象會經過壓棧的方式插入到AutoreleasePoolPage當中,那麼顯然AutoreleasePoolPagepush方法就承包了AutoreleasePoolPage的建立和插入。

接着看下push方法的源碼:

static inline void *push() 
{
    id *dest;
    //判斷是否已經初始化AutoreleasePoolPage
    if (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;
}
複製代碼

這裏的POOL_BOUNDARY能夠理解爲哨兵對象,或者理解爲一種邊界標識,並且這個POOL_BOUNDARY值爲0,是個nil

接下來,先來看一下autoreleaseFast這個方法,

static inline id *autoreleaseFast(id obj)
{
    //獲取到當前page,這個hotPage是從當前線程的局部私有空間取出來的
    AutoreleasePoolPage *page = hotPage();
    
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}
複製代碼

咱們知道鏈表是有空間的,因此上面👆的源碼能夠理解爲:

(1). 當前page存在且沒有滿時,直接將對象添加到當前page中,即next指向的位置;

(2). 當前page存在而且已滿時,建立一個新的page,並將對象添加到新建立的page 中,而後將這兩個鏈表節點進行連接。

(3). 當前page不存在時,建立第一個page ,並將對象添加到新建立的page中。

這裏重點看一下page->add(obj)這個方法,

id *add(id obj)
{
    assert(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect();
    return ret;
}
複製代碼

能夠看到這裏返回的ret其實next指針指向的地址,由上面的push方法的源碼可知,這裏page->add(obj)傳入的obj其實就是POOL_BOUNDARY,也就是說每一次調用push方法,都會插入一個POOL_BOUNDARY,因此objc_autoreleasePoolPush的返回值就是這個哨兵對象的地址。

3.AutoreleasePoolPage::pop(ctxt)

經過上面對構造函數objc_autoreleasePoolPush的學習,已經知道objc_autoreleasePoolPush返回的是哨兵對象的地址,那麼在調用析構函數objc_autoreleasePoolPop的時候傳入的也就是這個哨兵對象的地址。隨着方法的一步步調用,緊接着來看下AutoreleasePoolPagepop方法的實現:

static inline void pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        if (hotPage()) {
            pop(coldPage()->begin());
        } else {
            setHotPage(nil);
        }
        return;
    }
            
    page = pageForPointer(token);  //根據傳入的哨兵對象的地址,獲取到page中的哨兵對象以後的地址空間
    stop = (id *)token;
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
        } else {
            return badPop(token);
        }
    }
    if (PrintPoolHiwat) printHiwat();
            
    page->releaseUntil(stop); //對當前鏈表當中的對象進行release操做
    if (DebugPoolAllocation  &&  page->empty()) {
    //釋放 `Autoreleased` 對象後,銷燬多餘的 page
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        page->kill();
        setHotPage(nil);
    }
   else if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}
複製代碼

這裏重點看一下page->releaseUntil(stop)方法:

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_BOUNDARY) {
            objc_release(obj);
        }
    }
    setHotPage(this);
}
複製代碼

這裏的stop一樣是POOL_BOUNDARY的地址,這裏分析一下這個方法:

(1). 外部循環挨個遍歷autoreleased 對象,直到遍歷到哨兵對象POOL_BOUNDARY

(2). 若是當前page沒有 POOL_BOUNDARY,而且爲空,則將hotPage設置爲當前page的父節點。

(3). 給當前autoreleased對象發送release消息。

(4). 最後再次配置hotPage

4.autorelease

經過上面的分析已經知道了構造方法objc_autoreleasePoolPush會建立AutoreleasePoolPage,並插入哨兵對象POOL_BOUNDARY,析構方法objc_autoreleasePoolPop會對哨兵對象以後插入的對象發送release消息,那麼在這兩個方法之間,對象經過調用autorelease是怎麼插入到AutoreleasePoolPage的呢?下面來看下autorelease的源碼實現:

static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}
複製代碼

這裏的重點仍是autoreleaseFast(obj);因爲這裏插入對象的方法和AutoreleasePoolPage調用push方法的實現是同樣的,只不過push操做插入的是一個 POOL_BOUNDARY,而autorelease操做插入的是一個具體的autoreleased對象,在此處就不作多餘分析。

經過上面👆的這些分析,已經大概知道AutoreleasePool是怎樣的一個構造,以及內如是如何實現自動釋放的。

5.AutoreleasePool的嵌套

對於嵌套的AutoreleasePool也是一樣的原理,在pop的時候總會釋放對象到上次push的位置爲止,也就是哨兵位置,多層的pool就是插入多個哨兵對象而已,而後根據哨兵對象來進行釋放,就像剝洋蔥同樣一層一層的,互不影響。

那麼這裏有個疑問,若是在AutoreleasePool多層嵌套中是同一個對象呢,那麼會怎麼釋放?下面經過一個小例子🌰來看一下:

@autoreleasepool {
    ZHPerson *person = [ZHPerson object];
    NSLog(@"current count %d",_objc_rootRetainCount(person));
    @autoreleasepool {
        ZHPerson *person1 = person;
        NSLog(@"current count %d",_objc_rootRetainCount(person));
        @autoreleasepool {
            ZHPerson *person2 = person;
            NSLog(@"current count %d",_objc_rootRetainCount(person));
        }
    }
}
複製代碼

打印結果以下:

這裏 dealloc方法只調用了一次,由上面的代碼可知:當前 person1person2是對 person的引用,若是系統會爲每一次引用都自動插入一個 autorelease,那麼對象在執行第一個 autorelease的時候,會調用 objc_release(obj)來釋放當前的對象,那麼當調用 rootRelease()的時候就會報錯,由於當前對象已經被釋放了,那麼也就是說對於引用的對象只會被釋放一次。(同一個對象不可以反覆的 autorelease)

NSthread、NSRunLoop、AutoReleasePool

1.NSthread和AutoReleasePool

先來看個簡單的例子:

temp的位置設置一個斷點,而後在控制檯輸入 watchpoint set variable temp,
等到這個線程執行結束以後,來看一下左側邊欄的內容:
當執行到 NSLog(@"thread end");這句代碼,表示線程執行結束,這裏,其實線程會先調用 [NSthread exit],而後執行 _pthread_tsd_cleanup,清除當前線程的有關資源,而後調用 tls_dealloc,也就是把當前線程關聯的 AutoReleasePool釋放掉,最後調用 weak_clear_no_lock清除指針。

那麼這一系列過程就說明了:在NSThread退出了以後,與NSThread對應的AutoReleasePool也會被自動清空,因此當一個線程結束的時候,就會回收♻️AutoReleasePool中自動釋放的對象。

結論:

每個線程都會維護本身的AutoReleasePool,而每個AutoReleasePool都會對應惟一一個線程,可是線程能夠對應多個AutoReleasePool

2.NSRunLoop和AutoReleasePool

對於NSThread只是一個簡單的線程,若是把它換成一個常駐線程呢?

這裏建立一個 NSTimer,並將其常駐。利用一樣的方式, watchpoint set variable temp,:

能夠看到這裏 NStimer是被加入到子線程當中的,可是在子線程中,咱們並無去寫關於 AutoReleasePool的內容,咱們只知道 test作了 autorelease操做。下面回到源碼中來看:

static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

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

id *autoreleaseNoPage(id obj)
{
   AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
   setHotPage(page);
}
//這裏省略了部分代碼
複製代碼

因此從上面的源碼咱們能夠得出結論:子線程在使用autorelease對象的時候,會懶加載出來一個AutoreleasePoolPage,而後將對象插入進去。

那麼問題又來了,autorelease對象在何時釋放的呢?也就說AutoreleasePoolPage在何時調用了pop方法?

其實在上面建立一個NSThread的時候,在調用[NSthread exit]的時候,會釋放當前資源,也就是把當前線程關聯的autoReleasePool釋放掉,而在這裏當RunLoop執行完成退出的時候,也會執行pop方法,這就說明了爲何在子線程當中,咱們沒有顯示的調用pop,它也能釋放當前AutoreleasePool的資源的緣由。

3.主線程的NSRunLoop和AutoReleasePool

那麼在主線程的RunLoop到底何時把對象進行釋放回收的呢?

簡單粗暴點,直接在控制檯經過po [NSRunloop currentRunloop]打印主線程的RunLoop:

這裏,系統在主線程的RunLoop裏註冊了兩個Observer,回調都是_wrapRunLoopWithAutoreleasePoolHandler,第一個Observer的狀態是activities = 0x1,第二個Observer的狀態是activities = 0xa0,這兩種狀態表明什麼意思呢?

先在這裏插入一點RunLoop的內容(RunLoop的狀態枚舉):

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),              // 1
    kCFRunLoopBeforeTimers = (1UL << 1),       // 2
    kCFRunLoopBeforeSources = (1UL << 2),      // 4
    kCFRunLoopBeforeWaiting = (1UL << 5),      // 32
    kCFRunLoopAfterWaiting = (1UL << 6),       // 64
    kCFRunLoopExit = (1UL << 7),               // 128
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼

0x1表明的是kCFRunLoopEntry,也就是說第一個 Observer監視的事件是Entry(即將進入Loop時),其回調內會調用_objc_autoreleasePoolPush()建立一個自動釋放池。其order優先級是-2147483647,優先級最高,保證建立自動釋放池發生在其餘全部回調以前。

0xa0對應的是kCFRunLoopBeforeWaitingkCFRunLoopExit,也就是說第二個Observer監視了兩個事件:kCFRunLoopBeforeWaiting準備進入休眠,kCFRunLoopExit即將退出RunLoop。在kCFRunLoopBeforeWaiting事件時調用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 釋放舊的自動釋放池並建立新的自動釋放池;在kCFRunLoopExit事件時調用_objc_autoreleasePoolPop() 來釋放自動釋放池,同時這個Observerorder優先級是 2147483647,優先級最低,保證其釋放自動釋放池的操做發生在其餘全部回調以後。

因此在沒有手動增長AutoreleasePool的狀況下,Autorelease對象都是在當前的runloop迭代結束時釋放的,而它可以釋放的緣由是系統在每一個runloop迭代中都加入了自動釋放池pushpop操做。

總結:

對於不一樣線程,應當建立本身的AutoReleasePool。若是應用長期存在,應該按期drain和建立新的AutoReleasePool,AutoReleasePoolRunLoop 與線程是一一對應的關係,AutoReleasePoolRunLoop在開始迭代時作push操做,在RunLoop休眠或者迭代結束時作pop操做。

AutoreleasePool的應用場景

一般狀況下咱們是不須要手動建立AutoreleasePool,可是也有一些特殊的:

  1. 編寫的程序不基於UI框架,如命令行程序。

  2. 在循環中建立大量臨時對象時用以下降內存佔用峯值。

  3. 在主線程以外建立新的線程,在新線程開始執行處,建立本身的AutoreleasePool,不然將致使內存泄漏。

下面就來簡單看下第二種狀況,直接來個for循環:

for (int i = 0; i < 100000000; i ++) {
        NSString * str = [NSString stringWithFormat:@"noAutoReleasePool"];
        NSString *tempstr = str;
    }
}
複製代碼

來看一下Memory的使用狀況:

相反的,若是加上AutoreleasePool,來看一下:

for (int i = 0; i < 100000000; i ++) {
    @autoreleasepool {
        NSString * str = [NSString stringWithFormat:@"AutoReleasePool"];
        NSString *tempstr = str;
    }
}
複製代碼

來看一下這種狀況下的Memory的使用狀況:

這個對比傷害就很明顯了。

這個作個備註:在主函數main.m文件中的@autoreleasepool,若是在這裏作個測試,使用for循環建立大量的臨時對象,是否加上這個@autoreleasepoolMemory的使用狀況沒有特別大的影響。

總結

寫到這裏,對於AutoReleasePool學習內容就暫告一段了,正常狀況下,咱們不須要去關心AutoReleasePool的建立和釋放,可是學習理解了AutoReleasePool可以使咱們更加理解ARC模式下系統是怎樣來管理內存的。

文中內容若有不當之處,還請指出,謝謝您!

相關文章
相關標籤/搜索