iOS平臺的內(nèi)存使用引用計數(shù)的機(jī)制,并且引入了半自動釋放機(jī)制;這種使用上的多樣性,導(dǎo)致開發(fā)者在內(nèi)存使用上非常容易出現(xiàn)內(nèi)存泄漏和內(nèi)存莫名的增長情況; 本文會介紹iOS平臺的內(nèi)存使用原則與使用陷阱; 深度剖析autorelease機(jī)制;低內(nèi)存報警后的處理流程;并結(jié)合自身實(shí)例介紹內(nèi)存暴增的問題追查記錄以及相關(guān)工具的使用情況;
TAG
內(nèi)存暴增,內(nèi)存泄漏,autorelease;內(nèi)存報警;
iOS平臺內(nèi)存常見問題
作為iOS平臺的開發(fā)者,是否曾經(jīng)為內(nèi)存問題而苦惱過?內(nèi)存莫名的持續(xù)增長,程序莫名的 crash,難以發(fā)現(xiàn)的內(nèi)存泄漏,這些都是iOS平臺內(nèi)存相關(guān)的常見問題;本文將會詳細(xì)介紹iOS平臺的內(nèi)存管理機(jī)制,autorelease機(jī)制和內(nèi)存的使用陷阱,這些將會解決iOS平臺內(nèi)存上的大部分問題,提高了程序的穩(wěn)定性;
1 iOS平臺內(nèi)存管理介紹
iOS平臺的內(nèi)存管理采用引用計數(shù)的機(jī)制;當(dāng)創(chuàng)建一個對象時使用alloc或者allWithZone方法時,引用計數(shù)就會+1;當(dāng)釋放對象使用release方法時,引用計數(shù)就是-1;這就意味著每一個對象都會跟蹤有多少其他對象引用它,一旦引用計數(shù)為0,該對象的內(nèi)存就會被釋放掉;另外,iOS也提供了一種延時釋放的機(jī)制 AutoRelease,以這種方式申請的內(nèi)存,開發(fā)者無需手動釋放,系統(tǒng)會在某一時機(jī)釋放該內(nèi)存; 由于iOS平臺的這種內(nèi)存管理的多樣性,導(dǎo)致開發(fā)者在內(nèi)存使用上很容易出現(xiàn)內(nèi)存泄漏或者程序莫名崩潰的情況,本文會詳細(xì)介紹iOS平臺內(nèi)存的使用規(guī)范與技巧以及如何利用工具避免或者發(fā)現(xiàn)問題;
下圖是內(nèi)存從申請到釋放的一個完整示例:
2 iOS平臺內(nèi)存使用原則
2.1 對象的所有權(quán)與銷毀
2.1.1 誰創(chuàng)建,誰釋放;
如果是以alloc,new或者copy,mutableCopy創(chuàng)建的對象,則必須調(diào)用release或者autorelease方法釋放內(nèi)存;
如果沒有釋放,則導(dǎo)致內(nèi)存泄漏!
2.1.2 誰retain,誰釋放;
如果對一個對象發(fā)送 retain消息,其引用計數(shù)會+1,則使用完必須發(fā)送release或者autorelease方法釋放內(nèi)存或恢復(fù)引用計數(shù);
如果沒有釋放,則導(dǎo)致內(nèi)存泄漏!
2.1.3 沒創(chuàng)建且沒retain,別釋放;
不要釋放那些不是自己alloc或者retain的對象,否則程序會crash;
不要釋放autorelease的對象,否則程序會crash;
2.2 對象的深拷貝與淺拷貝
一般來說,復(fù)制一個對象包括創(chuàng)建一個新的實(shí)例,并以原始對象中的值初始化這個新的實(shí)例。復(fù)制非指針型實(shí)例變量的值很簡單,比如布爾,整數(shù)和浮點(diǎn)數(shù)。復(fù)制指 針型實(shí)例變量有兩種方法。一種方法稱為淺拷貝,即將原始對象的指針值復(fù)制到副本中。因此,原始對象和副本共享引用數(shù)據(jù)。另一種方法稱為深拷貝,即復(fù)制指針 所引用的數(shù)據(jù),并將其賦給副本的實(shí)例變量。
2.2.1 深拷貝
深拷貝的流程是 先創(chuàng)建一個新的對象且引用計數(shù)為1,并用舊對象的值初始化這個新對象;
ClassA* objA = [[ClassA alloc] init];
ClassA* objB = [objA copy];
objB是一個新對象,引用計數(shù)為1,且objB的數(shù)據(jù)等同objA的數(shù)據(jù);
注意: objB需要釋放,否則會引起內(nèi)存泄漏!
2.2.2 淺拷貝
淺拷貝的流程是,無需引入新的對象,把原有對象的引用計數(shù)+1即可
ClassA* objA = [[ClassA alloc] init];
ClassA* objB = [objA retain];
注意: objB需要釋放,恢復(fù)objA的引用計數(shù),否則會引起內(nèi)存泄漏!
2.3對象的存取方法
2.3.1 屬性聲明和實(shí)現(xiàn)
變量聲明的常用屬性類型包括readonly,assign,retain和copy;且系統(tǒng)會自動為聲明了屬性的變量生成set和get函數(shù);
readonly屬性: 只能讀,不能寫;
assign屬性: 是默認(rèn)屬性,直接賦值,沒有任何保留與釋放問題;
retain屬性: 會增加原有對象的引用計數(shù)并且在賦值前會釋放原有對象,然后在進(jìn)行賦值;
copy屬性: 會復(fù)制原有對象,并在賦值前釋放原有對象,然后在進(jìn)行賦值;
2.3.2 使用屬性聲明可能帶來的隱患
當(dāng)一個非指針變量使用retain(或者copy)這個屬性時,盡量不要顯性的release這個變量;直接給這個變量置空即可;否則容易產(chǎn)生過度釋放,導(dǎo)致程序crash; 例如:
ClassA類的strName是NSString* 類型的變量且聲明的屬性為retain;
ClassA.strName = nil; /* 釋放原有對象且對此對象賦值為空 */
[ClassA.strName release]; /* strName內(nèi)存可能已經(jīng)被釋放過了,將導(dǎo)致程序crash */
Assign這個屬性一般是非指針變量(布爾類型,整形等)時用這個類型;屬于直接賦值型,不需要考慮內(nèi)存的保留與釋放;
如果一個指針類型的變量使用assign類型的屬性,有可能引用已經(jīng)釋放的變量;導(dǎo)致程序crash; 例如:
ClassB* obj =[[[ClassB alloc] init] autorelease];
……
ClassA.strName = obj; /* strName 指向obj的內(nèi)存地址*/
后續(xù)在使用ClassA.strName的時候, 因?yàn)閛bj是autorelease的,可能obj的內(nèi)存已經(jīng)被回收;導(dǎo)致引用無效內(nèi)存,程序crash;
3iOS平臺AutoRelease機(jī)制
3.1 自動釋放池的常見問題
大家在開發(fā)iOS程序的時候,是否遇到過在列表滑動的情況內(nèi)存莫名的增長,頻繁訪問圖片的時候內(nèi)存莫名的增長,頻繁的打開和關(guān)閉數(shù)據(jù)庫的時候內(nèi)存莫名的增長…… 這些都是拜iOS的autorelease機(jī)制所賜;具體分析如下:
1: 滑動列表的時候,內(nèi)存出現(xiàn)莫名的增長,原因可能有如下可能:
1:沒有使用UITableView的reuse機(jī)制; 導(dǎo)致每顯示一個cell都用autorelease的方式重新alloc一次; 導(dǎo)致cell的內(nèi)存不斷的增加;
2:每個cell會顯示一個單獨(dú)的UIView, 在UIView發(fā)生內(nèi)存泄漏,導(dǎo)致cell的內(nèi)存不斷增長;
2: 頻繁訪問圖片的時候,內(nèi)存莫名的增長;
頻繁的訪問網(wǎng)絡(luò)圖片,導(dǎo)致iOS內(nèi)部API,會不斷的分配autorelease方式的buffer來處理圖片的解碼與顯示; 利用圖片cache可以緩解一下此問題;
3: 頻繁打開和關(guān)閉SQLite,導(dǎo)致內(nèi)存不斷的增長;
在進(jìn)行SQLite頻繁打開和關(guān)閉操作,而且讀寫的數(shù)據(jù)buffer較大,那么 SQLite在每次打開與關(guān)閉的時候,都會利用autorelease的方式分配51K的內(nèi)存; 如果訪問次數(shù)很多,內(nèi)存馬上就會頂?shù)綆资祝踔辽习僬祝?所以針對頻繁的讀寫數(shù)據(jù)庫且數(shù)據(jù)buffer較大的情況, 可以設(shè)置SQLite的長連接方式;避免頻繁的打開和關(guān)閉數(shù)據(jù)庫;
3.2 自動釋放池的概念
NSAutoreleasePool內(nèi)部包含一個數(shù)組(NSMutableArray),用來保存聲名為autorelease的所有對象。如果一個對象聲明為autorelease,系統(tǒng)所做的工作就是把這個對象加入到這個數(shù)組中去。
ClassA *obj1 = [[[ClassA alloc] init] autorelease]; //retain count = 1,把此對象加入autorelease pool中
NSAutoreleasePool自身在銷毀的時候,會遍歷一遍這個數(shù)組,release數(shù)組中的每個成員。如果此時數(shù)組中成員的retain count為1,那么release之后,retain count為0,對象正式被銷毀。如果此時數(shù)組中成員的retain count大于1,那么release之后,retain count大于0,此對象依然沒有被銷毀,內(nèi)存泄露。
3.3 自動釋放池的作用域與嵌套
AutoreleasePool是可以嵌套使用的!
池是被嵌套的,嵌套的結(jié)果是個棧,同一線程只有當(dāng)前棧頂pool實(shí)例是可用的:
當(dāng)短生命周期內(nèi),比如一個循環(huán)中,會產(chǎn)生大量的臨時內(nèi)存,可以創(chuàng)建一個臨時的autorelease pool,這樣可以達(dá)到快速回收內(nèi)存的目的;
3.4 自動施放池的手動創(chuàng)建與自動創(chuàng)建
3.4.1 需要手動創(chuàng)建自動釋放池
●如果你正在編寫一個不是基于Application Kit的程序,比如命令行工具,則沒有對自動釋放池的內(nèi)置支持;你必須自己創(chuàng)建它們。
●如果你生成了一個從屬線程,則一旦該線程開始執(zhí)行,你必須立即創(chuàng)建你自己的自動釋放池;否則,你將會泄漏對象。
●如果你編寫了一個循環(huán),其中創(chuàng)建了許多臨時對象,你可以在循環(huán)內(nèi)部創(chuàng)建一個自動釋放池,以便在下次迭代之前銷毀這些對象。這可以幫助減少應(yīng)用程序的最大內(nèi)存占用量。
3.4.2 系統(tǒng)自動創(chuàng)建自動釋放池
Application Kit會在一個事件周期(或事件循環(huán)迭代)的開端—比如鼠標(biāo)按下事件—自動創(chuàng)建一個自動釋放池,并且在事件周期的結(jié)尾釋放它.
4 iOS平臺內(nèi)存使用陷阱
4.1 重復(fù)釋放
在前文已經(jīng)提到,不要釋放不是自己創(chuàng)建的對象;
釋放自己的autorelease對象,app會crash;
釋放系統(tǒng)的autorelease對象,app會crash;
4.2 循環(huán)引用
循環(huán)引用,容易產(chǎn)生野引用,內(nèi)存無法回收,最終導(dǎo)致內(nèi)存泄漏!可以通過弱引用的方式來打破循環(huán)引用鏈;所謂的弱引用就是不需要retain,直接賦值的方式,這樣的話,可以避免循環(huán)引用的問題,但是需要注意的是,避免重復(fù)釋放的問題;
5 iOS平臺內(nèi)存報警機(jī)制
由于iOS平臺的內(nèi)存管理機(jī)制,不支持虛擬內(nèi)存,所以在內(nèi)存不足的情況,不會去Ram上創(chuàng)建虛擬內(nèi)存;所以一旦出現(xiàn)內(nèi)存不足的情況,iOS平臺會通知所有已經(jīng)運(yùn)行的app,不論是前臺app還是后臺掛起的app,都會收到 memory warning的notice;一旦app收到memory warning的notice,就應(yīng)該回收占用內(nèi)存較大的變量;
5.1 內(nèi)存報警處理流程
1: app收到系統(tǒng)發(fā)過來的memory warning的notice;
2: app釋放占用較大的內(nèi)存;
3: 系統(tǒng)回收此app所創(chuàng)建的autorelease的對象;
4: app返回到已經(jīng)打開的頁面時,系統(tǒng)重新調(diào)用viewdidload方法,view重新加載頁面數(shù)據(jù);重新顯示;
5.2 內(nèi)存報警測試方法
在Simulate上可以模擬低內(nèi)存報警消息;
iOS模擬器 -> 硬件 -> 模擬內(nèi)存警告;
開發(fā)者可以在模擬器上來模擬手機(jī)上的低內(nèi)存報警情況,可以避免由于低內(nèi)存報警引出的app的莫名crash問題;
6 iOS平臺內(nèi)存檢查工具
6.1 編譯和分析工具Analyze
iOS的分析工具可以發(fā)現(xiàn)編譯中的warning,內(nèi)存泄漏隱患,甚至還可以檢查出logic上的問題;所以在自測階段一定要解決Analyze發(fā)現(xiàn)的問題,可以避免出現(xiàn)嚴(yán)重的bug;
內(nèi)存泄漏隱患提示:
Potential Leak of an object allocated on line ……
數(shù)據(jù)賦值隱患提示:
The left operand of …… is a garbage value;
對象引用隱患提示:
Reference-Counted object is used after it is released;
以上提示均比較嚴(yán)重,可能會引起嚴(yán)重問題,需要開發(fā)者密切關(guān)注!
6.2 內(nèi)存檢測工具
6.2.1 內(nèi)存泄漏檢測工具—Leak
Leak工具可以很容易的統(tǒng)計所有內(nèi)存泄漏的點(diǎn),而且還可以顯示在那個文件,哪行代碼有內(nèi)存泄漏,這樣定位問題比較容易,也比較方面;但是Leak在統(tǒng)計內(nèi)存泄漏的時候會把a(bǔ)utorelease方式的內(nèi)存也統(tǒng)計進(jìn)來; 所以我們在查找內(nèi)存泄漏情況的時候,可以autorelease的情況忽略掉;
Leak工具:
通過Leak工具可以很快發(fā)現(xiàn)代碼中的內(nèi)存泄漏,通過工具也可以很快找到發(fā)生內(nèi)存泄漏的代碼段:
6.2.2 內(nèi)存猛增檢測工具—Allocations
Allocations工具可以很容易的列出所有分配內(nèi)存的點(diǎn),這樣我們可以按照分配內(nèi)存大小來進(jìn)行排序, 這樣可以很容易的發(fā)現(xiàn)哪些點(diǎn)分配的內(nèi)存最多,而且是持續(xù)分配,這樣我們來針對性的分析這些持續(xù)分配較大內(nèi)存的地方;
此工具會顯示出所有申請內(nèi)存的地方,并統(tǒng)計申請的次數(shù)和大小; 從這個列表中可以找出內(nèi)存申請次數(shù)最多且申請內(nèi)存最大的語句;從而分析出哪些地方使用的內(nèi)存最多,進(jìn)而可以優(yōu)化和改進(jìn);
上圖是按照申請內(nèi)存多少來排序的,可以方便的了解哪些代碼申請的內(nèi)存多;
7 參考資料
http://www.cocoachina.com/bbs/read.php?tid=15963
http://developer.apple.com/library/IOs/navigation/
原文:http://stblog.baidu-tech.com/?p=1371