標籤:
記憶體管理一直是學習 Objective-C 的重點和痛點之一,儘管現在已經是 ARC 時代了,但是瞭解 Objective-C 的記憶體管理機制仍然是十分必要的。其中,弄清楚 autorelease 的原理更是重中之重,只有理解了 autorelease 的原理,我們才算是真正瞭解了 Objective-C 的記憶體管理機制。註:本文使用的 runtime 源碼是當前的最新版本 objc4-646.tar.gz 。
autoreleased 對象什麼時候釋放
autorelease 本質上就是延遲調用 release ,那 autoreleased 對象究竟會在什麼時候釋放呢?為了弄清楚這個問題,我們先來做一個小實驗。這個小實驗分 3 種情境進行,請你先自行思考在每種情境下的 console 輸出,以加深理解。註:本實驗的源碼可以在這裡 AutoreleasePool 找到。
__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 輸出:
// 情境 12015-05-30 10:32:20.837 AutoreleasePool[33876:1448343] string: leichunfeng2015-05-30 10:32:20.838 AutoreleasePool[33876:1448343] string: leichunfeng2015-05-30 10:32:20.845 AutoreleasePool[33876:1448343] string: (null)// 情境 22015-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)// 情境 32015-05-30 10:33:07.075 AutoreleasePool[33984:1449418] string: leichunfeng2015-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 中看到類似的輸出,說明我們已經成功地設定了一個觀察點:
設定好觀察點後,點擊 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 的記憶體結構如所示:
magic 用來校正 AutoreleasePoolPage 的結構是否完整;
next 指向最新添加的 autoreleased 對象的下一個位置,初始化時指向 begin() ;
thread 指向當前線程;
parent 指向父結點,第一個結點的 parent 值為 nil ;
child 指向子結點,最後一個結點的 child 值為 nil ;
depth 代表深度,從 0 開始,往後遞增 1;
hiwat 代表 high water mark 。
另外,當 next == begin() 時,表示 AutoreleasePoolPage 為空白;當 next == end() 時,表示 AutoreleasePoolPage 已滿。
Autorelease Pool Blocks
我們使用 clang -rewrite-objc 命令將下面的 Objective-C 代碼重寫成 C++ 代碼:
@autoreleasepool {}
將會得到以下輸出結果(只保留了相關代碼):
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 {} 的實現代碼可以進一步簡化如下:
/* @autoreleasepool */ { void *atautoreleasepoolobj = objc_autoreleasePoolPush(); // 使用者代碼,所有接收到 autorelease 訊息的對象會被添加到這個 autoreleasepool 中 objc_autoreleasePoolPop(atautoreleasepoolobj);}
因此,單個 autoreleasepool 的運行過程可以簡單地理解為 objc_autoreleasePoolPush()、[對象 autorelease] 和 objc_autoreleasePoolPop(void *) 三個過程。
push 操作
上面提到的 objc_autoreleasePoolPush() 函數本質上就是調用的 AutoreleasePoolPage 的 push 函數。
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 操作的時候作為函數的入參。
static inline void *push(){ id *dest = autoreleaseFast(POOL_SENTINEL); assert(*dest == POOL_SENTINEL); return dest;}
push 函數通過調用 autoreleaseFast 函數來執行具體的插入操作。
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 函數在執行一個具體的插入操作時,分別對三種情況進行了不同的處理:
當前 page 存在且沒有滿時,直接將對象添加到當前 page 中,即 next 指向的位置;
當前 page 存在且已滿時,建立一個新的 page ,並將對象添加到新建立的 page 中;
當前 page 不存在時,即還沒有 page 時,建立第一個 page ,並將對象添加到新建立的 page 中。
每調用一次 push 操作就會建立一個新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一個 POOL_SENTINEL ,並且返回插入的 POOL_SENTINEL 的記憶體位址。
autorelease 操作
通過 NSObject.mm 源檔案,我們可以找到 -autorelease 方法的實現:
- (id)autorelease { return ((id)self)->rootAutorelease();}
通過查看 ((id)self)->rootAutorelease() 的方法調用,我們發現最終調用的就是 AutoreleasePoolPage 的 autorelease 函數。
__attribute__((noinline,used))idobjc_object::rootAutorelease2(){ assert(!isTaggedPointer()); return AutoreleasePoolPage::autorelease((id)this);}
AutoreleasePoolPage 的 autorelease 函數的實現對我們來說就比較容量理解了,它跟 push 操作的實現非常相似。只不過 push 操作插入的是一個 POOL_SENTINEL ,而 autorelease 操作插入的是一個具體的 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 *) 函數本質上也是調用的 AutoreleasePoolPage 的 pop 函數。
voidobjc_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 的記憶體位址。
此時,如果執行 pop(token1) 操作,那麼該 autoreleasepool 堆棧的記憶體結構將會變成如所示:
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 的:
如果你編寫的程式不是基於 使用者介面架構的,比如說命令列工具;
如果你編寫的迴圈中建立了大量的臨時對象;
如果你建立了一個輔助線程。
最後,希望本文能對你有所協助,have fun !
Objective-C Autorelease Pool 的實現原理