內存管理一直是學習 Objective-C 的重點和難點之一,盡管現在已經是 ARC 時代了,但是了解 Objective-C 的內存管理機制仍然是十分必要的。其中,弄清楚 autorelease 的原理更是重中之重,只有理解了 autorelease 的原理,我們才算是真正了解了 Objective-C 的內存管理機制。注:本文使用的 runtime 源碼是當前的最新版本 objc4-646.tar.gz
。
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 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 輸出:
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 ,我們一起來分析下為什麼會得到這樣的結果。
分析: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 。
細心的讀者應該已經有所察覺,我們在上面已經提到了 -[NSAutoreleasePool release]
方法最終是通過調用 AutoreleasePoolPage::pop(void *)
函數來負責對 autoreleasepool 中的 autoreleased 對象執行 release 操作的。
那這裡的 AutoreleasePoolPage 是什麼東西呢?其實,autoreleasepool 是沒有單獨的內存結構的,它是通過以 AutoreleasePoolPage 為結點的雙向鏈表來實現的。我們打開 runtime 的源碼工程,在 NSObject.mm
文件的第 438-932 行可以找到 autoreleasepool 的實現源碼。通過閱讀源碼,我們可以知道:
一個空的 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 已滿。
我們使用 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 *)
三個過程。
上面提到的 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
函數在執行一個具體的插入操作時,分別對三種情況進行了不同的處理:
next
指向的位置;每調用一次 push 操作就會創建一個新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一個 POOL_SENTINEL ,並且返回插入的 POOL_SENTINEL 的內存地址。
通過 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;
}
同理,前面提到的 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
的內存地址。
此時,如果執行 pop(token1)
操作,那麼該 autoreleasepool 堆棧的內存結構將會變成如下圖所示:
根據蘋果官方文檔中對 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 !