iOS開發那些事--性能優化–內存泄露問題的解決
內存泄漏問題的解決
內存泄漏(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:message delegate:self cancelButtonTitle:@”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:message delegate:self cancelButtonTitle:@”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:message delegate:self cancelButtonTitle:@”Ok” otherButtonTitles:nil]; [alert show]; [message release]; [tableView deselectRowAtIndexPath:indexPath animated:YES]; }
添加[message release]語句。很多人還會猜測alert對象(UIAlertView*)會有泄漏,因此重新運行Instruments工具,反複點擊單元格測 試,並未發現表示內存泄漏的紅線! Instruments工具認為alert對象不釋放不會引起內存泄漏,如果我們想進一步評估它對於內存的應用,這個時候我們可以看看 Allocations模板的折線圖表,每次點擊總占用內存數都有所增加,這說明alert對象沒有釋放雖然不是很嚴重,但是也會增加占用內存,因此 alert對象釋放也是必須的。
這就是我們介紹的內存泄漏問題解決方法,事實上內存泄漏是極其複雜問題,工具使用是一方麵,經驗是另一方麵。提高經驗,然後借助於工具才是解決內存泄漏的根本。
最後更新:2017-04-04 07:03:44