75. Autorelease機制及釋放時機
Autorelease機制是iOS開發人員管理對象內存的好夥伴。MRC中。調用[obj autorelease]來延遲內存的釋放是一件簡單自然的事;ARC下,我們甚至能夠全然不知道Autorelease 系統就能管理好內存。而在這背後,objc和編譯器都幫我們做了哪些事呢。一起來探究下Autorelease機制吧。
概述
當向一個對象發送一個autorelease消息時,Cocoa就會將該對象的一個引用放入到最新的自己主動釋放池。它仍然是個正當的對象,因此自己主動釋放池定義的作用域內的其他對象能夠向它發送消息。當自己主動釋放池釋放時,當中全部被管理對象都會收到”relrease”的消息, 從而池中的全部對象也就被釋放。註意,同一個對象能夠被多次調用”autorelease”方法,並能夠放到同一個”AutoreleasePool”中。
所以引入這個自己主動釋放池機制,對象的”autorelease”方法取代”relrease”方法能夠延長它的生命周期,直接到當前”AutorelreasePool”釋放。
iOS通過引用計數管理內存
OC 是通過”referring counting”(引用計數)的方式來管理內存的, 對象在開始分配內存(alloc)的時候引用計數為一,以後每當碰到有copy,retain的時候引用計數都會加一, 每當碰到release和autorelease的時候引用計數就會減一,假設此對象的計數變為了0, 就會被系統銷毀.
GC(Garbage Collection) 即垃圾回收, 須要註意的是iOS沒有垃圾回收機制的, 僅僅靠引用計數來進行管理內存, 這事實上也是 Autorelease 原理的核心. 大家不要混淆這樣的方法和垃圾回收機制. 只是非常多語言還是有自己的垃圾回收機制的, 推薦一篇文章關於垃圾回收(GC)的三種基本方式
NSAutoreleasePool
怎樣使用
NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
當運行[pool autorelease]的時候,系統會進行一次內存釋放,把autorelease的對象釋放掉,假設沒有NSAutoreleasePool , 那這些內存不會釋放
註意,對象並非自己主動被增加到當前pool中。而是須要對對象發送autorelease消息。這樣,對象就被加到當前pool的管理裏了。當當前pool接受到drain消息時。它就簡單的對它所管理的全部對象發送release消息。
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSString* nsstring;
char* cstring = "Hello CString";
nsstring = [NSString stringWithUTF8String:cstring];
[pool drain];
註意事項
1.NSAutoreleasePool的管理範圍是在NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];與[pool drain];之間的對象
2.在程序的入口main函數就調用NSAutoreleasePool,這樣保證程序中不調用NSAutoreleasePool。但在退出時自己主動釋放。新開線程最好實現NSAutoreleasePool
3.NSAutoreleasePool實際上是個對象引用計數自己主動處理器. NSAutoreleasePool能夠同一時候有多個,它的組織是個棧,總是存在一個棧頂pool,也就是當前pool,每創建一個pool。就往棧裏壓一個,改變當前pool為新建的pool。然後,每次給pool發送drain消息,就彈出棧頂的pool,改當前pool為棧裏的下一個 pool。
4.假設在Automatic Reference Counting(ARC) 不能直接使用autorelease pools,[email protected]{}, @autoreleasepool{} 比直接使用NSAutoreleasePool 效率高。但在 MRC 下兩者都是適用的.
5.在非 GC的引用計數環境下。drain和release一樣,可是在garbage-collected環境中,使用drain。
(”release”與”drain”的差別是”drain”在有GC的環境中會引起GC回收操作。”release”反之。但在非GC環境中,兩者同樣。官方的說法是為了程序的兼容性。應該考慮用”drain”取代”release”。
)
Autorelease原理
如今看一段 MRC 下關於單層 autoreleasePool使用的代碼:
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSArray * array = [[[NSArray alloc] init] autorelease];
[pool drain];
AutoreleasePoolPage
ARC下,[email protected][email protected]以下的樣子:
void *context = objc_autoreleasePoolPush();// {}中的代碼objc_autoreleasePoolPop(context);
而這兩個函數都是對AutoreleasePoolPage的簡單封裝。所以自己主動釋放機制的核心就在於這個類。
AutoreleasePoolPage是一個C++實現的類
AutoreleasePool並沒有單獨的結構。而是由若幹個AutoreleasePoolPage以雙向鏈表的形式組合而成(分別相應結構中的parent指針和child指針)。
AutoreleasePool是按線程一一相應的(結構中的thread指針指向當前線程)。
AutoreleasePoolPage每一個對象會開辟4096字節內存(也就是虛擬內存一頁的大小)。除了上面的實例變量所占空間,剩下的空間全部用來儲存autorelease對象的地址。
上面的id *next指針作為遊標指向棧頂最新add進來的autorelease對象的下一個位置。
一個AutoreleasePoolPage的空間被占滿時,會新建一個AutoreleasePoolPage對象,連接鏈表,後來的autorelease對象在新的page增加。
所以,若當前線程中僅僅有一個AutoreleasePoolPage對象,並記錄了非常多autorelease對象地址時。內存例如以下圖:
上圖中的情況。這一頁再增加一個autorelease對象就要滿了(也就是next指針立即指向棧頂),這時就要運行上面說的操作,建立下一頁page對象,與這一頁鏈表連接完畢後,新page的next指針被初始化在棧底(begin的位置),然後繼續向棧頂增加新對象。
所以。向一個對象發送- autorelease消息,就是將這個對象增加到當前AutoreleasePoolPage的棧頂next指針指向的位置。
釋放時刻
每當進行一次objc_autoreleasePoolPush調用時,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象。值為0(也就是個nil),那麽這一個page就變成了以下的樣子:
objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址。被objc_autoreleasePoolPop(哨兵對象)作為入參。於是:
1、依據傳入的哨兵對象地址找到哨兵對象所處的page。
2、在當前page中。將晚於哨兵對象插入的全部autorelease對象都發送一次- release消息,並向回移動next指針到正確位置。
3、補充2:從最新增加的對象一直向前清理,能夠向前跨越若幹個page,直到哨兵所在的page,剛才的objc_autoreleasePoolPop運行後。終於變成以下的樣子:
嵌套的AutoreleasePool
但因為你提到了生成的每一個實例可能會比較大。僅僅在循環外嵌套,可能導致在pool釋放前,內存裏已經有10000個實例存在,造成瞬間占用內存過大的情況。
因此,假設你的每一個實例僅須要在單次循環過程中用到,那麽能夠考慮能夠在循環內創建pool並釋放
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
for (int i = 0; i < 10000; i++)
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
// ...
[pool drain];
}
[pool drain];
知道了上面的原理,嵌套的AutoreleasePool就非常easy了,pop的時候總會釋放到上次push的位置為止,多層的pool就是多個哨兵對象而已,就像剝洋蔥一樣。每次一層,互不影響。
Autorelease 釋放時機
非常多人說, 當程序運行到作用域結束的位置時(當前作用域大括號結束時)。自己主動釋放池就會被釋放,這個說法是不正確的。正確的過程是怎樣呢?
iOS的運行時是由一個一個runloop組成的,每一個runloop都會運行下圖的一些步驟:
能夠看到,每一個runloop中都創建一個Autorelease Pool,並在runloop的末尾進行釋放,所以。普通情況下,每一個接受autorelease消息的對象。都會在下個runloop開始前被釋放。也就是說,在一段同步的代碼中運行過程中。生成的對象接受autorelease消息後,通常是不會在作用域結束前釋放的。
所以嚴謹的說, 在沒有手動增加Autorelease Pool的情況下。Autorelease對象是在當前的runloop叠代結束時釋放的,而它能夠釋放的原因是系統在每一個runloop叠代中都增加了自己主動釋放池Push和Pop。
小實驗
__weak id reference = nil;
- (void)viewDidLoad {
[super viewDidLoad]; NSString *str = [NSString stringWithFormat:@"sunnyxx"]; // str是一個autorelease對象,設置一個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)}
因為這個vc在loadView之後便add到了window層級上,所以viewDidLoad和viewWillAppear是在同一個runloop調用的,因此在viewWillAppear中,這個autorelease的變量依舊有值。
當然,我們也能夠手動幹預Autorelease對象的釋放時機:
- (void)viewDidLoad
{
[super viewDidLoad];
@autoreleasepool { NSString *str = [NSString stringWithFormat:@"sunnyxx"];
} NSLog(@"%@", str); // Console: (null)}
參考資料:Autorelease原理解析
75. Autorelease機制及釋放時機