??在程序開(kāi)發(fā)中,內(nèi)存管理是極其重要的一部分。雖然ARC的引入極大的簡(jiǎn)化了objective-c開(kāi)發(fā)過(guò)程中的內(nèi)存管理工作,但ARC并不是萬(wàn)能的,仍然會(huì)存在循環(huán)引用、內(nèi)存泄露等問(wèn)題。因此,系統(tǒng)的理解內(nèi)存管理機(jī)制還是非常有必要的。
??本文先從最基礎(chǔ)內(nèi)存區(qū)域劃分入手,圍繞變量在內(nèi)存中的生命周期來(lái)探討ios開(kāi)發(fā)中的內(nèi)存管理問(wèn)題。
??首先來(lái)分析一個(gè)可執(zhí)行文件的加載命令。隨便找一個(gè)ipa包,解壓后找到可執(zhí)行文件,用“otool -lv 二進(jìn)制文件”來(lái)打印加載命令,可以看到segname后面標(biāo)識(shí)的各種段名。不同的段名可以理解為不同的內(nèi)存區(qū)域,一般有以下幾種:
__PAGEZERO: 操作系統(tǒng)預(yù)留的內(nèi)存,用來(lái)解決空指針問(wèn)題或捕捉將整數(shù)當(dāng)作指針引用的問(wèn)題
__TEXT:程序代碼段
__DATA:程序數(shù)據(jù)段
__LLVM:和bitcode機(jī)制相關(guān)的區(qū)域
__LINKEDIT:二進(jìn)制加載器dyld機(jī)制使用的字符串表、符號(hào)表等數(shù)據(jù)
以及:
棧區(qū)域:程序運(yùn)行中存放局部變量、函數(shù)實(shí)參等數(shù)據(jù),由系統(tǒng)維護(hù)
堆區(qū)域:程序運(yùn)行中調(diào)用alloc生成的對(duì)象,由編程人員維護(hù)
??對(duì)于以上區(qū)域,其實(shí)只有堆區(qū)域是由程序員來(lái)維護(hù)的,其余區(qū)域都由系統(tǒng)負(fù)責(zé)分配和回收。只需要注意不要造成棧溢出,如遞歸調(diào)用的邊界條件沒(méi)有寫對(duì)等,一般不會(huì)有其他的問(wèn)題。所以最需要留意的內(nèi)存管理問(wèn)題,主要是發(fā)生在堆區(qū)上的。一般常見(jiàn)的問(wèn)題有空指針、野指針、循環(huán)引用問(wèn)題。由于objective-c的語(yǔ)言特性對(duì)空指針問(wèn)題做了保護(hù),所以其實(shí)只需重點(diǎn)關(guān)注循環(huán)引用和野指針。
??在MacOS/iOS系統(tǒng)中,給應(yīng)用分配的棧和堆區(qū)域空間實(shí)際都是有限的,并非全部的可用內(nèi)存。在應(yīng)用使用的堆內(nèi)存達(dá)到報(bào)警閾值后,會(huì)通過(guò)didReceiveMemoryWarning消息發(fā)送給程序,如果不作處理而使應(yīng)用使用了超過(guò)操作系統(tǒng)分配的堆內(nèi)存,操作系統(tǒng)會(huì)直接殺掉該應(yīng)用。
??下面通過(guò)一段簡(jiǎn)單的代碼,來(lái)說(shuō)明在程序運(yùn)行過(guò)程中的內(nèi)存分配和使用:
// 先定義兩個(gè)類
@class ObjectB;
@interface ObjectA : NSObject
@property (strong, nonatomic, readwrite) ObjectB *b;
@end
@interface ObjectB : NSObject
@property (strong, nonatomic, readwrite) ObjectA *a;
@end
// 內(nèi)存使用示例代碼
- (void)memoryTest {
ObjectA *a = [[ObjectA alloc] init];
ObjectB *b = [[ObjectB alloc] init];
a.b = b;
b.a = a;
}
??分析上面代碼的執(zhí)行過(guò)程,首先,在程序開(kāi)始運(yùn)行后,上面的代碼會(huì)被加載程序加載到__TEXT代碼段中。當(dāng)memoryTest函數(shù)被調(diào)用時(shí),從代碼段找到這段函數(shù)體地址,壓入棧中,開(kāi)始執(zhí)行這段代碼。
??在結(jié)束的花括號(hào)打斷點(diǎn),觀察這段代碼運(yùn)行的中間狀態(tài)值,如下:
(lldb) po a
<ObjectA: 0x60800001e890>
(lldb) po &a
0x00007fff52288ba8
(lldb) po b
<ObjectB: 0x60800001e810>
(lldb) po &b
0x00007fff52288ba0
??函數(shù)體第一行代碼,先在堆0x60800001e890處分配一塊內(nèi)存區(qū)域(此次的內(nèi)存地址是虛擬地址,并非實(shí)際地址),大小由ObjectA的類聲明確定,然后調(diào)用ObjectA的init方法初始化這塊內(nèi)存區(qū)域,之后生成局部變量a,即在棧頂0x00007fff52288ba8處壓入一個(gè)指針,指向0x60800001e890。局部變量a默認(rèn)設(shè)置了__strong屬性,所以會(huì)持有0x60800001e890這塊內(nèi)存區(qū)域,這塊內(nèi)存區(qū)域的引用計(jì)數(shù)加1,從0變成1。注意,引用計(jì)數(shù)的對(duì)象是堆上的內(nèi)存區(qū)域,而不是棧上的局部變量。
??同理,執(zhí)行第二句后,在堆上開(kāi)辟了ObjectB對(duì)象區(qū)域,在棧上壓入局部變量,并且引用計(jì)數(shù)加1。
??執(zhí)行第三句,局部變量a指向的內(nèi)存區(qū)域中,有個(gè)ObjectB類型的指針,該指針的值設(shè)置為了0x60800001e810,在第3行前后設(shè)置斷點(diǎn),用memory read命令可以驗(yàn)證這個(gè)過(guò)程:
// 賦值前
(lldb) memory read 0x60800001e890
0x60800001e890: 88 ff 7b 0c 01 00 00 00 00 00 00 00 00 00 00 00 ..{.............
0x60800001e8a0: 28 03 56 10 01 00 00 00 80 9f 0f 00 00 60 00 00 (.V..........`..
// 賦值后
(lldb) memory read 0x60800001e890
0x60800001e890: 88 ff 7b 0c 01 00 00 00 10 e8 01 00 80 60 00 00 ..{..........`..
0x60800001e8a0: 28 03 56 10 01 00 00 00 80 9f 0f 00 00 60 00 00 (.V..........`..
??由于ObjectA的b屬性定義為strong類型,所以0x60800001e810堆區(qū)域的引用計(jì)數(shù)加1,變?yōu)?。同理,第四行執(zhí)行過(guò)后,堆區(qū)域0x60800001e890的引用計(jì)數(shù)為2。
??此時(shí),對(duì)象在內(nèi)存中的狀態(tài)如下圖:

??顯然,這段代碼造成了循環(huán)引用。循環(huán)引用到底是怎么回事?為什么會(huì)造成內(nèi)存泄露?具體的細(xì)節(jié)涉及到對(duì)象的銷毀過(guò)程,在下一篇再進(jìn)行詳細(xì)解釋。