先來看下蘋果文檔:
- Memory Management Programming Guide for Core Foundation
- Advanced Memory Management Programming Guide
- Memory Usage Performance Guidelines
Objective-C提供兩種方式的內存管理方式:
- 手動管理(“manual retain-release” or MRR)
- 自動引用計數方式(Automatic Reference Counting, or ARC)
內存管理一般出現的問題:
Freeing or overwriting data that is still in use
This causes memory corruption, and typically results in your application crashing, or worse, corrupted user data.Not freeing data that is no longer in use causes memory leaks
A memory leak is where allocated memory is not freed, even though it is never used again. Leaks cause your application to use ever-increasing amounts of memory, which in turn may result in poor system performance or your application being terminated
內存管理的基本法則:
- You own any object you create(自己生成的對象,自己持有)
- You can take ownership of an object using retain(非自己生成的對象,自己也能持有)
- When you no longer need it, you must relinquish ownership of an object you own(不再需要自己持有的對象時釋放)
- You must not relinquish ownership of an object you do not own(無法釋放非自己持有的對象)
去年參加了一個技術分享,講的是iOS內存管理及優化
引子就很吸引人:
- 桌面系統中很少有應用因為使用內存過多而被Kill掉,為啥iOS會呢?
- 虛擬內存為何物?為啥有時它能超過物理總內存?虛擬內存占用過高會引來內存警告嗎?
- Allocations中的Dirty Size和Resident Size分別指的是什么?All Heap & Anonymous VM是什么?
- iOS內存管理機制是什么樣的?它基于什么原則來Kill掉進程的?
- 內存有分類嗎?什么類型的內存可以回收?
- 我們了解自己的程序嗎?什么地方占用內存多,什么地方可以優化?如何避免內存峰值過高?
程序員對內存的關注點:
正確使用(1.非法訪問 2.內存泄露)
高效使用(1.降低內存峰值 2.處理內存警告 3.Cache)
-
內存管理的歷史
-
邏輯地址 VS 物理地址
程序訪問的都是邏輯地址,邏輯地址需要經過轉換之后才能訪問物理地址。CPU訪問先通過界限寄存器的對比,如果越界就報越界錯誤,否則加上基址寄存器的值,然后構成物理地址.
邏輯地址轉換物理地址.png- Swap
當物理內存不夠用時,可以將不用的進程放到磁盤去,騰出內存空間給新的進程.相當于通過通過輔存(磁盤)來擴充實際的物理內存.
Swap.png
- Swap
-
-
虛擬地址
虛擬地址相當于邏輯地址.虛擬地址到物理地址是通過CPU內部的內存管理單元(MMU)處理.32位系統,虛擬地址為4GB,64位系統為16GGB.
CPU處理示意圖
虛擬地址與物理內存或后備存儲的對應 -
段式虛擬內存
以前的內存分配空間為整個進程空間,現在可以將其分為小單位的段以提高利用率,以前的連續分配改為離散分配,以前的無權限分區改為按邏輯分配權限.
段式虛擬內存
段式虛擬內存的轉換過程分為兩部分:段號和段內偏移.系統有一個全局的段表.先通過段號去段表里查基值和界限,然后加載到基址寄存器和界限寄存器上.然后在經過轉換訪問物理地址.
- 頁式虛擬內存
段式虛擬內存分配的最小單位是段,但相對來講還是比較大(幾兆).段與段之間可能會產生外部碎片.頁式虛擬內存用來解決外部碎片,將虛擬地址和物理地址劃分成等大小的頁框(4KB或8KB,iOS中為4KB).通過等大小的頁框來做映射關系.可以理解為段式虛擬內存的特例,所有段都等大小。有個特點就是頁錯誤,當訪問物理地址中的一個沒有做映射的地址時會觸發一個中斷,操作系統會接管這個中斷,將這個頁在輔存中的內容讀取到物理頁,然后在建立映射關系,然后再恢復現場,程序無感知.
頁式虛擬內存
-
程序內存分布
程序的內存結構
可執行文件里面有個頭,里面記錄著所有段的大小,進程加載器會根據頭將各個段加載到物理內存去,比如代碼段和數據段,有些段在可執行文件里面只是個占位符,實際加載到虛擬內存上才會分配內存.數據段是初始過的全局變量和靜態變量.未初始化的全局變量和靜態變量放在bss段.堆從地地址到高地地址,一般用malloc分配.棧從高地址到低地址,用來存儲局部變量或者函數調用時候用到. -
iOS中的內存段
- _PAGEZERO 固定分配在零地址,一個頁大小.沒有訪問權限,用于零地址出發exception.
- _TEXT 代碼段
- _DATA 數據段
- __MALLOC_TINY 堆地址,和以下兩個只是大小區別,小于一個頁大小,分配到TINY段里面
- __MALLOC_SMALL 大于一個頁小于一兆
- __MALLOC_LARGE 大于一兆
關于iOS的內存分類可以閱讀 Finding iOS memory
-
iOS內存管理
iOS內存管理
iOS使用全功能的內存管理模式,有端式和頁式. 桌面系統中很少有應用因為使用內存過多而被Kill掉,為啥iOS會呢?
因為iOS上沒有Swap機制.(1.移動設配的閃存容量有限2.閃存的寫次數有限,頻繁寫會降低壽命)思考:代碼是要加載到內存執行的,如果沒有Swap機制那代碼很大的程序豈不是很占內存?
-
低內存處理機制Jetsam
- 基于優先隊列,從上往下優先級越高.
Screen Shot 2016-04-12 at 9.29.11 PM.png
當系統內存過低時就會廣播消息,大家盡量去釋放內存.過一段時間后,內存還不夠用時,就是從上往下Kill進程. - UIKit提供三種通知方式:
- [UIApplicationDelegate applicationDidReceiveMemoryWarning:]
- [UIViewController didReceiveMemoryWarning:]
- UIApplicationDidReceiveMemoryWarningNotification
- 內存警告消息來自主線程,應避免主線程這時候卡頓后者分配過大內存或者快速分配(騰訊Buddly可以檢測主線程卡頓)
- 如果App因為內存警告被Kill掉,會生成LowMemory***.log
- 基于優先隊列,從上往下優先級越高.
-
內存的分類
- Clean Memory 在閃存中有備份,能再次讀取重建
- Code,framework,memory-mapped files
- Dirty Memory 所有非Clear Memory,系統無法回收
- Heap allocations,decompressed images,caches
例子:
- Heap allocations,decompressed images,caches
- Clean Memory 在閃存中有備份,能再次讀取重建
NSString *str1 = [NSString stringWithString:@"Welcome!"]; //堆分配的內存 Dirty Memory
NSString *str2 = @"Welcome!"; //常量字符串,存放在一個只讀數據段里面,這段內存釋放后,還可以在讀取重建 Clear Memory
char *buf = malloc(100 * 1024 *1024); // Clear Memory分配100M虛擬內存,當沒有用時沒有建立映射關系
for (int i = 0; i < 3 * 1024 * 1024; ++i) {
buf[i] = rand();
}
關于Clear Memory和Dirty Memory的介紹 [What is resident and dirty memory of iOS?](http://stackoverflow.com/questions/13437365/what-is-resident-and-dirty-memory-of-ios)
* Dirty & Resident & Virtual Memory
* 虛擬內存層面
* Virtual Memory = Clear Memory + Dirty Memory
* 物理內存層面
* Resident Memory = Clean Memory(Loaded in Physical Memory) + Dirty Memory
* 物理頁面的生命周期
根據狀態來劃分
* Free(空閑) 物理頁沒被任何虛擬內存使用
* Active(活躍) 物理頁正用于一個虛擬內存頁,并且最近被引用過,這種頁面一般不會被交換出去
* Inactive(非活躍)物理頁正用于一個虛擬內存頁,但最近沒有被引用過,這種頁面有可能被交換出去
* Speculative(投機) 針對可能的內存需要做了一個猜測的分配,對頁面進行投機映射,因為很可能很快被訪問到
* Wired(聯動) 物理頁正用于一個虛擬內存頁,但不能被交換出去,一般用于內核代碼數據

* 內存的分析工具Allocations

All Heap & Anonymous VM中All Heap為堆上分配的對象,Anonymous VM比如創建UIView時CALayer底層所占空間.Diry Size為Dirty Memory所占內存,Resident Size為實際所占的物理內存.
* 內存最佳實踐
* Weak Strong Dance(解決block循環引用的技巧)
AFNetworking中的實踐:
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
}];
可以查看[對Weak String Dance的思考](http://www.lxweimin.com/p/4ec18161d790)
* Dealloc Block Executor(釋放內存的小技巧)

AssociateObject的父對象釋放的時候,子對象也會被釋放.通過block來釋放對象,不用使用dealloc.

* 降低內存峰值
* Lazy Allocation
MyBuffer *GetGlobalBuffer()
{
static MyBuffer *sMyBuffer = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sMyBuffer = [MyBuffer new];
});
return sMyBuffer;
}
直到使用的時候才分配,且為線程安全,可以用于分配對象或者資源文件讀取等,方便Patch,比如JSPatch.
* alloca VS malloc
棧內存分配alloca(size_t)
* 棧分配僅僅修改棧指針寄存器,比malloc遍歷并修改空閑列表要快得多
* 棧內存一般都已經在物理內存中,不用擔心頁錯誤
* 函數返回的時候棧分配的空間都會自動釋放
* 但僅適合小空間的分配,并且函數嵌套不宜過深
* calloc VS malloc + memset
calloc(size_t num,size_t size)分配內存時是虛擬內存,只有在訪問的時候才會發生物理頁的映射關系,malloc+memset就會產生Dirty Memory.
* 分配內存并初始化
* 立即分配虛擬空間并設置清0標記位,但不分配物理內存
* 只有相應的虛擬地址空間被讀寫操作的時候才需要分配相應的物理內存頁并初始化
* AutoreleasePool
* 基于引用計數,Pool執行drain方法會release所有該Pool中的autorelease對象
* 可以嵌套多個AutoReleasePool
* 每個線程并沒有設置默認的AutoReleasePool,需要手動創建,避免內存泄露
* 在一段內存分配頻繁的代碼中嵌套AutoReleasePool有利于降低整體內存峰值
* imageNamed VS imageWithContentOfFile
* imageNamed使用系統緩存,適用于頻繁使用的小圖片
* imageWithContentOfFile不帶緩存機制,適用于大圖片,使用完就釋放
* NSData with fileMapping
NSData & 內存映射文件,NSData有兩種讀取方式:
* [NSData dataWithContentsOfFile:path];
* [NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:&error];映射文件到虛擬內存,只有讀取操作的時候才會讀取相應頁的內容到物理內存頁中,常用語大文件中.
* NSCache & NSPurgableData
* NSCache
* 2種界限條件:totalCostLimt & countLimit 超過這兩種界限時都會去釋放一些舊的資源.
* 類NSMutableDictionary,setObject:forKey:cost
* evictsObjectWithDiscardContent & <NSDicardableContent>
* 最好監聽內存警告消息并移除所有Cache
* NSPurgableData
* 當系統處于低內存的時候自動移除
* 適用于大數據
* 內存警告的處理
* 盡可能釋放多資源,尤其圖片等占內存多的資源,等需要用的時候再重建
* 單例對象不要創建之后就一直持有數據,在內存緊張的時候釋放掉
* iOS6之后系統內存緊張會自動釋放CALayer的CABackingStore對象,需要使用的時候在調用drewRect來構建,所以沒必要將self.view = nil,但有時候對于隱藏的ViewController直接設置self.view = nil能簡化代碼邏輯
示例代碼:
@interface ViewController ()
@property (strong,readonly)NSString testData;
@end
@implementation ViewController
@synthesize testData=_testData;
// Override the default getter for testData
-(NSString)testData
{
if(nil==_testData)
_testData=[self createSomeData];
return _testData;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
_testData=nil;
}
* 業內趨勢
* 內存壓縮
* 地址空間布局隨機化ASLR