ios內(nèi)存管理(一):內(nèi)存區(qū)域簡(jiǎn)述、對(duì)象的生成

??在程序開(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)引用內(nèi)存布局圖.png

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容