Objective-C Autorelease Pool 的實現原理(轉)

內存管理一直是學習 Objective-C 的重點和難點之一,儘管如今已是 ARC 時代了,可是瞭解 Objective-C 的內存管理機制仍然是十分必要的。其中,弄清楚 autorelease 的原理更是重中之重,只有理解了 autorelease 的原理,咱們纔算是真正瞭解了 Objective-C 的內存管理機制。:本文使用的 runtime 源碼是當前的最新版本 objc4-646.tar.gz 。html

autoreleased 對象何時釋放react

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

特別說明:在蘋果一些新的硬件設備上,本實驗的結果已經再也不成立,詳細狀況以下:git

  • iPad 2
  • iPad Air
  • iPad Air 2
  • iPad Pro
  • iPad Retina
  • iPhone 4s
  • iPhone 5
  • iPhone 5s
  • iPhone 6
  • iPhone 6 Plus
  • iPhone 6s
  • iPhone 6s Plus
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 28 29 30 31 32 33 34 
__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 輸出:github

1
2 3 4 5 6 7 8 9 10 11 12 13 14 
// 場景 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 ,咱們一塊兒來分析下爲何會獲得這樣的結果。objective-c

分析: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 中看到相似的輸出,說明咱們已經成功地設置了一個觀察點:

設置觀察點

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

設置觀察點

咱們先看 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 的內存結構以下圖所示:

AutoreleasePoolPage

  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 3 
@autoreleasepool {  } 

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

1
2 3 4 5 6 7 8 9 10 11 12 
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 9 10 
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

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

AutoreleasePoolPage2

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. 若是你建立了一個輔助線程。

最後,但願本文能對你有所幫助,have fun !

參考連接

http://blog.sunnyxx.com/2014/10/15/behind-autorelease/ http://clang.llvm.org/docs/AutomaticReferenceCounting.html http://www.yifeiyang.net/development-of-the-iphone-simply-3/

版權聲明:我已將本文在微信公衆平臺的發表權「獨家代理」給 iOS 開發(iOSDevTips)微信公衆號。掃下方二維碼便可關注「iOS 開發」:

iOS 開發二維碼

« Objective-C Category 的實現原理Objective-C Method Swizzling 的最佳實踐 »

Recent Posts

About Me

雷純鋒,iOS 開發者,GitHub 第三方客戶端 GitBucket 的做者,熱衷於分享,曾在南航移動團隊工做過兩年,目前就任於廣州一家小的創業公司。

新浪微博:雷純鋒2011 
微信公衆號:leichunfeng 
GitHub:leichunfeng

友情連接

Copyright © 2016 - 雷純鋒 - Powered by Octopress - 感謝 GitCafe 爲本站提供存儲空間

相關文章
相關標籤/搜索