1. 程式人生 > IOS開發 >iOS -- Autorelease & AutoreleasePool

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 釋放操作。

2.物件什麼時候釋放(ARC規則)

一般的說法是物件會在當前作用域大括號結束時釋放, 有這樣一個ARC環境下簡單的例子?:首先建立一個ZHPerson類:

//// 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物件,輸出如下:

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

把上面的建立物件的方法,變一變寫法:

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

輸出如下:

這裡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的使用,因為系統已經做了處理。

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模式下系統是怎樣來管理記憶體的。

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