iOS内存暴增问题追查与使用陷阱
iOS平臺的內存使用引用計數的機制,并且引入了半自動釋放機制;這種使用上的多樣性,導致開發者在內存使用上非常容易出現內存泄漏和內存莫名的增長情況; 本文會介紹iOS平臺的內存使用原則與使用陷阱; 深度剖析autorelease機制;低內存報警后的處理流程;并結合自身實例介紹內存暴增的問題追查記錄以及相關工具的使用情況;
TAG
內存暴增,內存泄漏,autorelease;內存報警;
1 iOS平臺內存管理介紹
iOS平臺的內存管理采用引用計數的機制;當創建一個對象時使用alloc或者allWithZone方法時,引用計數就會+1?;當釋放對象使用release方法時,引用計數就是-1?;這就意味著每一個對象都會跟蹤有多少其他對象引用它,一旦引用計數為0,該對象的內存就會被釋放掉;另外,iOS也提供了一種延時釋放的機制AutoRelease,以這種方式申請的內存,開發者無需手動釋放,系統會在某一時機釋放該內存; 由于iOS平臺的這種內存管理的多樣性,導致開發者在內存使用上很容易出現內存泄漏或者程序莫名崩潰的情況,本文會詳細介紹iOS平臺內存的使用規范與技巧以及如何利用工具避免或者發現問題;?
2 iOS平臺內存使用原則
2.1 對象的所有權與銷毀
2.1.1 誰創建,誰釋放;
如果是以alloc,new或者copy,mutableCopy創建的對象,則必須調用release或者autorelease方法釋放內存;
例如: ClassA* obj = [[ClassAalloc?] init];
……
[objrelease?];
obj = nil;? /* 防止野指針*/
2.1.2 誰retain,誰釋放;
如果對一個對象發送 retain消息,其引用計數會+1,則使用完必須發送release或者autorelease方法釋放內存或恢復引用計數;
例如:ClassA* obj = [[ClassAalloc?] init];/* 引用計數 1*/
ClassA* obj1 = obj;
[obj1retain?]; ?/*引用計數 2*/
……
[obj1release?]; /* 引用計數 1*/
obj1=nil;
[objrelease?]; /* 引用計數 0*/
obj=nil;
2.1.3 使用完,要釋放;
不論使用的是alloc(copy,new)創建的對象,還是通過retain增加了引用計數,在對象使用完后,都要調用release或者autorelease方法;釋放內存,確保沒有內存泄漏;
2.1.4 沒創建且沒retain,別釋放;
不要釋放那些不是自己alloc或者retain的對象,否則程序會crash?;
不要釋放autorelease的對象,否則程序會crash
例如:
ClassA* obj = [[[ClassAalloc?] init]autorelease?];/* 自動釋放*/
……
[objrelease?]; /* 程序crash */
obj=nil;
NSString* content = [NSString stringWithFormat: @”…”];
/* 系統返回的對象是autorelease?的 */
……
[contentrelease?]; /* 試圖釋放一個autorelease對象,程序會crash?*/
content=nil;
2.2 對象的深拷貝與淺拷貝
一般來說,復制一個對象包括創建一個新的實例,并以原始對象中的值初始化這個新的實例。復制非指針型實例變量的值很簡單,比如布爾,整數和浮點數。復制指 針型實例變量有兩種方法。一種方法稱為淺拷貝,即將原始對象的指針值復制到副本中。因此,原始對象和副本共享引用數據。另一種方法稱為深拷貝,即復制指針 所引用的數據,并將其賦給副本的實例變量。
2.2.1 深拷貝
實例變量的set方法的實現應該能夠反映出您需要使用的復制類型。如果相應的set方法復制了新的值,如下面的方法所示,那么您應該深拷貝這個實例變量:
| - (void)setMyVariable:(id)newValue |
| { |
| [myVariable autorelease]; |
| myVariable = [newValuecopy?]; |
| } |
2.2.2 淺拷貝
如果相應的set方法保留了新的值,如下面的方法所示,那么您應該淺拷貝這個實例變量:
| - (void)setMyVariable:(id)newValue |
| { |
| [myVariable autorelease]; |
| myVariable = [newValueretain?]; |
| } |
2.3 對象的存取方法
2.3.1 屬性聲明和實現
一般在程序的頭文件中,會設置成員變量的屬性,obj-C可以自動生成對成員變量的set?和get?函數;
聲明:
@property (copy) NSString *str; /* 復制對象,引用計數初始為1*/ @property (readonly) NSString *str1; /* 只讀對象,不可更改*/ @property (retain) NSString *str2; /* retain對象,引用計數+1*/ @property (assign) int num; /* 非指針變量,直接賦值 */實現:
@synthesize str; @synthesize str1; @synthesize str2; @synthesize num;2.3.2 存取方法的內部實現
屬性的聲明和實現,實際上是系統幫助開發者自動生成了對成員變量的set和get方法;當然開發者也可以顯性的定義set和get函數;下面介紹一下系統自動生成set和get方法的定義,以@property (retain?) NSString *str2; 舉例說明:
@interface ClassA : NSObject
-(NSString *) getStr2;
-(void) setStr2:(NSString *) value;
@end
@implementation ClassA
-(NSString *) getStr2{
return str2;
}
- setStr2:(NSString *) value {
if (str2 != value){? /* 如果參數與原來的值不同,則先釋放原來的值,然后在賦值并retain */
[str2release?];
str2 = [valueretain?];
}
}
注意: 在屬性中聲明為retain或者copy的成員變量,在delloc函數中,都要顯性的release;
-(void) dealloc{
[str2release?];
……
}
3 iOS平臺AutoRelease機制
3.1 自動釋放池的概念
自動釋放池是一個NSAutoreleasePool實例,其中“包含”已經收到autorelease消息的其他對象;當自動釋放池被回收時,它會向其中的每個對象發送一條release消息。一個對象可以被數次放入一個自動釋放池中,并且在每次被放入池中的時候都會收到一條release消息。因此,向對象發送autorelease消息(而不是release消息)可以至少將該對象的生命周期延長至自動釋放池本身被釋放的時候(如果在此期間對象被保留,則它可以存活更久)。
Cocoa總是期望有一個自動釋放池可用。如果自動釋放池不可用,那么自動釋放對象就無法得到釋放,您也就泄漏了內存。如果當自動釋放池不可用的時候,您發送了autorelease消息,那么Cocoa會記錄相應的錯誤信息。
您可以使用常見的alloc和init消息來創建一個NSAutoreleasePool對象,并使用drain?銷毀它。自動釋放池應該總是在與它被創建時所處的相同上下文環境(方法或函數的調用,或循環體)中被銷毀。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
……
??????? [pool drain];自動釋放池被置于一個堆棧中,雖然它們通常被稱為被“嵌套”的。當您創建一個新的自動釋放池時,它被添加到堆棧的頂部。當自動釋放池被回收時,它們從堆棧中被刪除。當一個對象收到送autorelease消息時,它被添加到當前線程的目前處于棧頂的自動釋放池中。
3.2 自動釋放池的作用域與嵌套
我們通常會提及自動釋放池是被嵌套的。但是,您也可以認為嵌套自動釋放池位于一個堆棧中,其中,“最內層”的自動釋放池位于棧頂。如前所述,嵌套自動釋放池實際上是這樣實現的:程序中的每個線程都維護一個自動釋放池的堆棧。當您創建一個自動釋放池時,它被壓入當前線程的堆棧的棧頂。?當一個對象被自動釋放時—也就是說,當一個對象收到一條autorelease消息或者當它作為一個參數被傳入addObject:類方法時—它總是被放入堆棧頂部的自動釋放池中。
因此,自動釋放池的作用域是由它在堆棧中的位置以及它的存在情況定義的?。自動釋放對象被添加至棧頂的自動釋放池中。如果另一個自動釋放 池被創建,則當前位于棧頂的池就超出其作用域,直到新的池被釋放為止(此時原來的自動釋放池再次成為棧頂的自動釋放池)。當自動釋放池本身被釋放的時候, 它(顯然)就永久地超出其作用域。
如果您釋放了一個不是位于堆棧頂部的自動釋放池,則這會導致堆棧中所有位于它上面的(未釋放的)自動釋放池,連同它們包含的所有對象一起被釋放。?當您用完自動釋放池時,如果您一時疏忽,忘記向它發送release消息(不推薦您這樣做),那么,當嵌套在它外層的自動釋放池中的某個被釋放時,它也會被釋放。
這種行為對于異常的處理很有意義。如果發生異常,并且線程突然轉移出當前的上下文環境,則與該上下文相關聯的自動釋放池將被釋放。但 是,如果被釋放的池不是線程堆棧頂部的池,則所有位于該自動釋放池之上的自動釋放池也會被釋放(并在這個過程中釋放其中所有的對象)。然后,先前位于被釋 放的池下面的自動釋放池則成為線程堆棧最頂端的自動釋放池。由于這種行為,異常處理程序則不需要釋放收到autorelease消息的對象。對于異常處理程序來說,沒有必要也不值得向它的自動釋放池發送release,除非異常處理程序重新引發該異常。
3.3 自動施放池的手動創建與自動創建
3.3.1 需要手動創建自動釋放池
- 如果你正在編寫一個不是基于Application Kit的程序,比如命令行工具,則沒有對自動釋放池的內置支持;你必須自己創建它們。
- 如果你生成了一個從屬線程,則一旦該線程開始執行,你必須立即創建你自己的自動釋放池;否則,你將會泄漏對象。
- 如果你編寫了一個循環,其中創建了許多臨時對象,你可以在循環內部創建一個自動釋放池,以便在下次迭代之前銷毀這些對象。這可以幫助減少應用程序的最大內存占用量。
3.3.2 系統自動創建自動釋放池
Application Kit會在一個事件周期(或事件循環迭代)的開端—比如鼠標按下事件—自動創建一個自動釋放池,并且在事件周期的結尾釋放它.
4 iOS平臺內存使用陷阱
4.1 重復釋放
在前文已經提到,不要釋放不是自己創建的對象;
釋放自己的autorelease對象,app會crash;
釋放系統的autorelease對象,app會crash;
4.2 循環引用
?
循環引用,容易產生野引用,內存無法回收,最終導致內存泄漏!可以通過弱引用的方式來打破循環引用鏈;
5 iOS平臺內存報警機制
由于iOS平臺的內存管理機制,不支持虛擬內存,所以在內存不足的情況,不會去Ram上創建虛擬內存;所以一旦出現內存不足的情況,iOS平臺會通知所有已經運行的app,不論是前臺app還是后臺掛起的app,都會收到 memory warning的notice;一旦app收到memory warning的notice,就應該回收占用內存較大的變量;
5.1 內存報警處理流程
1: app收到系統發過來的memory warning的notice;
2: app釋放占用較大的內存;
3: 系統回收此app所創建的autorelease的對象;
4: app返回到已經打開的頁面時,系統重新調用viewdidload方法,view重新加載頁面數據;重新顯示;
5.2 內存報警測試方法
在Simulate上可以模擬低內存報警消息;
iOS模擬器 -> 硬件 -> 模擬內存警告;
開發者可以在模擬器上來模擬手機上的低內存報警情況,可以避免由于低內存報警引出的app的莫名crash問題;
6 iOS平臺內存檢查工具
6.1 編譯和分析工具Analyze
iOS的分析工具可以發現編譯中的warning,內存泄漏隱患,甚至還可以檢查出logic上的問題;所以在自測階段一定要解決Analyze發現的問題,可以避免出現嚴重的bug;
內存泄漏隱患提示?:
Potential Leak of an object allocated on line ……
數據賦值隱患提示?:
The left operand of …… is a garbage value;
對象引用隱患提示?:
Reference-Counted object is used after it is released;
以上提示均比較嚴重,可能會引起嚴重問題,需要開發者密切關注!
6.2 內存檢測工具Leak
內存檢測工具可以通過iOS自帶Leak工具檢測 是否有內存泄漏;
一般通過Leak工具可以很快的檢查出程序哪里有內存泄漏,一般這種問題也比較容易解決,可是有時候即使解決了所有的內存泄漏,但還是發現程序在運行中,內存還是在不斷的瘋漲,這時候可能就要借助另外一個工具Allocations來檢查是那些地方使用的內存比較多而且是持續增長;下面詳細介紹一下這兩個工具的使用方法: Leak和Allocations;
Leak工具:?
?
通過Leak工具可以很快發現代碼中的內存泄漏,通過工具也可以很快找到發生內存泄漏的代碼段:?
?
Allocations工具:?
?
此工具會顯示出所有申請內存的地方,并統計申請的次數和大小; 從這個列表中可以找出內存申請次數最多且申請內存最大的語句;從而分析出哪些地方使用的內存最多,進而可以優化和改進;?
上圖是按照申請內存多少來排序的,可以方便的了解哪些代碼申請的內存多;
注意:?
1:iOS的SQLite 最好不要頻繁的打開和關閉數據庫,這樣SQLite在內部會增加內存Cache, 每次會增長51K的內存buffer,如果頻繁打開和關閉SQLite的話,內存很快就會漲到幾十兆,甚至上百兆!
2:Image的顯示,從網絡上下載的圖片或者頭像,Image內部API,每次會增加9K的cache;
7 參考資料
http://www.cocoachina.com/bbs/read.php?tid=15963
http://developer.apple.com/library/IOs/navigation/
by lixin
原文鏈接:?http://www.udpwork.com/redirect/6406
轉載于:https://www.cnblogs.com/zsw-1993/archive/2012/10/12/4880812.html
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的iOS内存暴增问题追查与使用陷阱的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 装系统时没盘符了怎么办 新标题:系统装不
- 下一篇: u不见了怎么办 U消失了该如何应对