
之前就總結過內存管理的內容,但并不系統(tǒng)、全面。所以,一直想找時間好好把這一塊內容規(guī)整一下,因為說起內存管理,這是一個很龐大而又與開發(fā)密不可分的內容,雖然ARC下我們幾乎不需要手動管理內存,但我們還是要了解一些內存管理的原理和內存優(yōu)化的方案。
一.iOS內存管理基本原理
iOS和其它系統(tǒng)一樣,內存分頁,每頁4K。多個頁構成一個region統(tǒng)一管理,負責管理的對象是VM object,其中包含了pager、size、resident pages等諸多屬性。
不管是Objective-C的[NSObject alloc],還是C代碼的對內存分配,最終重任都會落到malloc庫上,釋放也是如此,最終都將使用malloc庫中的free()。
當內存吃緊時,對于可以重新載入的只讀數(shù)據來說,直接清理掉,而對于可寫的數(shù)據,只能通過App自己去管理維護。內存緊張時,iOS會向App發(fā)起memory warning,不配合釋放足夠內存者,kill!
二. ARC(引用計數(shù))
1. 何為引用計數(shù)機制?
ARC 是 WWDC2011 和 iOS5 引入的變化。ARC 是 LLVM 3.0 編譯器的特性,用來自動管理內存。
與 Java 中 GC 不同,ARC 是編譯器特性,而不是基于運行時的,所以 ARC 其實是在編譯階段自動幫開發(fā)者插入了管理內存的代碼,而不是實時監(jiān)控與回收內存。
以經典的開燈例子舉例,在辦公室內至少有一人保持開燈狀態(tài),無人時保持關燈狀態(tài)
最早進入辦公室的人 ===> 開燈 ==== > 生成對象
之后進入辦公室的人 ===> 需要照明 ==== > 持有對象
下班離開辦公室的人 ===> 不需要照明 ==== > 釋放對象
最后下班離開辦公室的人===> 關燈 ==== > 廢棄對象
中間無論多少人,只要遵循進來就需要照明,走就不要照明。這樣辦公室的照明就可以得到很好的管理;同理使用引用計數(shù)功能,對象也能夠得到很好的管理。
原則:
1) 當對象被創(chuàng)建(通過alloc、new或copy等方法)時,其引用計數(shù)初始值為1。
2 ) 持有對象,當對象被強引用時,引發(fā)retain方法,引用計數(shù)加1。
3) 對象被釋放時,引發(fā)release操作,引用計數(shù)減1。
4) 當引用計數(shù)為0時,引發(fā)dealloc操作,對象被銷毀廢棄。
注意:不可對已經被銷毀的對象發(fā)送消息
2. ARC的修飾符
ARC的修飾符提供成員變量訪問方法、權限、環(huán)境、內存管理類型的聲明。
屬性的參數(shù)分為三類,基本數(shù)據類型默認為(atomic,readwrite,assign),對象類型默認為(atomic,readwrite,strong),其中第三個參數(shù)就是該屬性的內存管理方式修飾,修飾詞可以是以下之一:
- strong:強引用(引用計數(shù)+1),持有所指向對象的所有權,無修飾符情況下的默認值。如需強制釋放,可置nil。
- weak:弱引用,不持有所指向對象的所有權,引用指向的對象內存被回收之后,引用本身會置nil,避免野指針。
使用set方法賦值時,實質上不保留新值,也不釋放舊值,只設置新值。 - retain:release舊值,再retain新值(引用計數(shù)+1)
使用set方法賦值時,實質上是會先保留新值,再釋放舊值,再設置新值,避免新舊值一樣時導致對象被釋放的的問題。 - copy:release舊值,再copy新值(拷貝內容)
一般用來修飾String、Dict、Array等需要保護其封裝性的對象,尤其是在其內容可變的情況下,因此會拷貝(深拷貝)一份內容給屬性使用,避免可能造成的對源內容進行改動。
使用set方法賦值時,實質上是會先拷貝新值,再釋放舊值,再設置新值。
實際上,遵守NSCopying的對象都可以使用copy,當然,如果你確定是要共用同一份可變內容,你也可以使用strong或retain。 - assign:直接賦值,一般用來修飾基本數(shù)據類型
3. block的內存管理
iOS中使用block必須自己管理內存,錯誤的內存管理將導致循環(huán)引用等內存泄漏問題,這里主要說明在ARC下block聲明和使用的時候需要注意的兩點:
1) 如果你使用@property去聲明一個block的時候,一般使用copy來進行修飾(當然也可以不寫,編譯器自動進行copy操作),盡量不要使用retain。
@property (nonatomic, copy) void(^block)(NSData * data);
2) block會對內部使用的對象進行強引用,因此在使用的時候應該確定不會引起循環(huán)引用,當然保險的做法就是添加弱引用標記。
__weak typeof(self) weakSelf = self;
4. autorelease和autorelaesepool
官方文檔解釋:
每個線程(包括主線程),都維護了一個管理 NSAutoreleasePool 的棧。當創(chuàng)先新的 Pool 時,他們會被添加到棧頂。當 Pool 被銷毀時,他們會被從棧中移除。
autorelease 的對象會被添加到當前線程的棧頂?shù)?Pool 中。當 Pool 被銷毀,其中的對象也會被釋放。
當線程結束時,所有的 Pool 被銷毀釋放。
autorelease 顧名思義及時自動釋放的意思。它有點類似 C 語言中自動變量的特性。程序執(zhí)行時,若自動變量超出其作用域,該自動變量將被自動廢棄。
1. autorelease具體的使用方法:
- 生成并持有 NSAutoreleasePool 對象
- 調用已分配對象的 autorelease 實例方法
- 廢棄 NSAutoreleasePool 對象
使用場景:需要延遲釋放某些對象的情況時,可以把他們先放到對應的Autorelease Pool中,等Autorelease Pool生命周期結束時再一起釋放。
利用@autoreleasepool優(yōu)化循環(huán)的內存占用,可能是我們會用到的一點。如下面的循環(huán),次數(shù)非常多,而且循環(huán)體里面的對象都是臨時創(chuàng)建使用的,就可以用@autoreleasepool包起來,讓每次循環(huán)結束時,可以及時的釋放臨時對象的內存。
for (int i = 0; i < 10000; i++) {
@autoreleasepool {
// 創(chuàng)建對象 TODO:
NSObject *obj = [[NSObject alloc]init];
};
}
2. Autorelease對象什么時候釋放?
在沒有手加Autorelease Pool的情況下,Autorelease對象是在當前的runloop迭代結束時釋放的,而它能夠釋放的原因是系統(tǒng)在每個runloop迭代中都加入了自動釋放池Push和Pop
ARC 中我們使用@autoreleasepool{}來使用一個AutoreleasePool的是時候,編譯器就將其改寫成下面的樣子:
void *context = objc_autoreleasePoolPush();
// {}中的代碼,所有接收到 autorelease 消息的對象會被添加到這個 autoreleasepool 中
objc_autoreleasePoolPop(context);
而這兩個函數(shù)都是對AutoreleasePoolPage的簡單封裝,所以自動釋放機制的核心就在于這個類。AutoreleasePoolPage是一個C++實現(xiàn)的類。

* AutoreleasePool并沒有單獨的結構,而是由若干個``AutoreleasePoolPage``以雙向鏈表的形式組合而成(分別對應結構中的parent指針和child指針)
* AutoreleasePool是按線程一一對應的(結構中的thread指針指向當前線程)
* ``AutoreleasePoolPage``每個對象會開辟4096字節(jié)內存(也就是虛擬內存一頁的大小),除了上面的實例變量所占空間,剩下的空間全部用來儲存autorelease對象的地址
* 上面的id *next指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置
* 一個``AutoreleasePoolPage``的空間被占滿時,會新建一個``AutoreleasePoolPage``對象,連接鏈表,后來的``autorelease``對象在新的page加入
所以,若當前線程中只有一個AutoreleasePoolPage對象,并記錄了很多autorelease對象地址時內存如下圖:
![Upload 51530583gw1elj5gvphtqj20dy0cx756.jpg failed. Please try again.]
圖中的情況,這一頁再加入一個autorelease對象就要滿了(也就是next指針馬上指向棧頂),這時就要執(zhí)行上面說的操作,建立下一頁page對象,與這一頁鏈表連接完成后,新page的next指針被初始化在棧底(begin的位置),然后繼續(xù)向棧頂添加新對象。
所以,向一個對象發(fā)送-autorelease消息,就是將這個對象加入到當前AutoreleasePoolPage的棧頂next指針指向的位置
釋放
每當進行一次objc_autoreleasePoolPush調用時,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象,值為0(也就是個nil),那么這一個page就變成了下面的樣子:

objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop(哨兵對象)作為入參,于是:
- 根據傳入的哨兵對象地址找到哨兵對象所處的page
- 在當前page中,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次- release消息,并向回移動next指針到正確位置
- 補充2:從最新加入的對象一直向前清理,可以向前跨越若干個page,直到哨兵所在的page
剛才的objc_autoreleasePoolPop執(zhí)行后,最終變成了下面的樣子:

這篇,主要講了內存管理的原理和概念性的東西,下一篇會針對內存泄漏及內存優(yōu)化解決方案做詳細的介紹
親,喜歡的話點個贊唄!