face it-iOS-內(nèi)存管理

內(nèi)存

內(nèi)存分為:代碼段、數(shù)據(jù)段、堆區(qū)、棧區(qū)、內(nèi)核區(qū)


m
  • 代碼段:編譯之后的代碼
  • 數(shù)據(jù)段
    字符串常量:如NSString *str = @"123";
    已初始化數(shù)據(jù):已初始化的全局變量靜態(tài)變量
    未初始化數(shù)據(jù):未初始化的全局變量、靜態(tài)變量
  • 棧:函數(shù)調(diào)用開銷,比如局部變量。分配的內(nèi)存空間地址越來越小
  • 堆:通過alloc、malloccalloc等動(dòng)態(tài)分配的空間,分配的內(nèi)存空間地址越來越大

內(nèi)存泄漏

指程序中間動(dòng)態(tài)分配了內(nèi)存,但在程序結(jié)束時(shí) 或使用完這塊內(nèi)存后,沒有釋放這部分內(nèi)存,從而造成那一部分內(nèi)存不可用的情況。
一般的,內(nèi)存泄漏是指 堆(heap)內(nèi)存的泄漏。
真正有危害的是內(nèi)存泄漏的積累,這會(huì)最終耗盡系統(tǒng)所有的內(nèi)存。

iOS內(nèi)存管理

內(nèi)存管理的目的:避免“過早釋放”和“內(nèi)存泄漏”。

基本思想就是 引用計(jì)數(shù)。通過它來控制內(nèi)存對象的生命周期。

1、引用計(jì)數(shù)(Reference Count)

一個(gè)簡單有效地管理對象生命周期的方式。在OC內(nèi)存管理中,每個(gè)對象都有自己的計(jì)數(shù)器,表示對象被引用的次數(shù)。

2、引用計(jì)數(shù)的工作原理

當(dāng)創(chuàng)建一個(gè)新對象時(shí),它的引用計(jì)數(shù)為 1;
當(dāng)有一個(gè)新的指針指向這個(gè)對象時(shí),我們將其引用計(jì)數(shù)加 1;
當(dāng)某個(gè)指針不再指向這個(gè)對象是,我們將其引用計(jì)數(shù)減 1;
當(dāng)對象的引用計(jì)數(shù)變?yōu)?0 時(shí),說明這個(gè)對象不再被任何指針指向了,這時(shí)就可以將對象銷毀,回收內(nèi)存。

當(dāng)創(chuàng)建(alloc)一個(gè)新對象A時(shí),它的引用計(jì)數(shù)從0變?yōu)?1;
當(dāng)有一個(gè)指針指向這個(gè)對象A,也就是某對象想通過引用保留(retain)該對象A時(shí),引用計(jì)數(shù)+1;
當(dāng)某個(gè)指針/對象不再指向這個(gè)對象A,也就是釋放(release)該引用后,我們將其引用計(jì)數(shù)-1;
  • 引用計(jì)數(shù)的存儲(chǔ)
    在64bit中,引用計(jì)數(shù)可以直接存儲(chǔ)在優(yōu)化過的isa指針中,也可能存儲(chǔ)在SideTable類中

內(nèi)存管理原則
創(chuàng)建的對象,當(dāng)不再需要時(shí),釋放掉。
需要使用的對象,保留。
如果沒必要釋放,不要釋放沒有擁有的對象。

3、ARC與MRC

iOS內(nèi)存管理主要有兩種機(jī)制:MRC、ARC。

  • MRC(手工引用計(jì)數(shù))
    對象的生成、銷毀、引用計(jì)數(shù)的變化都是由開發(fā)人員來完成

  • ARC(自動(dòng)引用計(jì)數(shù))
    2011年在MacOS X 10.7iOS 5中引入的新技術(shù),用于代替之前的MRC。ARC下幾乎把所有內(nèi)存管理事宜,都交給編譯器。(ARC下會(huì)自動(dòng)生成retain、release、autorelease)

ARC 的原理是依賴編譯器的靜態(tài)分析能力,通過在編譯時(shí)找出合理的插入引用計(jì)數(shù)管理代碼,從而徹底解放程序員。
ARC機(jī)制由 LLVM編譯器 + Runtime系統(tǒng),相互協(xié)作

ARC的作用:
降低內(nèi)存泄露等風(fēng)險(xiǎn) ; 開發(fā)者只負(fù)責(zé)對象的生成,不需要關(guān)心其銷毀。專注于業(yè)務(wù)邏輯。

4、ARC的局限

ARC 能夠解決 iOS 開發(fā)中 90% 的內(nèi)存管理問題,但另外 10% 內(nèi)存管理,是需要開發(fā)者自己處理的。就是:

  • 過度使用block之后,解決循環(huán)引用問題。
  • 與底層 Core Foundation對象交互的那部分。底層的Core Foundation 對象由于不在 ARC 的管理下,所以需要自己維護(hù)其引用計(jì)數(shù)。

循環(huán)引用問題

  • 例:
    對象 A 和對象 B,相互引用了對方作為自己的成員變量,只有當(dāng)自己銷毀時(shí),才會(huì)將成員變量的引用計(jì)數(shù)減 1。
    因?yàn)?A 的銷毀依賴于 B 銷毀,而 B 的銷毀又依賴于 A 的銷毀,這樣就造成了“循環(huán)引用”。這兩個(gè)對象即使在外界已經(jīng)沒有任何指針能夠訪問到它們了,它們也無法被釋放。
    另外,多個(gè)對象依次持有對方,形式一個(gè)環(huán)狀,也可以造成循環(huán)引用。

解決循環(huán)引用問題,主要有2個(gè)辦法

辦法1:主動(dòng)置nil

明確知道會(huì)存在循環(huán)引用,在合理的位置和時(shí)機(jī),主動(dòng)斷開環(huán)中的一個(gè)引用,使得對象得以回收。常見于各種與 block 相關(guān)的代碼邏輯中。如:一般在最后把block置nil。

辦法2:使用弱引用weak

弱引用雖然持有對象,但并不增加引用計(jì)數(shù),也就避免了循環(huán)引用。在 iOS 開發(fā)中,通常在 delegate 模式中使用。

原理:系統(tǒng)對于每一個(gè)有弱引用的對象,都維護(hù)一個(gè)表來記錄它所有的弱引用的指針地址。這樣,當(dāng)一個(gè)對象的引用計(jì)數(shù)為 0 時(shí),系統(tǒng)就通過這張表,找到所有的弱引用指針,繼而把它們都置成 nil。
可見,弱引用的使用是有額外的開銷的。雖然開銷很小,但如果一個(gè)地方肯定它不需要弱引用,就不應(yīng)該盲目使用弱引用。

Core Foundation 對象的內(nèi)存管理

創(chuàng)建對象

// 創(chuàng)建一個(gè) CFStringRef 對象
CFStringRef str= CFStringCreateWithCString(kCFAllocatorDefault, “hello", kCFStringEncodingUTF8);

// 創(chuàng)建一個(gè) CTFontRef 對象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);

對于這些對象的引用計(jì)數(shù)的修改,使用 CFRetainCFRelease 方法,手工管理引用計(jì)數(shù)。

此外,還有另外一個(gè)問題。在 ARC 下,有時(shí)需要將一個(gè)Core Foundation 對象轉(zhuǎn)換成Objective-C對象,這時(shí)需要告訴編譯器,轉(zhuǎn)換過程中的引用計(jì)數(shù)需要做如何的調(diào)整。這就引入了bridge相關(guān)的關(guān)鍵字。

  • __bridge: 只做類型轉(zhuǎn)換,不修改相關(guān)對象的引用計(jì)數(shù),
    原來的 Core Foundation 對象在不用時(shí),需調(diào)用 CFRelease方法。
  • __bridge_retained:類型轉(zhuǎn)換后,將相關(guān)對象的引用計(jì)數(shù)加 1,
    原來的Core Foundation對象在不用時(shí),需調(diào)用CFRelease方法。
  • __bridge_transfer:類型轉(zhuǎn)換后,將該對象的引用計(jì)數(shù)交給 ARC 管理,Core Foundation 對象在不用時(shí),不再需要調(diào)用 CFRelease方法。

根據(jù)具體的業(yè)務(wù)邏輯,合理使用上面的 3 種轉(zhuǎn)換關(guān)鍵字,就可以解決 Core Foundation對象與 Objective-C對象相對轉(zhuǎn)換的問題了。

定時(shí)器的內(nèi)存管理

定時(shí)器使用完畢時(shí)需要將其停止,并置nil。

自動(dòng)釋放池autorelease

執(zhí)行方法autorelease不立即釋放,而是注冊到(自動(dòng)釋放池)中,等到pool結(jié)束時(shí)釋放池,再自動(dòng)調(diào)用release進(jìn)行釋放工作。

自動(dòng)釋放池的主要底層數(shù)據(jù)結(jié)構(gòu)是:
__AtAutoreleasePool、AutoreleasePoolPage

調(diào)用了autorelease的對象,最終都是通過AutoreleasePoolPage對象來管理

  • AutoreleasePoolPage的結(jié)構(gòu)
    每個(gè)AutoreleasePoolPage對象占用4096字節(jié)內(nèi)存,除了存放它的成員變量,剩下的空間用來存放autorelease對象的地址。所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起
-

調(diào)用push方法會(huì)將一個(gè)POOL_BOUNDARY入棧,并且返回其存放的內(nèi)存地址;
調(diào)用pop方法時(shí)傳入一個(gè)POOL_BOUNDARY的內(nèi)存地址,會(huì)從最后一個(gè)入棧的對象開始發(fā)送release消息,直到遇到這個(gè)POOL_BOUNDARY;
id *next指向了下一個(gè)能存放autorelease對象地址的區(qū)域。

  • 關(guān)鍵字autoReleasePool
for (int i = 0; i < 100000; i++) {

    NSString *string = @"Abc";
    string = [string lowercaseString];
    string = [string stringByAppendingString:@"xyz"];
    NSLog(@"%@", string);
}

該for循環(huán)內(nèi)產(chǎn)生大量的臨時(shí)對象,直至循環(huán)結(jié)束才釋放,可能導(dǎo)致內(nèi)存泄漏。解決方法是在循環(huán)中創(chuàng)建自己的autoReleasePool,及時(shí)釋放占用內(nèi)存大的臨時(shí)變量,減少內(nèi)存占用峰值。

for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        NSString *string = @"Abc";
        ...
        NSLog(@"%@", string);
    }
}

ARC 下,當(dāng)使用alloc/new/copy/mutableCopy開始的方法 進(jìn)行初始化時(shí),系統(tǒng)會(huì)自動(dòng) 在合適位置release,不需要pool進(jìn)行管理。

主線程默認(rèn)為我們開啟 Runloop,Runloop 會(huì)自動(dòng)創(chuàng)建Autoreleasepool,并進(jìn)行Push、Pop 等操作來進(jìn)行內(nèi)存管理。

使用內(nèi)存管理工具

可以用Xcode工具儀器的幫助下,分析內(nèi)存的使用情況。它包括的工具有活動(dòng)監(jiān)視器,分配,泄漏,僵尸等。

菜單欄選擇:Product -> Profile


m

iOS 模擬器會(huì)運(yùn)行起來,模擬器里切換一些界面。稍等幾秒鐘,就可以看到 Instruments 檢測到了我們的這次循環(huán)引用。


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

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