OC中Autorelease Pool實現原理與autorelease何時被釋放

autorelease 基本用法

1,對象執行autorelease方法時會將對象添加到自動釋放池中

2,當自動釋放池銷燬時自動釋放池中所有對象作release操作

3,對象執行autorelease方法後自身引用計數器不會改變,而且會返回對象本身

autoreleased 對象什麼時候釋放

autorelease 本質上就是延遲調用 release ,那 autoreleased 對象究竟會在什麼時候釋放呢?爲了弄清楚這個問題,我們先來做一個小實驗。這個小實驗分 3 種場景進行,請你先自行思考在每種場景下的 console 輸出,以加深理解。注:本實驗的源碼可以在這裏 AutoreleasePool 找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
__weak NSString *string_weak_ = nil;
- (void)viewDidLoad {
    [super viewDidLoad];
    // 場景 1
    NSString *string = [NSString stringWithFormat:@"leichunfeng"];
    string_weak_ = string;
    // 場景 2
//    @autoreleasepool {
//        NSString *string = [NSString stringWithFormat:@"leichunfeng"];
//        string_weak_ = string;
//    }
    // 場景 3
//    NSString *string = nil;
//    @autoreleasepool {
//        string = [NSString stringWithFormat:@"leichunfeng"];
//        string_weak_ = string;
//    }
    NSLog(@"string: %@", string_weak_);
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"string: %@", string_weak_);
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"string: %@", string_weak_);
}

思考得怎麼樣了?相信在你心中已經有答案了。那麼讓我們一起來看看 console 輸出:

1
2
3
4
5
6
7
8
9
10
11
12
// 場景 1
2015-05-30 10:32:20.837 AutoreleasePool[33876:1448343] string: leichunfeng
2015-05-30 10:32:20.838 AutoreleasePool[33876:1448343] string: leichunfeng
2015-05-30 10:32:20.845 AutoreleasePool[33876:1448343] string: (null)
// 場景 2
2015-05-30 10:32:50.548 AutoreleasePool[33915:1448912] string: (null)
2015-05-30 10:32:50.549 AutoreleasePool[33915:1448912] string: (null)
2015-05-30 10:32:50.555 AutoreleasePool[33915:1448912] string: (null)
// 場景 3
2015-05-30 10:33:07.075 AutoreleasePool[33984:1449418] string: leichunfeng
2015-05-30 10:33:07.075 AutoreleasePool[33984:1449418] string: (null)
2015-05-30 10:33:07.094 AutoreleasePool[33984:1449418] string: (null)

跟你預想的結果有出入嗎?Any way ,我們一起來分析下爲什麼會得到這樣的結果。

分析:3 種場景下,我們都通過 [NSString stringWithFormat:@"leichunfeng"] 創建了一個 autoreleased 對象,這是我們實驗的前提。並且,爲了能夠在 viewWillAppear 和 viewDidAppear 中繼續訪問這個對象,我們使用了一個全局的 __weak 變量 string_weak_ 來指向它。因爲 __weak 變量有一個特性就是它不會影響所指向對象的生命週期,這裏我們正是利用了這個特性。

場景 1:當使用 [NSString stringWithFormat:@"leichunfeng"] 創建一個對象時,這個對象的引用計數爲 1 ,並且這個對象被系統自動添加到了當前的 autoreleasepool 中。當使用局部變量 string 指向這個對象時,這個對象的引用計數 +1 ,變成了 2 。因爲在 ARC 下 NSString *string 本質上就是 __strong NSString *string 。所以在 viewDidLoad 方法返回前,這個對象是一直存在的,且引用計數爲 2 。而當 viewDidLoad 方法返回時,局部變量 string 被回收,指向了 nil 。因此,其所指向對象的引用計數 -1 ,變成了 1 。

而在 viewWillAppear 方法中,我們仍然可以打印出這個對象的值,說明這個對象並沒有被釋放。咦,這不科學吧?我讀書少,你表騙我。不是一直都說當函數返回的時候,函數內部產生的對象就會被釋放的嗎?如果你這樣想的話,那我只能說:騷年你太年經了。開個玩笑,我們繼續。前面我們提到了,這個對象是一個 autoreleased 對象,autoreleased 對象是被添加到了當前最近的 autoreleasepool 中的,只有當這個 autoreleasepool 自身 drain 的時候,autoreleasepool 中的 autoreleased 對象纔會被 release 。

另外,我們注意到當在 viewDidAppear 中再打印這個對象的時候,對象的值變成了 nil ,說明此時對象已經被釋放了。因此,我們可以大膽地猜測一下,這個對象一定是在 viewWillAppear 和 viewDidAppear 方法之間的某個時候被釋放了,並且是由於它所在的 autoreleasepool 被 drain 的時候釋放的。

你說什麼就是什麼咯?有本事你就證明給我看你媽是你媽。額,這個我真證明不了,不過上面的猜測我還是可以證明的,不信,你看!

在開始前,我先簡單地說明一下原理,我們可以通過使用 lldb 的 watchpoint 命令來設置觀察點,觀察全局變量 string_weak_ 的值的變化,string_weak_ 變量保存的就是我們創建的 autoreleased 對象的地址。在這裏,我們再次利用了 __weak 變量的另外一個特性,就是當它所指向的對象被釋放時,__weak 變量的值會被置爲 nil 。瞭解了基本原理後,我們開始驗證上面的猜測。

我們先在第 35 行打一個斷點,當程序運行到這個斷點時,我們通過 lldb 命令 watchpoint set v string_weak_ 設置觀察點,觀察 string_weak_ 變量的值的變化。如下圖所示,我們將在 console 中看到類似的輸出,說明我們已經成功地設置了一個觀察點:

1.jpg

設置好觀察點後,點擊 Continue program execution 按鈕,繼續運行程序,我們將看到如下圖所示的界面:

2.jpg

我們先看 console 中的輸出,注意到 string_weak_ 變量的值由 0x00007f9b886567d0 變成了 0x0000000000000000 ,也就是 nil 。說明此時它所指向的對象被釋放了。另外,我們也可以注意到一個細節,那就是 console 中打印了兩次對象的值,說明此時 viewWillAppear 也已經被調用了,而 viewDidAppear 還沒有被調用。

接着,我們來看看左側的線程堆棧。我們看到了一個非常敏感的方法調用 -[NSAutoreleasePool release] ,這個方法最終通過調用 AutoreleasePoolPage::pop(void *) 函數來負責對 autoreleasepool 中的 autoreleased 對象執行 release 操作。結合前面的分析,我們知道在 viewDidLoad 中創建的 autoreleased 對象在方法返回後引用計數爲 1 ,所以經過這裏的 release 操作後,這個對象的引用計數 -1 ,變成了 0 ,該 autoreleased 對象最終被釋放,猜測得證。

另外,值得一提的是,我們在代碼中並沒有手動添加 autoreleasepool ,那這個 autoreleasepool 究竟是哪裏來的呢?看完後面的章節你就明白了。

場景 2:同理,當通過 [NSString stringWithFormat:@"leichunfeng"] 創建一個對象時,這個對象的引用計數爲 1 。而當使用局部變量 string 指向這個對象時,這個對象的引用計數 +1 ,變成了 2 。而出了當前作用域時,局部變量 string 變成了 nil ,所以其所指向對象的引用計數變成 1 。另外,我們知道當出了 @autoreleasepool {} 的作用域時,當前 autoreleasepool 被 drain ,其中的 autoreleased 對象被 release 。所以這個對象的引用計數變成了 0 ,對象最終被釋放。

場景 3:同理,當出了 @autoreleasepool {} 的作用域時,其中的 autoreleased 對象被 release ,對象的引用計數變成 1 。當出了局部變量 string 的作用域,即 viewDidLoad 方法返回時,string 指向了 nil ,其所指向對象的引用計數變成 0 ,對象最終被釋放。

理解在這 3 種場景下,autoreleased 對象什麼時候釋放對我們理解 Objective-C 的內存管理機制非常有幫助。其中,場景 1 出現得最多,就是不需要我們手動添加 @autoreleasepool {} 的情況,直接使用系統維護的 autoreleasepool ;場景 2 就是需要我們手動添加 @autoreleasepool {} 的情況,手動干預 autoreleased 對象的釋放時機;場景 3 是爲了區別場景 2 而引入的,在這種場景下並不能達到出了 @autoreleasepool {} 的作用域時 autoreleased 對象被釋放的目的。

PS:請讀者參考場景 1 的分析過程,使用 lldb 命令 watchpoint 自行驗證下在場景 2 和場景 3 下 autoreleased 對象的釋放時機,you should give it a try yourself 。

AutoreleasePoolPage

細心的讀者應該已經有所察覺,我們在上面已經提到了 -[NSAutoreleasePool release] 方法最終是通過調用 AutoreleasePoolPage::pop(void *) 函數來負責對 autoreleasepool 中的 autoreleased 對象執行 release 操作的。

那這裏的 AutoreleasePoolPage 是什麼東西呢?其實,autoreleasepool 是沒有單獨的內存結構的,它是通過以 AutoreleasePoolPage 爲結點的雙向鏈表來實現的。我們打開 runtime 的源碼工程,在 NSObject.mm 文件的第 438-932 行可以找到 autoreleasepool 的實現源碼。通過閱讀源碼,我們可以知道:

  • 每一個線程的 autoreleasepool 其實就是一個指針的堆棧;

  • 每一個指針代表一個需要 release 的對象或者 POOL_SENTINEL(哨兵對象,代表一個 autoreleasepool 的邊界);

  • 一個 pool token 就是這個 pool 所對應的 POOL_SENTINEL 的內存地址。當這個 pool 被 pop 的時候,所有內存地址在 pool token 之後的對象都會被 release ;

  • 這個堆棧被劃分成了一個以 page 爲結點的雙向鏈表。pages 會在必要的時候動態地增加或刪除;

  • Thread-local storage(線程局部存儲)指向 hot page ,即最新添加的 autoreleased 對象所在的那個 page 。

一個空的 AutoreleasePoolPage 的內存結構如下圖所示:

blob.png

  1. magic 用來校驗 AutoreleasePoolPage 的結構是否完整;

  2. next 指向最新添加的 autoreleased 對象的下一個位置,初始化時指向 begin() ;

  3. thread 指向當前線程;

  4. parent 指向父結點,第一個結點的 parent 值爲 nil ;

  5. child 指向子結點,最後一個結點的 child 值爲 nil ;

  6. depth 代表深度,從 0 開始,往後遞增 1;

  7. hiwat 代表 high water mark 。

另外,當 next == begin() 時,表示 AutoreleasePoolPage 爲空;當 next == end() 時,表示 AutoreleasePoolPage 已滿。

Autorelease Pool Blocks

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

1
2
@autoreleasepool {
}

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

1
2
3
4
5
6
7
8
9
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 {} 的實現真的是非常巧妙,真正可以稱得上是代碼的藝術。蘋果通過聲明一個 __AtAutoreleasePool 類型的局部變量 __autoreleasepool 來實現 @autoreleasepool {} 。當聲明 __autoreleasepool 變量時,構造函數 __AtAutoreleasePool() 被調用,即執行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;當出了當前作用域時,析構函數 ~__AtAutoreleasePool() 被調用,即執行 objc_autoreleasePoolPop(atautoreleasepoolobj); 。也就是說 @autoreleasepool {} 的實現代碼可以進一步簡化如下:

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

因此,單個 autoreleasepool 的運行過程可以簡單地理解爲 objc_autoreleasePoolPush()、[對象 autorelease] 和 objc_autoreleasePoolPop(void *) 三個過程。

push 操作

上面提到的 objc_autoreleasePoolPush() 函數本質上就是調用的 AutoreleasePoolPage 的 push 函數。

1
2
3
4
5
6
void *
objc_autoreleasePoolPush(void)
{
    if (UseGC) return nil;
    return AutoreleasePoolPage::push();
}

因此,我們接下來看看 AutoreleasePoolPage 的 push 函數的作用和執行過程。一個 push 操作其實就是創建一個新的 autoreleasepool ,對應 AutoreleasePoolPage 的具體實現就是往 AutoreleasePoolPage 中的 next 位置插入一個 POOL_SENTINEL ,並且返回插入的 POOL_SENTINEL 的內存地址。這個地址也就是我們前面提到的 pool token ,在執行 pop 操作的時候作爲函數的入參。

1
2
3
4
5
6
static inline void *push()
{
    id *dest = autoreleaseFast(POOL_SENTINEL);
    assert(*dest == POOL_SENTINEL);
    return dest;
}

push 函數通過調用 autoreleaseFast 函數來執行具體的插入操作。

1
2
3
4
5
6
7
8
9
10
11
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 函數在執行一個具體的插入操作時,分別對三種情況進行了不同的處理:

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

  2. 當前 page 存在且已滿時,創建一個新的 page ,並將對象添加到新創建的 page 中;

  3. 當前 page 不存在時,即還沒有 page 時,創建第一個 page ,並將對象添加到新創建的 page 中。

每調用一次 push 操作就會創建一個新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一個 POOL_SENTINEL ,並且返回插入的 POOL_SENTINEL 的內存地址。

autorelease 操作

通過 NSObject.mm 源文件,我們可以找到 -autorelease 方法的實現:

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

通過查看 ((id)self)->rootAutorelease() 的方法調用,我們發現最終調用的就是 AutoreleasePoolPage 的 autorelease 函數。

1
2
3
4
5
6
7
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

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

1
2
3
4
5
6
7
8
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 *) 函數本質上也是調用的 AutoreleasePoolPage 的 pop 函數。

1
2
3
4
5
6
7
8
void
objc_autoreleasePoolPop(void *ctxt)
{
    if (UseGC) return;
    // fixme rdar://9167170
    if (!ctxt) return;
    AutoreleasePoolPage::pop(ctxt);
}

pop 函數的入參就是 push 函數的返回值,也就是 POOL_SENTINEL 的內存地址,即 pool token 。當執行 pop 操作時,內存地址在 pool token 之後的所有 autoreleased 對象都會被 release 。直到 pool token 所在 page 的 next 指向 pool token 爲止。

下面是某個線程的 autoreleasepool 堆棧的內存結構圖,在這個 autoreleasepool 堆棧中總共有兩個 POOL_SENTINEL ,即有兩個 autoreleasepool 。該堆棧由三個 AutoreleasePoolPage 結點組成,第一個 AutoreleasePoolPage 結點爲 coldPage() ,最後一個 AutoreleasePoolPage 結點爲 hotPage() 。其中,前兩個結點已經滿了,最後一個結點中保存了最新添加的 autoreleased 對象 objr3 的內存地址。

AutoreleasePoolPage1.jpg

此時,如果執行 pop(token1) 操作,那麼該 autoreleasepool 堆棧的內存結構將會變成如下圖所示:

AutoreleasePoolPage2.jpg

NSThread、NSRunLoop 和 NSAutoreleasePool

根據蘋果官方文檔中對 NSRunLoop 的描述,我們可以知道每一個線程,包括主線程,都會擁有一個專屬的 NSRunLoop 對象,並且會在有需要的時候自動創建。

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

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

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 中還提到,每一個線程都會維護自己的 autoreleasepool 堆棧。換句話說 autoreleasepool 是與線程緊密相關的,每一個 autoreleasepool 只對應一個線程。

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

弄清楚 NSThread、NSRunLoop 和 NSAutoreleasePool 三者之間的關係可以幫助我們從整體上了解 Objective-C 的內存管理機制,清楚系統在背後到底爲我們做了些什麼,理解整個運行機制等。

總結

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

  1. 如果你編寫的程序不是基於 UI 框架的,比如說命令行工具;

  2. 如果你編寫的循環中創建了大量的臨時對象;

  3. 如果你創建了一個輔助線程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章