記憶體流失問題的解決
記憶體流失(Memory Leaks)是當一個對象或變數在使用完成後沒有釋放掉,這個對象一直佔有著這塊記憶體,直到應用停止。如果這種對象過多記憶體就會耗盡,其它的應用就無法運行。這個問題在C++、C和Objective-C的MRR中是比較普遍的問題。
在Objective-C中釋放對象的記憶體是發送release和autorelease訊息,它們都是可以將引用計數減1,當為引用計數為0時候,release訊息會使對象立刻釋放,autorelease訊息會使對象放入記憶體釋放池中延遲釋放。
上代碼:
- (void)viewDidLoad{[super viewDidLoad];NSBundle *bundle = [NSBundle mainBundle];NSString *plistPath = [bundle pathForResource:@"team"ofType:@"plist"];//擷取屬性列表檔案中的全部資料self.listTeams = [[NSArray alloc] initWithContentsOfFile:plistPath];}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{static NSString *CellIdentifier = @”CellIdentifier”;UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];if (cell == nil) {cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];}NSUInteger row = [indexPath row];NSDictionary *rowDict = [self.listTeams objectAtIndex:row];cell.textLabel.text = [rowDict objectForKey:@"name"];NSString *imagePath = [rowDict objectForKey:@"image"];imagePath = [imagePath stringByAppendingString:@".png"];cell.imageView.image = [UIImage imageNamed:imagePath];cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;return cell;}- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{NSUInteger row = [indexPath row];NSDictionary *rowDict = [self.listTeams objectAtIndex:row];NSString *rowValue = [rowDict objectForKey:@"name"];NSString *message = [[NSString alloc] initWithFormat:@”您選擇了%@隊。”, rowValue];UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@”請選擇球隊”message:messagedelegate:selfcancelButtonTitle:@”Ok”otherButtonTitles:nil];[alert show];[tableView deselectRowAtIndexPath:indexPath animated:YES];}
大 家看看上面的3個方法會有什麼問題呢?如果代碼是基於ARC的是沒有問題的,遺憾的是基於MRR,上面的代碼都存在記憶體流失的可能性。理論上講內 存泄漏是對象或變數沒有釋放引起的,但實踐證明並非所有的未釋放對象或變數都會導致記憶體流失,這與硬體環境和作業系統環境有關,因此我們需要偵查工具協助 我們找到這些“泄漏點”。
在Xcode中提供了兩種工具協助尋找泄漏點:Analyze和Profile,Analyze是靜態分析工具 可以通過菜單 Product→Analyze啟動,為靜態分析之後的代碼畫面;Profile是動態分析工具,這個工具叫“Instruments”,它是Xcode 整合在一起,可以在Xcode中通過菜單Product→Profile啟動,Instruments有很多Trace Template(跟蹤模板)可以動態分析和跟蹤記憶體、CPU和檔案系統。
我們可以兩個工具結合使用尋找泄漏點,先使用Analyze靜態分析尋找可疑泄漏點,再用Profile動態分析中的Leaks和Allocations跟蹤模板進行動態跟蹤分析,確認這些點是否泄漏,或者是否有新的泄漏出現等。
其 中的線段表明了程式執行的路徑,在這個路徑中,1:說明在25行Objective-C對象引用計數是1,說明在這裡建立了一個 Objective-C對象;2:說明在27行引用計數為1這個,該對象沒有釋放,懷疑有泄漏。這樣的說明已經很明顯的告訴我們問題所在了, [[NSArray alloc] initWithContentsOfFile:plistPath]建立了一個對象,並賦值給 listTeams屬性所代表的成員變數,然而完成了賦值工作之後,建立的對象並沒有顯示地發送release和autorelease訊息。代碼修改
NSArray *array = [[NSArray alloc] initWithContentsOfFile:plistPath];self.listTeams = array;[array release];
我們看一下tableView:cellForRowAtIndexPath:方法中的疑似泄漏點行末尾的藍色表徵圖展開分析結果
其 中主要是說明UITableViewCell*類型的cell對象在64行有可能存在泄漏。在表視圖中 tableView:cellForRowAtIndexPath:方法是為表視圖儲存格執行個體化並設定資料的,因此cell對象執行個體化後不能馬上 release,應該使用autorelease延遲釋放。可以在建立cell對象的時候發送autorelease訊息,代碼修改如下:
if (cell == nil) {cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];}
我們看一下tableView:didSelectRowAtIndexPath:方法中的疑似泄漏點有兩個,行末尾的表徵圖展開分析結果。
message對象建立之後沒有釋放,我們只需要在[alert show]之後添加[message release]語句代碼就可以了。在Objective-C中執行個體化對象有兩種方式:
NSString *message = [[NSString alloc] initWithFormat:@”您選擇了%@隊。”, rowValue]; ①
NSString *message = [NSString stringWithFormat:@"您選擇了%@隊。", rowValue]; ②
① 行所示以init-開頭構造方法,它的是在alloc之後調用該方法我們稱為“執行個體構造方法”,該方法建立對象所有權是調用者,調用者需要對它的 生命週期負責,具體說負責建立和釋放。而另一種是②行所示string-(去掉NS後類名)開頭方法,它是通過類直接調用我們稱為“類級構造方法”,該方 法是建立的對象所有權非調用者所有,調用者不無權釋放它,否則就會因過渡釋放而“殭屍化”
UIAlertView*類型alert對象建立之後沒有釋放,我們只需要在[alert show]之後添加[alert release]語句代碼就可以了,修改之後的代碼
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{NSUInteger row = [indexPath row];NSDictionary *rowDict = [self.listTeams objectAtIndex:row];NSString *rowValue = [rowDict objectForKey:@"name"];NSString *message = [[NSString alloc] initWithFormat:@”您選擇了%@隊。”, rowValue];UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@”請選擇球隊”message:messagedelegate:selfcancelButtonTitle:@”Ok”otherButtonTitles:nil];[alert show];[alert release];[message release];[tableView deselectRowAtIndexPath:indexPath animated:YES];}
上 面介紹的使用Analyze靜態分析尋找可疑泄漏點,之所以稱為“可疑泄漏點”,但是這些點未必一定泄漏,確認這些點是否泄漏還要通過 Profile動態分析工具Instruments中的Leaks和Allocations跟蹤模板,Analyze靜態分析只是一個理論上的預測過程。 通過菜單Product→Profile啟動, Profile動態分析工具中選擇Leaks模板
Instruments 中雖然是選擇了Leaks模板,但預設情況也會添加Allocations模板,基本上凡是分析記憶體都會使用 Allocations模板,它可以監控記憶體分布情況,選中Allocations模板(圖中①地區),右邊③地區會顯示隨著時間的變化記憶體使用量折線圖 表,同時在④地區會顯示記憶體使用量的詳細資料,其中剛剛對象分配情況。點擊Leaks模板(圖中②地區),可以查看記憶體流失情況,如果在③地區有紅線出現, 則有記憶體流失,④地區會顯示泄漏的對象。
出 現的泄漏是在點擊表視圖中儲存格測試tableView:didSelectRowAtIndexPath:方法方法時候發生的,其中 NSCFString類型的對象發生了泄漏,NSCFString類型在NSFoundation中是NSString*類型。點擊泄漏對象前面的三角形 展開對象,可以看到它們的記憶體位址、佔用位元組、所屬架構和回應程式法資訊。
開啟擴充詳細視圖,可以看到右邊的跟蹤堆棧資訊,其中我們自己應用代碼,可以點擊進入我們程式碼,會開啟對應代碼。
代碼77並不是泄漏點,而是其中的NSString*類型對象在之後發生了泄漏,因此可以斷定是message對象之後沒有釋放導致泄漏。我們修改代碼如下:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{NSUInteger row = [indexPath row];NSDictionary *rowDict = [self.listTeams objectAtIndex:row];NSString *rowValue = [rowDict objectForKey:@"name"];NSString *message = [[NSString alloc] initWithFormat:@”您選擇了%@隊。”, rowValue];UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@”請選擇球隊”message:messagedelegate:selfcancelButtonTitle:@”Ok”otherButtonTitles:nil];[alert show]; [message release];[tableView deselectRowAtIndexPath:indexPath animated:YES];}
添 加[message release]語句。很多人還會猜測alert對象(UIAlertView*)會有泄漏,因此重新運行Instruments工具,反覆點擊儲存格測 試,並未發現表示記憶體流失的紅線! Instruments工具認為alert對象不釋放不會引起記憶體流失,如果我們想進一步評估它對於記憶體的應用,這個時候我們可以看看 Allocations模板的折線圖表,每次點擊總佔用記憶體數都有所增加,這說明alert對象沒有釋放雖然不是很嚴重,但是也會增加佔用記憶體,因此 alert對象釋放也是必須的。
這就是我們介紹的記憶體流失問題解決方案,事實上記憶體流失是極其複雜問題,工具使用是一方面,經驗是另一方面。提高經驗,然後藉助於工具才是解決記憶體流失的根本。