文章也同時(shí)在個(gè)人博客 http://kimihe.com/更新
引言
本文主要針對(duì)iOS內(nèi)存管理進(jìn)行總結(jié),相信看過(guò)之后一定會(huì)讓你有所收獲。對(duì)于內(nèi)存管理,網(wǎng)上有很多這方面的文章,但論質(zhì)量可謂良莠不齊。其中的很多總結(jié)已經(jīng)過(guò)時(shí),甚至有些內(nèi)容是似懂非懂的誤讀。因此,本文立足嚴(yán)謹(jǐn)?shù)闹螌W(xué)態(tài)度,希望能夠盡可能全面地讓大家理解內(nèi)存管理的要點(diǎn)。
本文涵蓋的知識(shí)點(diǎn)比較多,大家可以根據(jù)需要擇章節(jié)閱讀(部分章節(jié)待更),下面是目錄:
- 基礎(chǔ)知識(shí)
- 引用計(jì)數(shù)
- ARC
- delloc方法
- 循環(huán)引用
- 自動(dòng)釋放池@autoreleasepool
- 關(guān)于retainCount
基礎(chǔ)知識(shí)
在理解內(nèi)存管理之前,你需要理解內(nèi)存的到底如何被我們的操作系統(tǒng)使用的,需要有一個(gè)清晰的內(nèi)存模型。
對(duì)于內(nèi)存的使用模型,大家應(yīng)該不陌生堆棧這次詞。但具體的內(nèi)部原理其實(shí)還是比較復(fù)雜的。大家谷歌或者百度一下,可以搜到如下這張內(nèi)存堆棧圖:
這張圖的信息量還是比較大的,請(qǐng)大家耐心閱讀筆者的講解。
首先常說(shuō)的堆棧一詞包含了“堆”和“棧”兩個(gè)概念。它們是有區(qū)別的,而且和數(shù)據(jù)結(jié)構(gòu)中的堆棧是不同的。“堆”和“棧”分別代表了兩種數(shù)據(jù)管理方式,前者有程序員自己控制,后者由操作系統(tǒng)(由編譯器在編譯時(shí)確定套路)控制。這句話是網(wǎng)上很多文章常說(shuō)的,但可能并沒(méi)有說(shuō)明白其中的本質(zhì)。如果你想更加清楚地了解堆棧在內(nèi)存中的底層細(xì)節(jié),可以參考一下筆者的這篇文章《內(nèi)存中的堆和棧到底是什么》。
在明白了上述細(xì)節(jié)后,大家請(qǐng)看如下代碼:
NSString *someString = @"Some String";
NSString *anotherString = someString;
對(duì)于上述代碼,我們表明上看起來(lái)似乎創(chuàng)建了兩個(gè)NSString對(duì)象,其實(shí)它們二者是復(fù)用的關(guān)系。要理解這一點(diǎn),你可能需要明白什么是指針。可以看筆者的這篇文章《快速入門Linux下GDB和匯編開(kāi)發(fā)工具》中介紹指針原理的部分。
如果你清楚指針的含義,請(qǐng)繼續(xù)往后看。實(shí)際上,上述代碼只創(chuàng)建了一個(gè)NSString對(duì)象,它分配在堆中,需要進(jìn)行內(nèi)存管理。注意,我們并不是說(shuō)這個(gè)字符串常量在堆上,而是創(chuàng)建一個(gè)OC對(duì)象需要的內(nèi)存開(kāi)銷是在堆上,普通的字符串常量存在數(shù)據(jù)段的.rodata區(qū)。筆者強(qiáng)烈建議先閱讀一下介紹內(nèi)存堆棧的那篇文章。
而someString和anotherString是兩個(gè)指針(請(qǐng)注意它們C語(yǔ)言風(fēng)格的*
號(hào)),它們存在于棧中,跟隨棧的管理由系統(tǒng)控制,我們?nèi)斯o(wú)需也最好不要去控制。具體看來(lái),我們?cè)诋?dāng)前代碼區(qū)域的棧中分配了兩個(gè)內(nèi)存,但這兩個(gè)內(nèi)存只是用來(lái)存放指針,與堆中用來(lái)存儲(chǔ)字符串實(shí)例是完全不同的。這兩個(gè)內(nèi)存的大小剛好能夠分別存下一個(gè)指針(在32位CPU上是4字節(jié),在64位上是8字節(jié)),而且兩個(gè)內(nèi)存中的值是一樣的,即兩個(gè)指針的值是一樣的,于是兩個(gè)指針指向同一個(gè)堆內(nèi)存區(qū)域,從而導(dǎo)致someString和anotherString這兩個(gè)指針指向的是相同的字符串實(shí)例。如下圖:
總結(jié)一下:someString和anotherString分配在棧上,無(wú)需我們控制。NSString: @"Some String"分配在堆上,需要我們進(jìn)行內(nèi)存管理。而這里的內(nèi)存管理你需要理解引用計(jì)數(shù),請(qǐng)看下一小節(jié)。
除此之外,OC雖然是一門面向?qū)ο笳Z(yǔ)言,但本質(zhì)上它是一個(gè)C的超級(jí)。很多理念和C是通用的,如果你熟悉C,學(xué)習(xí)OC將很快。注意到上述指針的*
號(hào),OC中的對(duì)象都是以指針來(lái)引用的,因此對(duì)象類型的實(shí)例的名字前都會(huì)帶有一個(gè)*
號(hào)。
對(duì)于普通的非對(duì)象類型的變量,比如CGRect結(jié)構(gòu),它們也會(huì)使用棧空間,而非對(duì)象類型所用的堆空間。因此,相比于創(chuàng)建結(jié)構(gòu)體創(chuàng)建對(duì)象需要分配和釋放內(nèi)存等額外開(kāi)銷,所以若需要存儲(chǔ)int,float,double,char,bool等非對(duì)象類型,建議直接使用結(jié)構(gòu)體。
建議花一點(diǎn)時(shí)間學(xué)習(xí)一下C,這將有助于OC的學(xué)習(xí)。
其余章節(jié)待更~
自動(dòng)釋放池@autoreleasepool
OC對(duì)象的生命周期取決于引用計(jì)數(shù),我們有兩種方式可以釋放對(duì)象:一種是直接調(diào)用release釋放;另一種是調(diào)用autorelease將對(duì)象加入自動(dòng)釋放池中。自動(dòng)釋放池用于存放那些需要在稍后某個(gè)時(shí)刻釋放的對(duì)象。
自動(dòng)釋放池的創(chuàng)建
如果沒(méi)有自動(dòng)釋放池而給對(duì)象發(fā)送autorelease消息,將會(huì)收到控制臺(tái)報(bào)錯(cuò)。但一般我們無(wú)需擔(dān)心自動(dòng)釋放池的創(chuàng)建問(wèn)題。
我們的Mac以及iOS系統(tǒng)會(huì)自動(dòng)創(chuàng)建一些線程,例如主線程和GCD中的線程,都默認(rèn)擁有自動(dòng)釋放池。每次執(zhí)行 “事件循環(huán)”(event loop)時(shí),就會(huì)將其清空,這一點(diǎn)非常重要,請(qǐng)務(wù)必牢記! 關(guān)于事件循環(huán),其涉及到runloop,可以看這篇文章:深入理解RunLoop。
因此我們一般不需要手動(dòng)創(chuàng)建自動(dòng)釋放池,通常只有一個(gè)地方需要它,那就是在main()函數(shù)里,如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
這個(gè)main()函數(shù)里面的池并非必需。因?yàn)閴K的末尾是應(yīng)用程序的終止處,即便沒(méi)有這個(gè)自動(dòng)釋放池,也會(huì)由操作系統(tǒng)來(lái)釋放。但是這些由UIApplicationMain函數(shù)所自動(dòng)釋放的對(duì)象就沒(méi)有池可以容納了,系統(tǒng)會(huì)發(fā)出警告。因此,這里的池可以理解成最外圍捕捉全部自動(dòng)釋放對(duì)象所用的池。
@autoreleasepool的作用
大家可以先看一下下面的iOS筆試題的第5題(修改代碼的錯(cuò)誤),如下圖:
這段代碼問(wèn)題在哪里呢?題目的解答請(qǐng)繼續(xù)閱讀。筆者先給一個(gè)提示:與內(nèi)存的釋放有關(guān)。
現(xiàn)考慮如下代碼:
for (int i = 0; i < 10000; i++) {
[self doSthWith:object];
}
這段代碼和筆試題關(guān)鍵部分大同小異。如果"doSthWith:"方法要?jiǎng)?chuàng)建一個(gè)臨時(shí)對(duì)象,那么這個(gè)對(duì)象很可能會(huì)放在自動(dòng)釋放池里。筆試題中最后stringByAppendingString方法很有可能屬于上述的方法。因此如果涉及到了自動(dòng)釋放池,那么問(wèn)題也應(yīng)該就出在上面。
注意:即便臨時(shí)對(duì)象在調(diào)用完方法后就不再使用了,它們也依然處于存活狀態(tài),因?yàn)槟壳八鼈兌荚谧詣?dòng)釋放池里,等待系統(tǒng)稍后進(jìn)行回收。但自動(dòng)釋放池卻要等到該線程執(zhí)行下一次事件循環(huán)時(shí)才會(huì)清空,這就意味著在執(zhí)行for循環(huán)時(shí),會(huì)有持續(xù)不斷的新的臨時(shí)對(duì)象被創(chuàng)建出來(lái),并加入自動(dòng)釋放池。要等到結(jié)束for循環(huán)才會(huì)釋放。在for循環(huán)中內(nèi)存用量會(huì)持續(xù)上漲,而等到結(jié)束循環(huán)后,內(nèi)存用量又會(huì)突然下降。
而如果把循環(huán)內(nèi)的代碼包裹在“自動(dòng)釋放池”中,那么在循環(huán)中自動(dòng)釋放的對(duì)象就會(huì)放在這個(gè)池,而不是在線程的主池里面。如下:
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *str = @"abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
}
}
新增的自動(dòng)釋放池可以減少內(nèi)存用量,因?yàn)橄到y(tǒng)會(huì)在塊的末尾把這些對(duì)象回收掉。而上述這些臨時(shí)對(duì)象,正在回收之列。
自動(dòng)釋放池的機(jī)制就像“棧”。系統(tǒng)創(chuàng)建好池之后,將其壓入棧中,而清空自動(dòng)釋放池相當(dāng)于將池從棧中彈出。在對(duì)象上執(zhí)行自動(dòng)釋放操作,就等于將其放入位于棧頂?shù)哪莻€(gè)池。
實(shí)驗(yàn)驗(yàn)證
我們可以通過(guò)實(shí)驗(yàn)進(jìn)行驗(yàn)證。新建工程加入上述代碼,并關(guān)閉ARC(不然是看不到區(qū)別的)。
在未添加autoreleasepool時(shí),我們的堆內(nèi)存實(shí)時(shí)分配情況如下圖:
大家可以看到Persistent Bytes不斷增加,到達(dá)100W次的創(chuàng)建峰值后(出for循環(huán))開(kāi)始逐步釋放。因此圖像是一個(gè)向上凸的曲線。
而在加入autoreleasepool后,我們看到如下的曲線:
可以發(fā)現(xiàn)盡管字符串在不斷地創(chuàng)建,但由于得到了及時(shí)的釋放,堆內(nèi)存始終保持在一個(gè)很低的水平。
其他注意點(diǎn)
@autoreleasepool語(yǔ)法還有一個(gè)好處,就是可以避免無(wú)意間誤用那些在清空池之后已被系統(tǒng)回收的對(duì)象,例如:
@autoreleasepool {
id obj = [self createObject];
}
[self useObject:obj];
上述代碼在編譯時(shí)就會(huì)基于錯(cuò)誤警告,因?yàn)閛bj出了自動(dòng)釋放池就不可用了。
@autoreleasepool小結(jié)
- 自動(dòng)釋放池排布在棧中,對(duì)象受到autorelease消息后,系統(tǒng)將其放入棧頂?shù)某乩铩?/li>
- 合理運(yùn)用自動(dòng)釋放池,可以降低程序的內(nèi)存峰值。
關(guān)于retainCount
大家在搜索引擎中輸入“retainCount”,可以看到非常多的文章講述retainCount的作用。
大多數(shù)文章都在強(qiáng)調(diào)這個(gè)retainCount是用來(lái)標(biāo)記對(duì)象,進(jìn)行內(nèi)存管理的。所謂“reatin +1;release -1;到0釋放;打印retainCount查看當(dāng)前計(jì)數(shù)”等等。但其實(shí)retainCount并不應(yīng)該像上述那樣使用,OC的引用計(jì)數(shù)也不是那樣理解的。
誤區(qū)一
retainCount似乎可以返回某對(duì)象的保留計(jì)數(shù),其實(shí)即便在MRC下,retainCount在實(shí)際編程過(guò)程中也并沒(méi)有什么用。首要原因在于:它返回的保留計(jì)數(shù)只是某個(gè)時(shí)間點(diǎn)上的值。該方法并未考慮到系統(tǒng)會(huì)稍后把自動(dòng)釋放池清空,因而沒(méi)有將后續(xù)的釋放操作從返回值里減去。
如果你想根據(jù)這個(gè)數(shù)值來(lái)進(jìn)行某些釋放操作,那就很糟糕了。例如:
while ([object retainCount]) {
[object release];
}
其中存在兩個(gè)錯(cuò)誤,第一個(gè)就是沒(méi)有考慮到自動(dòng)釋放操作,只是不停地release,直到對(duì)象被系統(tǒng)回收。假如此對(duì)象在自動(dòng)釋放池里,那么稍后系統(tǒng)在清理空池子時(shí)還要仔把它釋放一次,這將導(dǎo)致程序崩潰。
第二個(gè)錯(cuò)誤就是:retainCount可能永遠(yuǎn)也不會(huì)減少到0。因?yàn)橛袝r(shí)候系統(tǒng)會(huì)優(yōu)化對(duì)象的釋放行為,在保留計(jì)數(shù)還是1的時(shí)候就把它回收了。只有在系統(tǒng)不打算這么優(yōu)化時(shí),計(jì)數(shù)值才會(huì)遞減址0。所以靠上述條件進(jìn)行判斷,完全是在賭運(yùn)氣。
我們真正應(yīng)該做的是確保release和retain操作的匹配(即可以相互抵消),我們?cè)谄谕到y(tǒng)回收某對(duì)象時(shí),應(yīng)該確保沒(méi)有尚未抵消的保留操作,也就是不要讓保留計(jì)數(shù)大于我們的期望值。如果發(fā)現(xiàn)內(nèi)存泄漏了,那么應(yīng)該檢查還有社仍然保留這個(gè)對(duì)象,并查明為何沒(méi)有釋放。
雖然Apple建議大家使用ARC,實(shí)際也確實(shí)如此。但基于MRC的開(kāi)發(fā)也并非一無(wú)是處,至少它能夠讓我們更加清晰地理解內(nèi)存管理的真諦。
誤區(qū)二
如果我們打印一些字符串或者數(shù)值對(duì)象的retainCount,如下代碼:
NSString *string = @"Apple";
NSLog(@"string retainCount: %lu", (unsigned long)[string retainCount]);
NSNumber *numberI = @1;
NSLog(@"numberI retainCount: %lu", (unsigned long)[numberI retainCount]);
NSNumber *numberF = @3.14f;
NSLog(@"numberF retainCount: %lu", (unsigned long)[numberF retainCount]);
其輸出將會(huì)如下:
2016-12-04 20:04:51.007 tmp[1522:138528] string retainCount: 18446744073709551615
2016-12-04 20:04:51.008 tmp[1522:138528] numberI retainCount: 9223372036854775807
2016-12-04 20:04:51.008 tmp[1522:138528] numberF retainCount: 1
我們發(fā)現(xiàn)結(jié)果非常奇怪。怎么回事呢?
第一個(gè)string的保留計(jì)數(shù)其實(shí)為2的64次方-1,而第二個(gè)numberI是2的63次方-1。二者皆為“單例對(duì)象”(singleton object),所以保留計(jì)數(shù)都很大。
系統(tǒng)會(huì)盡可能地把NSString實(shí)現(xiàn)成單例對(duì)象。像上述代碼中的字符串,其實(shí)是一個(gè)編譯期常量,編譯器會(huì)把NSString對(duì)象所表示的數(shù)據(jù)放到應(yīng)用程序的二進(jìn)制文件里,這樣的話,運(yùn)行程序時(shí)就能直接用了。這一點(diǎn)對(duì)應(yīng)到底層的話,隸屬于程序的可執(zhí)行文件中數(shù)據(jù)段的.rodata小段的作用,如果你閱讀過(guò)開(kāi)頭筆者提到的《內(nèi)存中的堆和棧到底是什么》這篇文章,應(yīng)該可以很容易理解。
NSNumber也類似,它使用了一種叫做“標(biāo)簽指針”(tagged pointer)的概念來(lái)標(biāo)注特定類型的數(shù)值。關(guān)于tagged pointer,可以閱讀這篇唐巧的文章:深入理解Tagged Pointer。
tagged pointer不使用NSNumber對(duì)象,而是把數(shù)值有關(guān)的全部消息都放在指針值里面。運(yùn)行期系統(tǒng)會(huì)在消息派發(fā)期間檢測(cè)到這種標(biāo)簽指針,并對(duì)它執(zhí)行相應(yīng)操作,使其行為看上去和真正的NSNumber對(duì)象一樣。這種優(yōu)化只在某些場(chǎng)合使用,比如上述代碼中的浮點(diǎn)數(shù)對(duì)象就沒(méi)有優(yōu)化,所以保留計(jì)數(shù)就是1。
上述的這種單例對(duì)象,其保留計(jì)數(shù)絕對(duì)不會(huì)變。這種對(duì)象的保留及釋放操作都是“空操作”(noop)。
retainCount小結(jié)
對(duì)象的保留計(jì)數(shù)看似有用,其實(shí)最好根本不要去使用。因?yàn)槿魏谓o定時(shí)間點(diǎn)上的“絕對(duì)保留計(jì)數(shù)”(absolute retain count)都無(wú)法反應(yīng)對(duì)象生命期的全貌。