<h5>iOS內(nèi)存分區(qū)</h5>
<ul>
<li>棧區(qū),內(nèi)存管理由系統(tǒng)負責(zé),一個線程對應(yīng)一個棧區(qū),服從先進后出原則
</li>
<li>堆區(qū),內(nèi)存管理由程序員負責(zé),一個應(yīng)用程序?qū)?yīng)一個堆區(qū),作為全局資源在整個程序中共享</li>
<li>全局區(qū)/靜態(tài)區(qū),包括兩個部分,未初始化過、初始化過;也就是說,(全局區(qū)/靜態(tài)區(qū))在內(nèi)存中是放在一起的初始化的全局變量和靜態(tài)變量放在一塊區(qū)域,未初始化的全局、靜態(tài)變量放在另一塊區(qū)域;eg:int a,未初始化的;int a=10,初始化的</li>
<li>常量區(qū),用于存儲常量字符串</li>
<li>代碼區(qū),存放App二進制代碼</li>
</ul>
<h5>堆棧的簡化模型</h5>
<h5>堆與棧的比較</h5>
每個線程對應(yīng)一個棧區(qū),但是整個應(yīng)用對應(yīng)一個堆區(qū),
生命周期
棧,與線程綁定,線程創(chuàng)建時分配,線程結(jié)束時回收
堆,應(yīng)用啟動時分配,應(yīng)用退出時回收
所占內(nèi)存空間大小
棧的內(nèi)存大小在線程創(chuàng)建時確定;堆的內(nèi)存大小在應(yīng)用程序啟動時確定,但是可以根據(jù)需求增長
性能
棧的內(nèi)存塊通過棧頂指針移動(或數(shù)值改變)來進行分配和回收,同一塊內(nèi)存的重復(fù)使用率極高,故而棧的創(chuàng)建速度遠高于堆;另外,堆作為全局資源需要考慮多線程安全的問題,堆內(nèi)存的分配與銷毀需要與多個heap access同步
對象的創(chuàng)建
棧對象的創(chuàng)建,只要棧的剩余空間大于棧對象申請的空間 ,操作系統(tǒng)將為程序提供這段空間,否則報棧溢出異常;堆對象的創(chuàng)建,操作系統(tǒng)有一個用于記錄空閑內(nèi)存地址的鏈表,當(dāng)收到程序的申請時,會遍歷鏈表,尋找一個空間大于所申請的堆對象的節(jié)點,然后將此節(jié)點從空閑鏈表中刪除,并將空間分配給程序。
對象的存儲
int、float、struct等基本數(shù)據(jù)類型(值類型)存儲在棧中,依次緊密排列,占用一塊連續(xù)的內(nèi)存空間;繼承自NSObject的所有OC對象(引用類型)存儲在堆中,對象和對象之間留有不確定大小的空白,因此會產(chǎn)生很多內(nèi)存碎片
內(nèi)存管理
棧區(qū)是線程的暫存空間,函數(shù)調(diào)用時,一個內(nèi)存塊會被壓入棧頂,用于存儲局部變量和一些bookkeeping data(參數(shù)?);函數(shù)返回時,內(nèi)存塊被回收,等待下一次的函數(shù)調(diào)用;堆區(qū)用于動態(tài)內(nèi)存分配,不存在嚴格的內(nèi)存塊分配與回收機制,內(nèi)存的分配或者回收可以發(fā)生在任意時刻
故而我們常說棧的內(nèi)存分配由系統(tǒng)負責(zé),堆的內(nèi)存分配由程序猿來管理
為什么Objective-C對象要放在堆中?
有一些博客為了引導(dǎo)讀者更好地理解棧與堆的相互關(guān)系,會有如下寫法:棧存儲局部變量,堆用于存儲全局變量;這其實是一種錯誤的說法,所有的Objective-C對象都存儲在堆中,但是我們不能直接訪問這些變量,而是通過存儲在棧中的指針變量訪問它們
下圖可以幫助你很好的理解棧和堆的關(guān)系
為什么要這么存儲呢?因為棧遵循先進后出的原則,當(dāng)存入數(shù)據(jù)量過大時,存入棧會明顯的降低性能;因此將大量的數(shù)據(jù)放入堆中,然后在棧中存放堆的地址,當(dāng)須要調(diào)用數(shù)據(jù)時,可以快速地通過棧內(nèi)的地址找到堆中的數(shù)據(jù)
MRR&ARC
部分博客將MRR叫做MRC(Manual Reference Count),我在此處采用蘋果官方的開發(fā)者文檔的說法——MRR(Manual Retain Relase);其實兩種說法都可以體現(xiàn)內(nèi)存管理的實質(zhì)。
Objective-C采用引用計數(shù)(Reference Counting)管理對象的生命周期,當(dāng)對象被持有時,它的引用計數(shù)加1;當(dāng)持有者不再持有這個對象時,引用計數(shù)減1;對象的引用計數(shù)為0時,意味著它可以被銷毀,過程如圖:
上文中采用較為書面化的“持有”一詞可能會讓讀者有些困惑,對象的持有在MRR和ARC中有不同的表述方法,筆者認為MRR更有助于理解內(nèi)存管理的實質(zhì),所以在下文中重點介紹MRR。
NSObject Protocol中定義了一系列方法,來幫助程序猿手動管理Objective-C對象的引用計數(shù):
<p>??alloc??創(chuàng)建一個新對象,引用計數(shù)為1
</p>
<p>??retain??持有該對象,引用計數(shù)為加1
</p>
<p>??copy??復(fù)制當(dāng)前對象,新對象的引用計數(shù)為1
</p>
<p>??release??放棄持有,對象的引用計數(shù)減1
</p>
<p>??autorelease??放棄持有,但是延遲對象的銷毀
</p>
在前文可以看到,我們通過棧中指向?qū)ο蟮刂返闹羔樧兞縼碓L問存儲在堆中的Objective-C對象;而棧區(qū)的數(shù)據(jù)具有一定的生命周期,當(dāng)函數(shù)返回時,函數(shù)中定義的所有局部變量(local variable)和形參都會被銷毀;這意味著,如果我們沒在函數(shù)結(jié)束之前release這些指針變量指向的對象的話,存放在堆中的對象將永遠不會銷毀,而且我們不知道怎么訪問這些不會被銷毀的對象,因為存放其地址的指針已經(jīng)被銷毀掉了。
不再適用的對象未被釋放,即是內(nèi)存泄漏;小的內(nèi)存泄漏可能影響不大,但是隨著內(nèi)存占用的增加,你的程序最終會崩潰 。
所以在MRR時代,程序員們這樣寫代碼:
// main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObject:@"Jimmy C"];
NSLog(@"%@", array);
[array release];
}
return 0;
}
alloc方法在創(chuàng)建array對象的同時,使其引用計數(shù)為1;由于此段代碼中局部變量array是指向array對象的唯一指針,所以我們必須在函數(shù)返回之前將其釋放。
那么問題來了,實例變量應(yīng)該在什么時候釋放呢?以下代碼中,ViewController類定義了一個實例變量_array
// ViewController.m
#import "ViewController.h"
@implementation ViewController {
NSMutableArray *_array;
}
//省略代碼
@end
(類中的property本質(zhì)是對實例變量的封裝,在此我們不將其單獨拿出來討論)
類被創(chuàng)建以后,實質(zhì)上是一個OC對象,當(dāng)這個對象即將被銷毀時,runtime會調(diào)用NSObject中定義的dealloc方法。
如果ViewController對象被銷毀,則其實例變量_array也一并被銷毀,這樣_array指向的對象則永遠不可能被釋放;所以程序員們需要重寫dealloc方法 ,釋放成員變量:
// ViewController.m
- (void)dealloc {
[_array release];
[super dealloc];
}
了解了這些,我們似乎就可以用alloc、retain和release方法對實例變量和成員變量進行內(nèi)存管理了呢
......
當(dāng)然不可以
Autorelease對象什么時候釋放?
有些方法需要返回對象,故而不能使用release直接釋放對象,而是用autorelease延遲對象的釋放。
// CarStore.m
+ (CarStore *)carStore {
CarStore *newStore = [[CarStore alloc] init];
return [newStore autorelease];
}
autorelease對象什么時候釋放呢?它在最近的@autoreleasepool{}結(jié)束時釋放。
AutoreleasePool
AutoreleasePool是幫助管理內(nèi)存對象的好伙伴,它實質(zhì)上是一個由AutoreleasePoolPage對象連接而成的雙向鏈表,每個Page對象會開辟4096字節(jié)內(nèi)存,除了存儲對象的成員變量,剩余的空間全部用來存儲Autorelease對象的地址。
我們通過@autoreleasepool{}來使用AutoreleasePool,編譯器將會把它改寫成下面的樣子:
void *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);
objc_autoreleasePoolPush()的作用是向當(dāng)前的AutoreleasePoolPage對象add進一個哨兵對象(POOL_SENTINEL),值為nil;該哨兵對象傳入objc_autoreleasePoolPop方法時,會向AutoreleasePool中的每個autorelease對象發(fā)送release消息,直到遇到第一個哨兵;過程如下圖:
如圖所示,@autoreleasepool{}是可以嵌套使用的。
整個iOS應(yīng)用都是包含在一個自動釋放池中的,如果你打開項目的main.m文件,會看到如下代碼:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
main.m是整個應(yīng)用程序的入口,@autoreleasepool只包含了一行代碼,這行代碼將所有的事件、消息全部交給了UIApplication來處理。
當(dāng)然,僅僅使用一層autoreleasepool是遠遠達不到優(yōu)化內(nèi)存管理的目的的,事實上,系統(tǒng)在每個RunLoop迭代中都加入了自動釋放池push和pop。
AutoreleasePool與runloop
RunLoop是OS X/iOS中用于實現(xiàn)EventLoop的機制,一般來講,線程一次只能執(zhí)行一個任務(wù),執(zhí)行完成后線程就退出;而RunLoop提供的函數(shù)入口能夠使線程始終處于函數(shù)內(nèi)部的“接受消息-等待-處理”的循環(huán)當(dāng)中。
RunLoop被用于實現(xiàn)事件響應(yīng)、手勢識別、界面更新等功能;主線程的RunLoop在所有回調(diào)之前調(diào)用_objc_autoreleasePoolPush(),在所有回調(diào)之后調(diào)用_objc_autoreleasePoolPop()釋放自動池。
由此可見,AutoreleasePool通過多層嵌套的方式遍布整個應(yīng)用;故而在不特意寫@autoreleasepool{}的情況下,我們也會說Autorelease對象是在當(dāng)前的RunLoop迭代結(jié)束后釋放的。
RunLoop和autoreleasepool都與線程一一對應(yīng)。
RunLoop在AFNetworking中的實際應(yīng)用,為了能在后臺接受NSURLConnection的回調(diào),AFNetworking單獨創(chuàng)建了一個線程,并在這個線程啟動了一個RunLoop,看源碼可知RunLoop的啟動代碼被放在@autoreleasepool{}中:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
ARC(Automatic Reference Counting)
理解了MRR后,ARC就變得相當(dāng)簡單了;ARC所做的事情是在編譯階段在合適的地方插入retain、release和autorelease,其具體的使用也有很多有趣的地方,讀者可以前往唐巧的博客<a >理解iOS的內(nèi)存管理</a>一文閱讀學(xué)習(xí)。
相關(guān)異常
錯誤的內(nèi)存管理主要會造成以下兩種異常
<ul>
<li>內(nèi)存崩潰(memory corruption),釋放或覆蓋了尚在使用中的數(shù)據(jù)</li>
<li>內(nèi)存溢出(memory leaks),未釋放不再使用的數(shù)據(jù)</li>
</ul>
http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
http://draveness.me/autoreleasepool/