iOS內(nèi)存管理

<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>


stack.gif

<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)系

demostration.png

為什么要這么存儲呢?因為棧遵循先進后出的原則,當(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時,意味著它可以被銷毀,過程如圖:

reference-counting.png

上文中采用較為書面化的“持有”一詞可能會讓讀者有些困惑,對象的持有在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消息,直到遇到第一個哨兵;過程如下圖:

pop_release.png

如圖所示,@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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內(nèi)容

  • ARC ARC是 Object-C 編譯特性, 不是運行時特性也不是垃圾回收機制, ARC 所做的只是在代碼編譯自...
  • 轉(zhuǎn)自iOS經(jīng)典面試題總結(jié)--內(nèi)存管理 - CocoaChina_讓移動開發(fā)更簡單 內(nèi)存管理 1.什么是ARC? A...
    赤洱閱讀 223評論 0 0
  • 1. 內(nèi)總管理原則(引用計數(shù)) IOS的對象都繼承于NSObject, 該對象有一個方法:retainCount...
    lilinjianshu閱讀 2,172評論 0 2
  • 內(nèi)存管理 簡述OC中內(nèi)存管理機制。與retain配對使用的方法是dealloc還是release,為什么?需要與a...
    丶逐漸閱讀 1,986評論 1 16
  • 1 選擇到這個博物館來工作,不知道算不算明智。 因為這里和我大學(xué)的專業(yè)完全無關(guān)。 我?guī)е唵蔚男欣睿驹趯m殿一般的...
    紫夢譚閱讀 239評論 1 0