或許這個題目起得有點太高調了,不過我只是想糾正一些童鞋對於autorelease的認識,如果能幫到幾個人,那這篇文章也就值得了!當然,高手請繞道
本文主要探討兩個方面:(1)autorelease對象到底是合適被析構的?(2)OC內部是如何處理一個被autorelease掉的對象的?
(1)autorelease對象到底是何時被析構的?
這個問題說難不難,但說簡單也不簡單。我們還是先看一類熟悉的不能再熟悉的代碼吧:
1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 NSArray *localArr = [NSArray arrayWithObject:@"Weng Zilin"];//這是一個局部對象,封裝了autorelease方法
4 }
請問,localArr這個局部變量何時被析構呢?很多人會回答:“出了作用域,也就是花括號之后就會被回收”。但遺憾的是,事實並非你想象的那般順利。下面我通過幾行代碼向你證明,localArr出了作用於依舊活得好好的:(ARC環境下)
__weak id objTrace; - (void)viewDidLoad { [super viewDidLoad]; NSArray *localArr = [NSArray arrayWithObject:@"Weng Zilin"];//這是一個局部對象,封裝了autorelease方法 } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; NSLog(@"viewWillAppear__localArr:%@", objTrace); } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"viewWillAppear__localArr:%@", objTrace); }
在ARC環境下我用一個__weak類型來追蹤localArr的釋放時機,__weak並不會對localArr增加引用計數,因此不干擾其釋放,log顯示如下:
我們發現,localArr在viewWillAppear還活着,在DidAppear已經掛了。這說明了一件事:autorelease並不是根據作用域來決定釋放時機的。那到底是依據什么呢?答案是:runloop。runloop不在本文討論范圍內,感興趣的同學請自行查閱資料,傳送門點這里。簡單說,runloop就是iOS中的消息循環機制,當一個runloop結束時系統才會一次性清理掉被autorelease處理過的對象,其實本質上說是在本次runloop迭代結束時清理掉被本次迭代期間被放到autorelease pool中的對象的。至於何時runloop結束並沒有固定的duration!
那么問題來了:iOS的這種基於runloop的內存回收策略有不方便的時候嗎?我認為是顯然有的。但凡事物總是有兩面性的,使用autorelease的確方便,但在一定的情況下會帶來性能問題。我們看個例,這個例子轉載在我之前的文章:
for (int i = 0; i <= 1000; i ++) { //1.首先我們獲取到需要處理的圖片資源的路徑 NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"PNG"]; //2.將圖片加載到內存中,我們使用了alloc關鍵字,在使用完后,可以手動快速釋放掉內存 UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath]; //3.這一步我們將圖片進行了壓縮,並得到一個autorelease類型實例 self.image2 = [image imageByScalingAndCroppingForSize:CGSizeMake(480, 320)]; //4.釋放掉2步驟的內存 [image release]; }
上述例子看起來沒有什么問題,因為一切都是按照MRC的規定做的,可以說是一種“看起來”十分規范的寫法。但是主要到image2這個對象了沒,賦值給image2對象的臨時image對象是一個autorelease類型。實際去跑這段程序會發現,在循環1000次的條件下內存持續上升,因為那個autorelease對象並沒有如我們預期般在每次for循環的花括號結束時釋放掉!如果從runloop的角度考慮就顯得合理了。
那么問題又來了:既然交給runloop處理不放心(runloop其實是有人類的“拖延症”的),那我們可以人工干預autorelease對象的釋放時機嗎?答案是,歡天喜地,可以的。上文有提到autorelease pool,這是下一個問題要解決的任務,在這里不展開,你只需要知道,一旦一個對象被autorelease,則該對象會被放到iOS的一個池:autorelease pool,其實這個pool本質上是一個stack,扔到pool中的對象等價於入棧。我們把需要及時釋放掉的代碼塊放入我們生成的autorelease pool中,結束后清空這個自定義的pool,主動地讓pool清空掉,從而達到及時釋放內存的目的。以上述圖片處理的例子為例,優化如下:
1 for (int i = 0; i <= 1000; i ++) { 2 3 //創建一個自動釋放池 4 NSAutoreleasePool *pool = [NSAutoreleasePool new];//也可以使用@autoreleasePool{domeSomething}的方式 5 NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"PNG"]; 6 UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath]; 7 UIImage *image2 = [image imageByScalingAndCroppingForSize:CGSizeMake(480, 320)]; 8 [image release]; 9 //將自動釋放池內存釋放,它會同時釋放掉上面代碼中產生的臨時變量image2 10 [pool drain]; 11 }
其中對pool的操作也可以等價地使用@autoreleasePool{domeSomeThing;}替代。以上就簡要地回答了本文開始處拋出的第一個問題,小結一下就是:釋放時機是基於runloop而不是作用域;通過autorelease pool手動干預釋放;循環多次時當心要對autorelease進行優化。下面我們開始第二個問題的討論
(2)一個對象被標記為autorelease后經歷了怎么樣的過程?
其實我認為這個問題討論起來更有意思,因為它已經比較底層了。前面提到autorelease對象最終被放到autorelease pool中,那這個pool到底是何方神聖呢?當我們使用@autoreleasepool{}時,編譯器實際上將其轉化為以下代碼:
void *context = objc_autoreleasePoolPush(); // {}中的代碼 objc_autoreleasePoolPop(context);//當前runloop迭代結束時進行pop操作
而objc_autoreleasePoolPush與objc_autoreleasePoolPop又是什么呢?他們只是對autoreleasePoolPage的一層簡單封裝,下面是autoreleasePoolPage的結構,它是C++數據類型,本質是一個雙向鏈表。next就是指向當前棧頂的下一個位置。
里面還有各種參數,不過記住這句話就行:向一個對象發送- autorelease
消息,就是將這個對象加入到當前AutoreleasePoolPage的棧頂next指針指向的位置。
在文章的最后順便提一下,在iOS中有三種常用的遍歷方法:for、forin、enumerateObjectsUsingBlcok。實際使用中大家可能沒有感覺到又什么區別,前面兩個比較常用,最后一個是iOS特有的遍歷方式,但事實上還是有區別的。block版本的遍歷方式已經內嵌了@autoreleasepool{}操作,而前面兩個沒有,這樣就意味着使用block版本的遍歷方式會使app更加健壯,內存使用效率更加出色,而且,逼格更高,嘿嘿!
這篇文章的討論就到這里,that`s all.
Reference:
http://blog.sunnyxx.com/
http://www.cnblogs.com/wengzilin/p/3301549.html
http://www.cnblogs.com/xwang/p/3547685.html
=======================================================
原創文章,轉載請注明 編程小翁@博客園,郵件zilin_weng@163.com,微信Jilon,歡迎各位與我在C/C++/Objective-C/機器視覺等領域展開交流!
=======================================================