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

- 代碼段:編譯之后的代碼
- 數(shù)據(jù)段
字符串常量:如NSString *str = @"123";
已初始化數(shù)據(jù):已初始化的全局變量、靜態(tài)變量等
未初始化數(shù)據(jù):未初始化的全局變量、靜態(tài)變量等 - 棧:函數(shù)調(diào)用開銷,比如局部變量。分配的內(nèi)存空間地址越來越小
- 堆:通過
alloc、malloc、calloc等動(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.7與iOS 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ù)的修改,使用 CFRetain 和 CFRelease 方法,手工管理引用計(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

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