AutoreleasePool詳解和runloop的關(guān)系

內(nèi)存管理一直是學(xué)習(xí) Objective-C 的重點(diǎn)和難點(diǎn)之一,在實(shí)際的軟件開發(fā)工作中,經(jīng)常會遇見由于內(nèi)存原因而導(dǎo)致的crash。而autorelease pool在內(nèi)存管理中有著舉足輕重的作用,只有理解了 autorelease pool 的原理,我們才算是真正了解了 Objective-C 的內(nèi)存管理機(jī)制。下面我會從以下幾個(gè)方面給大家講解:

· NSAutoreleasePool是什么?
· NSAutoreleasePool的實(shí)現(xiàn)原理是什么?
· NSAutoreleasePool何時(shí)釋放?
· 如何使用Autorelease Pool Blocks
· AutoreleasePool與runloop和線程的關(guān)系
· NSAutoreleasePool是什么?

NSAutoreleasePool是什么?

NSAutoreleasePool實(shí)際上是個(gè)對象引用計(jì)數(shù)自動(dòng)處理器,在官方文檔中被稱為是一個(gè)類。Objective-C的對象(全部繼承自NSObject),就是使用引用計(jì)數(shù)的方法來管理對象的存活,眾所周知,當(dāng)引用計(jì)數(shù)為0時(shí),對象就被銷毀了。操作非常簡單,當(dāng)對象被創(chuàng)建時(shí),引用計(jì)數(shù)被設(shè)成1。可以給對象發(fā)送retain消息,讓對象對自己的引用計(jì)數(shù)加1。而當(dāng)對象接受到release消息時(shí),對象就會對自己的引用計(jì)數(shù)進(jìn)行減1,當(dāng)引用計(jì)數(shù)到了0,對象就會調(diào)用自己的dealloc處理。當(dāng)對象被加入到NSAutoreleasePool中,會對其對象retain一次,當(dāng)NSAutoreleasePool結(jié)束時(shí),會對其所有對象發(fā)送一次release消息。NSAutoreleasePool可以同時(shí)有多個(gè),它的組織是個(gè)棧,總是存在一個(gè)棧頂pool,也就是當(dāng)前pool,每創(chuàng)建一個(gè)pool,就往棧里壓一個(gè),改變當(dāng)前pool為新建的pool,然后,每次給pool發(fā)送drain消息,就彈出棧頂?shù)膒ool,改當(dāng)前pool為棧里的下一個(gè)pool。

NSAutoreleasePool的實(shí)現(xiàn)原理是什么?

AutoreleasePoolPage

ARC下,我們使用@autoreleasepool{}來使用一個(gè)AutoreleasePool,隨后編譯器將其改寫成下面的樣子:

void *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);

@autoreleasepool {} 在編譯時(shí) @autoreleasepool {} 被轉(zhuǎn)換為一個(gè)__AtAutoreleasePool ,通常這個(gè)結(jié)構(gòu)體會在初始化時(shí)調(diào)用 objc_autoreleasePoolPush()方法,在析構(gòu)時(shí)調(diào)用 objc_autoreleasePoolPop () 方法。而這些方法都是對AutoreleasePoolPage的簡單封裝。AutoreleasePool并沒有單獨(dú)的結(jié)構(gòu),而是由若干個(gè)AutoreleasePoolPage以雙向鏈表的形式組合而成(分別對應(yīng)結(jié)構(gòu)中的parent指針和child指針)。

而這兩個(gè)函數(shù)都是對AutoreleasePoolPage的簡單封裝,所以自動(dòng)釋放機(jī)制的核心就在于這個(gè)類。

-[NSAutoreleasePool release] 方法最終是通過調(diào)用 AutoreleasePoolPage::pop(void *) 函數(shù)來負(fù)責(zé)對 autoreleasepool 中的 autoreleased 對象執(zhí)行 release 操作的。

那這里的 AutoreleasePoolPage 是什么東西呢?其實(shí),autoreleasepool 是沒有單獨(dú)的內(nèi)存結(jié)構(gòu)的,它是通過以 AutoreleasePoolPage 為結(jié)點(diǎn)的雙向鏈表來實(shí)現(xiàn)的。我們打開 runtime 的源碼工程,在 NSObject.mm 文件的第 438-932 行可以找到 autoreleasepool 的實(shí)現(xiàn)源碼。通過閱讀源碼,我們可以知道:

  • 每一個(gè)線程的 autoreleasepool 其實(shí)就是一個(gè)指針的堆棧;
  • 每一個(gè)指針代表一個(gè)需要 release 的對象或者 POOL_SENTINEL(哨兵對象,代表一個(gè) autoreleasepool 的邊界);
  • 一個(gè) pool token 就是這個(gè) pool 所對應(yīng)的 POOL_SENTINEL 的內(nèi)存地址。當(dāng)這個(gè) pool 被 pop 的時(shí)候,所有內(nèi)存地址在 pool token 之后的對象都會被 release ;
  • 這個(gè)堆棧被劃分成了一個(gè)以 page 為結(jié)點(diǎn)的雙向鏈表。pages 會在必要的時(shí)候動(dòng)態(tài)地增加或刪除;
  • Thread-local storage(線程局部存儲)指向 hot page ,即最新添加的 autoreleased 對象所在的那個(gè) page 。

一個(gè)空的 AutoreleasePoolPage 的內(nèi)存結(jié)構(gòu)如下圖所示:

AutoreleasePoolPage
  1. magic 用來校驗(yàn) AutoreleasePoolPage 的結(jié)構(gòu)是否完整;
  2. next 指向最新添加的 autoreleased 對象的下一個(gè)位置,初始化時(shí)指向 begin()
  3. thread 指向當(dāng)前線程;
  4. parent 指向父結(jié)點(diǎn),第一個(gè)結(jié)點(diǎn)的 parent 值為 nil
  5. child 指向子結(jié)點(diǎn),最后一個(gè)結(jié)點(diǎn)的 child 值為 nil
  6. depth 代表深度,從 0 開始,往后遞增 1;
  7. hiwat 代表 high water mark ,表示最大可容納節(jié)點(diǎn)數(shù)量。

另外,當(dāng) next == begin() 時(shí),表示 AutoreleasePoolPage 為空;當(dāng) next == end() 時(shí),表示 AutoreleasePoolPage 已滿。

Autorelease Pool Blocks

我們使用 clang -rewrite-objc 命令將下面的 Objective-C 代碼重寫成 C++ 代碼:

@autoreleasepool {

}

將會得到以下輸出結(jié)果(只保留了相關(guān)代碼):

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

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

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

}

不得不說,蘋果對 @autoreleasepool {} 的實(shí)現(xiàn)真的是非常巧妙,真正可以稱得上是代碼的藝術(shù)。蘋果通過聲明一個(gè) __AtAutoreleasePool 類型的局部變量 __autoreleasepool 來實(shí)現(xiàn) @autoreleasepool {} 。當(dāng)聲明 __autoreleasepool 變量時(shí),構(gòu)造函數(shù) __AtAutoreleasePool()被調(diào)用,即執(zhí)行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;當(dāng)出了當(dāng)前作用域時(shí),析構(gòu)函數(shù) ~__AtAutoreleasePool() 被調(diào)用,即執(zhí)行 objc_autoreleasePoolPop(atautoreleasepoolobj); 。也就是說 @autoreleasepool {} 的實(shí)現(xiàn)代碼可以進(jìn)一步簡化如下:

/* @autoreleasepool */ {
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    // 用戶代碼,所有接收到 autorelease 消息的對象會被添加到這個(gè) autoreleasepool 中
    objc_autoreleasePoolPop(atautoreleasepoolobj);
}

因此,單個(gè) autoreleasepool 的運(yùn)行過程可以簡單地理解為 objc_autoreleasePoolPush()[對象 autorelease]objc_autoreleasePoolPop(void *) 三個(gè)過程。

push 操作

上面提到的 objc_autoreleasePoolPush() 函數(shù)本質(zhì)上就是調(diào)用的 AutoreleasePoolPage 的 push 函數(shù)。

void *objc_autoreleasePoolPush(void)
{
    if (UseGC) return nil;
    return AutoreleasePoolPage::push();
}

因此,我們接下來看看 AutoreleasePoolPage 的 push 函數(shù)的作用和執(zhí)行過程。一個(gè) push 操作其實(shí)就是創(chuàng)建一個(gè)新的 autoreleasepool ,對應(yīng) AutoreleasePoolPage 的具體實(shí)現(xiàn)就是往 AutoreleasePoolPage 中的 next 位置插入一個(gè) POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的內(nèi)存地址。這個(gè)地址也就是我們前面提到的 pool token ,在執(zhí)行 pop 操作的時(shí)候作為函數(shù)的入?yún)ⅰ?/p>

static inline void *push()
{
    id *dest = autoreleaseFast(POOL_SENTINEL);
    assert(*dest == POOL_SENTINEL);
    return dest;
}

push 函數(shù)通過調(diào)用 autoreleaseFast 函數(shù)來執(zhí)行具體的插入操作。

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

autoreleaseFast 函數(shù)在執(zhí)行一個(gè)具體的插入操作時(shí),分別對三種情況進(jìn)行了不同的處理:

  1. 當(dāng)前 page 存在且沒有滿時(shí),直接將對象添加到當(dāng)前 page 中,即 next 指向的位置;
  2. 當(dāng)前 page 存在且已滿時(shí),創(chuàng)建一個(gè)新的 page ,并將對象添加到新創(chuàng)建的 page 中;
  3. 當(dāng)前 page 不存在時(shí),即還沒有 page 時(shí),創(chuàng)建第一個(gè) page ,并將對象添加到新創(chuàng)建的 page 中。

每調(diào)用一次 push 操作就會創(chuàng)建一個(gè)新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一個(gè) POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的內(nèi)存地址。

autorelease 操作

通過 NSObject.mm 源文件,我們可以找到 -autorelease 方法的實(shí)現(xiàn):

- (id)autorelease {
    return ((id)self)->rootAutorelease();
}

通過查看 ((id)self)->rootAutorelease() 的方法調(diào)用,我們發(fā)現(xiàn)最終調(diào)用的就是 AutoreleasePoolPage 的 autorelease 函數(shù)。

__attribute__((noinline,used))
id objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

AutoreleasePoolPage 的 autorelease 函數(shù)的實(shí)現(xiàn)對我們來說就比較容量理解了,它跟 push 操作的實(shí)現(xiàn)非常相似。只不過 push 操作插入的是一個(gè) POOL_SENTINEL ,而 autorelease 操作插入的是一個(gè)具體的 autoreleased 對象。

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

pop 操作

同理,前面提到的 objc_autoreleasePoolPop(void *) 函數(shù)本質(zhì)上也是調(diào)用的 AutoreleasePoolPage 的 pop 函數(shù)。

void objc_autoreleasePoolPop(void *ctxt)
{
    if (UseGC) return;

    // fixme rdar://9167170
    if (!ctxt) return;

    AutoreleasePoolPage::pop(ctxt);
}

pop 函數(shù)的入?yún)⒕褪?push 函數(shù)的返回值,也就是 POOL_SENTINEL 的內(nèi)存地址,即 pool token 。當(dāng)執(zhí)行 pop 操作時(shí),內(nèi)存地址在 pool token 之后的所有 autoreleased 對象都會被 release 。直到 pool token 所在 page 的 next 指向 pool token 為止。

下面是某個(gè)線程的 autoreleasepool 堆棧的內(nèi)存結(jié)構(gòu)圖,在這個(gè) autoreleasepool 堆棧中總共有兩個(gè) POOL_SENTINEL ,即有兩個(gè) autoreleasepool 。該堆棧由三個(gè) AutoreleasePoolPage 結(jié)點(diǎn)組成,第一個(gè) AutoreleasePoolPage 結(jié)點(diǎn)為 coldPage() ,最后一個(gè) AutoreleasePoolPage 結(jié)點(diǎn)為 hotPage() 。其中,前兩個(gè)結(jié)點(diǎn)已經(jīng)滿了,最后一個(gè)結(jié)點(diǎn)中保存了最新添加的 autoreleased 對象 objr3 的內(nèi)存地址。

AutoreleasePoolPage1

此時(shí),如果執(zhí)行 pop(token1) 操作,那么該 autoreleasepool 堆棧的內(nèi)存結(jié)構(gòu)將會變成如下圖所示:

AutoreleasePoolPage2

NSAutoreleasePool何時(shí)釋放?

當(dāng)別人問你NSAutoreleasePool何時(shí)釋放?你回答“當(dāng)前作用域大括號結(jié)束時(shí)釋放”,顯然木有正確理解Autorelease機(jī)制。
在沒有手加Autorelease Pool的情況下,Autorelease對象是在當(dāng)前的runloop迭代結(jié)束時(shí)釋放的,而它能夠釋放的原因是系統(tǒng)在每個(gè)runloop迭代中都加入了自動(dòng)釋放池Push和Pop

小實(shí)驗(yàn)。

__weak id reference = nil;
- (void)viewDidLoad {
    [super viewDidLoad];    
    NSString *str = [NSString stringWithFormat:@"sunnyxx"];    // str是一個(gè)autorelease對象,設(shè)置一個(gè)weak的引用來觀察它
    reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];    
    NSLog(@"%@", reference); // Console: sunnyxx
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];    
    NSLog(@"%@", reference); // Console: (null)
}

由于這個(gè)vc在loadView之后便add到了window層級上,所以viewDidLoad和viewWillAppear是在同一個(gè)runloop調(diào)用的,因此在viewWillAppear中,這個(gè)autorelease的變量依然有值。

當(dāng)然,我們也可以手動(dòng)干預(yù)Autorelease對象的釋放時(shí)機(jī):

- (void)viewDidLoad
{
    [super viewDidLoad];
    @autoreleasepool {        
        NSString *str = [NSString stringWithFormat:@"sunnyxx"];
    }    
    NSLog(@"%@", str); // Console: (null)
}

如何使用AutoreleasePool Blocks

一些程序會創(chuàng)建大量自動(dòng)釋放的臨時(shí)對象。這些對象會在代碼塊結(jié)束之前增加程序的內(nèi)存占用率。有時(shí)候,在當(dāng)前時(shí)間循環(huán)迭代結(jié)束之前累積臨時(shí)變量不會導(dǎo)致內(nèi)存資源的過分消耗,但也有時(shí)候,巨大數(shù)量的臨時(shí)對象會占據(jù)客觀的內(nèi)存資源,這時(shí)你可能會希望對象盡可能快地被銷毀。在后者的情況下,你可能會需要?jiǎng)?chuàng)建自己的自動(dòng)釋放池塊。在代碼塊的末尾,這些臨時(shí)對象會被釋放,通常這些對象的釋放會降低程序的內(nèi)存使用率。
大多情況下臨時(shí)對象會一直在內(nèi)存中聚集,直到當(dāng)前的一次runloop迭代結(jié)束這樣對內(nèi)存造成很大的負(fù)擔(dān)。

蘋果用了一段代碼舉例說明

NSArray *urls = <# An array of file URLs #>; 

for (NSURL *url in urls) { 

    @autoreleasepool { 

        NSError *error; 

        NSString *fileContents = [NSString stringWithContentsOfURL:url 

                                         encoding:NSUTF8StringEncoding error:&error]; 

        /* Process the string, creating and autoreleasing more objects. */ 

    } 

}

每次循環(huán)代碼塊結(jié)束,里面的臨時(shí)對象也會釋放,這樣就很好的解決了內(nèi)存占用的問題。

NSThread、NSRunLoop 和 NSAutoreleasePool

根據(jù)蘋果官方文檔中對 NSRunLoop 的描述,我們可以知道每一個(gè)線程,包括主線程,都會擁有一個(gè)專屬的 NSRunLoop 對象,并且會在有需要的時(shí)候自動(dòng)創(chuàng)建。

Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed.

同樣的,根據(jù)蘋果官方文檔中對 NSAutoreleasePool 的描述,我們可知,在主線程的 NSRunLoop 對象(在系統(tǒng)級別的其他線程中應(yīng)該也是如此,比如通過 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 獲取到的線程)的每個(gè) event loop 開始前,系統(tǒng)會自動(dòng)創(chuàng)建一個(gè) autoreleasepool ,并在 event loop 結(jié)束時(shí) drain 。我們上面提到的場景 1 中創(chuàng)建的 autoreleased 對象就是被系統(tǒng)添加到了這個(gè)自動(dòng)創(chuàng)建的 autoreleasepool 中,并在這個(gè) autoreleasepool 被 drain 時(shí)得到釋放。

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

另外,NSAutoreleasePool 中還提到,每一個(gè)線程都會維護(hù)自己的 autoreleasepool 堆棧。換句話說 autoreleasepool 是與線程緊密相關(guān)的,每一個(gè) autoreleasepool 只對應(yīng)一個(gè)線程。

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.

弄清楚 NSThread、NSRunLoop 和 NSAutoreleasePool 三者之間的關(guān)系可以幫助我們從整體上了解 Objective-C 的內(nèi)存管理機(jī)制,清楚系統(tǒng)在背后到底為我們做了些什么,理解整個(gè)運(yùn)行機(jī)制等。

其他Autorelease相關(guān)知識點(diǎn)

使用容器的block版本的枚舉器時(shí),內(nèi)部會自動(dòng)添加一個(gè)AutoreleasePool:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 這里被一個(gè)局部@autoreleasepool包圍著
}];

當(dāng)然,在普通for循環(huán)和for in循環(huán)中沒有,所以,還是新版的block版本枚舉器更加方便。for循環(huán)中遍歷產(chǎn)生大量autorelease變量時(shí),就需要手加局部AutoreleasePool咯。

總結(jié)

看到這里,相信你應(yīng)該對 Objective-C 的內(nèi)存管理機(jī)制有了更進(jìn)一步的認(rèn)識。通常情況下,我們是不需要手動(dòng)添加 autoreleasepool 的,使用線程自動(dòng)維護(hù)的 autoreleasepool 就好了。根據(jù)蘋果官方文檔中對 Using Autorelease Pool Blocks 的描述,我們知道在下面三種情況下是需要我們手動(dòng)添加 autoreleasepool 的:

  1. 如果你編寫的程序不是基于 UI 框架的,比如說命令行工具;
  2. 如果你編寫的循環(huán)中創(chuàng)建了大量的臨時(shí)對象;
  3. 如果你創(chuàng)建了一個(gè)輔助線程。

參考鏈接:
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
http://blog.leichunfeng.com/blog/2015/05/31/objective-c-autorelease-pool-implementation-principle/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容