總之我們要回答的問題是,當我們建立了一個UIViewCOntroller之後,Xocde給我們的template中,會叫我們實作一個叫didReceiveMemoryWarning:的method,然後你可能從一些相關文件上知道,當系統記憶體不夠的時候,我們應該要在這個method裡頭釋放一些記憶體,那麼有那些記憶體是應該要釋放的?我們應該怎麼實作這個method?
記憶體不足警告(Memory Warnings)
在Desktop作業系統中,如果實體記憶體不足,應用程式使用的記憶體量,超過實體記憶體的數是,這時候作業系統會自動將記憶體中的部分資料,存入磁碟的虛擬記體(Virtaul Memory)當中,需要使用的時候,再從虛擬記體中載回實體記憶體。
iOS在發展之初到現在,都沒有虛擬記憶體,而是會在記憶體快要用完的時候,對應用程式發出記憶體不足的警告,要求釋放一些可以暫時不需要用到的物件,讓應用程式可以有足夠的記憶體繼續運作。如果無視記憶體警告,繼續放任記憶體用量成長,系統最後便會強制要求終止應用程式。
在記憶體不足的時候,除了會對UIApplication的delegate(就是所謂的AppDelegate)呼叫applicationDidReceiveMemoryWarning:之外,也會對系統中所有UIViewController呼叫didReceiveMemoryWarning:如果我們想要知道那些記憶體是可以在didReceiveMemoryWarning:釋放的,不妨先回顧一下在iOS6之前,iOS是怎麼做的。
從iOS問世到iOS5,只要發生記體不足,就會把所有不在最前景的View Controller的view釋放掉。因為這些View Controller的view並不在畫面上,用戶根本看不到,所以暫時先放掉也沒關系。
iOS6之前記憶體不足時系統主動釋放View的行為
所謂不在最前景的view controller就是:假如我們今天有一個tab bar controller,tab bar裡頭有四個項目,對應到四個view controller,但是其實只會顯示一個,那麼在iOS6之前,只要發生記憶體警告的時,其他三個view controller的view就會被釋放。
在navigation controller的navigation stack裡頭,也只有最上面的view controller的畫面需要顯示,其他view controller的view也可以被釋放。
所以,如果你曾經在iOS6之前的環境上開發過iOS App,可能會遇到一些奇怪的bug,你把一些狀態直接記錄在view裡面,像是改變了一些label裡頭的文字,但是繼續做了一些操作,然後回到這個view之後,發現view莫名其妙的回復到初始值,原本放在label的文字不見了,其實就是遇到了記憶體警告的結果。
UIviewController負責管理在應用程式中每個會用到的畫面,最主要的property是view而這個property是使用Lazy Loading pattern實到。Lazy Loading就是:我們要去使用某個物件的時候,我們才去建立那個物件,避免在物件初始的時候就建立了所有的property,而達到讓初始物件這個動作加速的效過。
當我們在透過alloc、init、或initWithNibName:boundle:建立View Controller的時候,並不會馬上建立view,而是當我們呼叫view這個屬性的時候才會建立。我們以下面的程式為例:
// 建立 MyViewController的instance,這時候還沒建立view MyViewController *controller = [[MyViewController alloc] initWithNibName:NSStringFromClass([MyViewController class]) boundle:nil]; // 在被加入到navigation stack的時候會去呼叫[controller view] // 這時候view才被建立起來 [navigationController pushViewController:controller animated:YES]; [controller release];用Lazy Loading的方式實作一個getter的方式大致如下。在我們自已的程式中,想要有效使用記體,我們可以嘗試這麼寫。
- (UIView *)view {
if(!_view) {
_view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
}
return view;
}
不過,UIViewController在還沒有view,而要去建立view的時候,會呼叫的其實是loadView這個method, 在view成功載入之後,則會呼叫viewDidLoad。
我們雖然不知道蘋果到底是怎麼實作UIViewController,但不外乎類似這樣:
- (UIView *)view {
if(!_view) {
[self loadView];
if(_view) {
[self viewDidLoad];
}
}
return view;
}
所以,如果你有天不小心寫出像下面的程式碼,就會進入無窮迴圈:因為呼叫[self view] 的時候發現沒有view,就會呼叫loadview,但loadView又去呼叫[self view]。
- (void)loadView {
[self view];
}
在iOS6之前,如果某個View Controller不在最上層,發出記憶體警告時,系統就會通知這些View Controller把View指向nil;而當我們再次需要使用這個View Controller的時候,就會因為呼叫到view而把view重新載入回來。
所以我們要注意,viewDidLoad並不是UIViewController的Initalizer,雖然我們在開始使用某個view controller的時候,一定會呼叫到一次viewDidLoad,我們也通常會在這個地方,做一些初始化這個view controller的事情,但viewDidLoad是有機會在View Controller的life cycle中被重複呼叫好幾遍,在建立了view之後,view也可以再次指向nil,所以view controller可能會被重複釋放與載入view,viewDidLoad也會被重複呼叫。
所以在iOS6之前,你曾經遇到某個View Controller回復到初始值這樣的問題,就是:原本有狀態的view因為記憶體警告被釋放了,而我們如果在viewDidLoad再次被呼叫的時候,沒有正確還原狀態,自然只有初始狀態的view。
iOS如何知道那個View Controller位在最上層?
那麼,view controller 自已怎麼知道自已位在最前景呢?其實很簡單:view controller被放到最上層時,會被呼叫到viewWillAppear:以及viewDidAppear:,離開最上層時,會呼叫viewWillDisappear:與viewDidDisAppear:。
只有呼叫過viewWillAppear:以及viewDidAppear:,而沒有呼叫過viewWillDisappear:與viewDidDisappear:的View Controller就是位在最前景的View Controller。
我們經常會override viewWillAppear:這些method,在做override的時候,應該要呼叫一次super的實作,因為super的viewWIllAppear:這些method其實作了一些必要的事情,在iOS6之前是用來確保那些view該被釋放,雖然蘋果推出iOS6時,或許是認為像是iPhone5這樣的裝置在可用資源上遠遠超越過去的硬體,因此不再刻意釋放view,但呼叫一下super的實作還是比較保險。
所以我們應該要在didReceiveMemoryWarning:做什麼?
從過去的經驗來看,不在最上層的view其實是可以釋放,在iOS6之後,當我們遇到記憶體不足的時候,我們也可以選擇性的決定要不要釋放view,像web view這種記憶體怪物在沒用到的時候實在應該要放掉。
以下是蘋果的範程式:
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
if ([self.view window] == nil) {
self.view = nil;
}
}
沒有留言:
張貼留言