一、常規檔案操作
- 常規檔案操作(read/write)有以下重要步驟:
-
- 行程發起讀檔案請求;
-
- 內核通過查找行程檔案符表,定位到內核已打開檔案集上的檔案資訊,從而找到此檔案的 inode;
-
- inode 在 address_space 上查找要請求的檔案頁是否已經快取在內核頁高速緩沖中,如果存在,則直接回傳這片檔案頁的內容;
-
- 如果不存在,則通過 inode 定位到檔案磁盤地址,將資料從磁盤復制到內核頁高速緩沖,之后再次發起讀頁面程序,進而將內核頁高速緩沖中的資料發給用戶行程,
- 常規檔案操作為了提高讀寫效率和保護磁盤,使用了頁快取機制,由于頁快取處在內核空間,不能被用戶行程直接尋址,所以需要將頁快取中資料頁再次拷貝到記憶體對應的用戶空間中;
- read/write 是系統呼叫很耗時,如下圖,它首先將檔案內容從硬碟拷貝到內核空間的一個緩沖區,然后再將這些資料拷貝到用戶空間,實際上完成了兩次資料拷貝;

- 如果兩個行程都對磁盤中的一個檔案內容進行訪問,那么這個內容在物理記憶體中有三份:行程 A 的地址空間 + 行程 B 的地址空間 + 內核頁高速緩沖空間;
- 寫操作也是一樣,待寫入的 buffer 在內核空間不能直接訪問,必須要先拷貝至內核空間對應的主存,再寫回磁盤中(延遲寫回),也是需要兩次資料拷貝,
二、mmap 記憶體映射
① mmap 簡介
- mmap 是一種記憶體映射檔案的方法,即將一個檔案或者其它物件映射到行程的地址空間,實作檔案磁盤地址和行程虛擬地址空間中一段虛擬地址的一一對映關系,實作這樣的映射關系后,行程就可以采用指標的方式讀寫操作這一段記憶體,而系統會自動回寫臟頁面到對應的檔案磁盤上,即完成了對檔案的操作而不必再呼叫 read、write 等系統呼叫函式,相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實作不同行程間的檔案共享,如下圖所示:

- 由上圖可以看出,行程的虛擬地址空間,由多個虛擬記憶體區域構成,虛擬記憶體區域是行程的虛擬地址空間中的一個同質區間,即具有同樣特性的連續地址范圍,上圖中所示的text資料段(代碼段)、初始資料段、BSS 資料段、堆、堆疊和記憶體映射,都是一個獨立的虛擬記憶體區域,而為記憶體映射服務的地址空間處在堆疊之間的空余部分,
- 在日常開發中偶爾會遇到 mmap,它最常用到的場景是 MMKV,其次用到的是日志列印,
- 行程是 App 運行的基本單位,行程之間相對獨立,iOS 系統中 App 運行的記憶體空間地址是虛擬空間地址,存盤資料是在各自的沙盒,當在 App 中去讀寫沙盒中的檔案時,會使用 NSFileManager 去查找檔案,然后可以使用 NSData 去加載二進制資料,檔案操作的更底層實作程序,是使用 linux 的 read()、write() 函式直接操作檔案句柄(也叫檔案描述符 fd),
- 在作業系統層面,當 App 讀取一個檔案時,實際是有兩步:
-
- 將檔案從磁盤讀取到物理記憶體;
-
- 從系統空間拷貝到用戶空間(可以認為是復制到系統給 App 統一分配的記憶體),
- iOS 系統使用頁快取機制,通過 MMU(Memory Management Unit)將虛擬記憶體地址和物理地址進行映射,并且由于行程的地址空間和系統的地址空間不一樣,所以還需要多一次拷貝,
- 而 mmap 將磁盤上檔案的地址資訊與行程用的虛擬邏輯地址進行映射,建立映射的程序與普通的記憶體讀取不同:正常的是將檔案拷貝到記憶體,mmap 只是建立映射而不會將檔案加載到記憶體中,

- 在記憶體映射的程序中,并沒有實際的資料拷貝,檔案沒有被載入記憶體,只是邏輯上被放入了記憶體,具體到代碼,就是建立并初始化了相關的資料結構(struct address_space),這個程序由系統呼叫 mmap() 實作,所以建立記憶體映射的效率很高,
- 既然建立記憶體映射沒有進行實際的資料拷貝,那么行程又怎么能最終直接通過記憶體操作訪問到硬碟上的檔案呢?那就要看記憶體映射之后的幾個相關的程序,
- mmap() 會回傳一個指標 ptr,它指向行程邏輯地址空間中的一個地址,這樣以后,行程無需再呼叫 read 或 write 對檔案進行讀寫,而只需要通過 ptr 就能夠操作檔案,但是 ptr 所指向的是一個邏輯地址,要操作其中的資料,必須通過 MMU 將邏輯地址轉換成物理地址,如上圖中程序 2 所示,這個程序與記憶體映射無關,
- 前面說道,建立記憶體映射并沒有實際拷貝資料,這時,MMU 在地址映射表中是無法找到與 ptr 相對應的物理地址的,也就是 MMU 失敗,將產生一個缺頁中斷,缺頁中斷的中斷回應函式會在 swap 中尋找相對應的頁面,如果找不到(也就是該檔案從來沒有被讀入記憶體的情況),則會通過 mmap() 建立的映射關系,從硬碟上將檔案讀取到物理記憶體中,如上圖中程序 3 所示,這個程序與記憶體映射無關,
- 如果在拷貝資料時,發現物理記憶體不夠用,則會通過虛擬記憶體機制(swap)將暫時不用的物理頁面交換到硬碟上,如圖1中程序4所示,這個程序也與記憶體映射無關,
- mmap 記憶體映射的實作程序,總的來說可以分為三個階段:
-
- 行程啟動映射程序,并在虛擬地址空間中為映射創建虛擬映射區域;
-
- 呼叫內核空間的系統呼叫函式 mmap(不同于用戶空間函式),實作檔案物理地址和行程虛擬地址的一一映射關系;
-
- 行程發起對這片映射空間的訪問,引發缺頁例外,實作檔案內容到物理記憶體(主存)的拷貝,
② 適用場景
- 有一個很大的檔案,因為映射有額外的性能消耗,所以適用于頻繁讀操作的場景;(單次使用的場景不建議使用),
- 有一個小檔案,它的內容您想要立即讀入記憶體并經常訪問,這種技術最適合那些大小不超過幾個虛擬記憶體頁的檔案(頁是地址空間的最小單位,虛擬頁和物理頁的大小是一樣的,通常為 4KB),
- 需要在記憶體中快取檔案的特定部分,檔案映射消除了快取資料的需要,這使得系統磁盤快取中的其他資料空間更大,
- 當隨機訪問一個非常大的檔案時,通常最好只映射檔案的一小部分,映射大檔案的問題是檔案會消耗活動記憶體,如果檔案足夠大,系統可能會被迫將其他部分的記憶體分頁以加載檔案,將多個檔案映射到記憶體中會使這個問題更加復雜,
③ 不適合的場景
- 希望從開始到結束的順序從頭到尾讀取一個檔案;
- 檔案有幾百兆位元組或者更大,將大檔案映射到記憶體中會快速地填充記憶體,并可能導致分頁,這將抵消首先映射檔案的好處,對于大型順序讀取操作,禁用磁盤快取并將檔案讀入一個小記憶體緩沖區;
- 該檔案大于可用的連續虛擬記憶體地址空間,對于 64 位應用程式來說,這不是什么問題,但是對于 32 位應用程式來說,這是一個問題,32 位虛擬記憶體最大是 4GB,可以只映射部分;
- 因為每次操作記憶體會同步到磁盤,所以不適用于移動磁盤或者網路磁盤上的檔案;
- 變長檔案不適用,
④ mmap 記憶體映射原理
- 行程啟動映射程序,并在虛擬地址空間中為映射創建虛擬映射區域:
-
- 行程在用戶空間呼叫庫函式 mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
-
- 在當前行程的虛擬地址空間中,尋找一段空閑的滿足要求的連續的虛擬地址;
-
- 為此虛擬區分配一個 vm_area_struct 結構,接著對這個結構的各個域進行了初始化;
-
- 將新建的虛擬區結構(vm_area_struct)插入行程的虛擬地址區域鏈表或樹中,
- 呼叫內核空間的系統呼叫函式 mmap(不同于用戶空間函式),實作檔案物理地址和行程虛擬地址的一一映射關系:
-
- 為映射分配了新的虛擬地址區域后,通過待映射的檔案指標,在檔案描述符表中找到對應的檔案描述符,通過檔案描述符,鏈接到內核“已打開檔案集”中該檔案的檔案結構體(struct file),每個檔案結構體維護著和這個已打開檔案相關各項資訊;
-
- 通過該檔案的檔案結構體,鏈接到 file_operations 模塊,呼叫內核函式 mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫函式;
-
- 內核 mmap 函式通過虛擬檔案系統 inode 模塊定位到檔案磁盤物理地址;
-
- 通過 remap_pfn_range 函式建立頁表,即實作了檔案地址和虛擬地址區域的映射關系,此時,這片虛擬地址并沒有任何資料關聯到主存中,
- 行程發起對這片映射空間的訪問,引發缺頁例外,實作檔案內容到物理記憶體(主存)的拷貝
-
- 行程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發現這一段地址并不在物理頁面上,因為目前只建立了地址映射,真正的硬碟資料還沒有拷貝到記憶體中,因此引發缺頁例外,
-
- 缺頁例外進行一系列判斷,確定無非法操作后,內核發起請求調頁程序,
-
- 調頁程序先在交換快取空間(swap cache)中尋找需要訪問的記憶體頁,如果沒有則呼叫 nopage 函式把所缺的頁從磁盤裝入到主存中,
-
- 之后行程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間后系統會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到檔案的程序,
- 前兩個階段僅在于創建虛擬區間并完成地址映射,但是并沒有將任何檔案資料的拷貝至主存,真正的檔案讀取是當行程發起讀或寫操作時,
- 修改過的臟頁面并不會立即更新回檔案中,而是有一段時間的延遲,可以呼叫 msync() 來強制同步, 這樣所寫的內容就能立即保存到檔案里,
⑤ mmap 相關函式
- 函式原型:
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
- 回傳說明:
-
- 成功執行時,mmap() 回傳被映射區的指標;
-
- 失敗時,mmap() 回傳 MAP_FAILED [其值為 (void *)-1], error 被設為以下的某個值:
1 EACCES:訪問出錯
2 EAGAIN:檔案已被鎖定,或者太多的記憶體已被鎖定
3 EBADF:fd不是有效的檔案描述詞
4 EINVAL:一個或者多個引數無效
5 ENFILE:已達到系統對打開檔案的限制
6 ENODEV:指定檔案所在的檔案系統不支持記憶體映射
7 ENOMEM:記憶體不足,或者行程已超出最大記憶體映射數量
8 EPERM:權能不足,操作不允許
9 ETXTBSY:已寫的方式打開檔案,同時指定MAP_DENYWRITE標志
10 SIGSEGV:試著向只讀區寫入
11 SIGBUS:試著訪問不屬于行程的記憶體區
- 引數
-
- start:映射區的開始地址;
-
- length:映射區的長度;
-
- prot:期望的記憶體保護標志,不能與檔案的打開模式沖突,是以下的某個值,可以通過 or 運算合理地組合在一起;
1 PROT_EXEC :頁內容可以被執行
2 PROT_READ :頁內容可以被讀取
3 PROT_WRITE :頁可以被寫入
4 PROT_NONE :頁不可訪問
-
- flags:指定映射物件的型別,映射選項和映射頁是否可以共享,它的值可以是一個或者多個以下位的組合體;
1 MAP_FIXED //使用指定的映射起始地址,如果由start和len引數指定的記憶體區重疊于現存的映射空間,重疊部分將會被丟棄,如果指定的起始地址不可用,操作將會失敗,并且起始地址必須落在頁的邊界上,
2 MAP_SHARED //與其它所有映射這個物件的行程共享映射空間,對共享區的寫入,相當于輸出到檔案,直到msync()或者munmap()被呼叫,檔案實際上不會被更新,
3 MAP_PRIVATE //建立一個寫入時拷貝的私有映射,記憶體區域的寫入不會影響到原檔案,這個標志和以上標志是互斥的,只能使用其中一個,
4 MAP_DENYWRITE //這個標志被忽略,
5 MAP_EXECUTABLE //同上
6 MAP_NORESERVE //不要為這個映射保留交換空間,當交換空間被保留,對映射區修改的可能會得到保證,當交換空間不被保留,同時記憶體不足,對映射區的修改會引起段違例信號,
7 MAP_LOCKED //鎖定映射區的頁面,從而防止頁面被交換出記憶體,
8 MAP_GROWSDOWN //用于堆疊,告訴內核VM系統,映射區可以向下擴展,
9 MAP_ANONYMOUS //匿名映射,映射區不與任何檔案關聯,
10 MAP_ANON //MAP_ANONYMOUS的別稱,不再被使用,
11 MAP_FILE //兼容標志,被忽略,
12 MAP_32BIT //將映射區放在行程地址空間的低2GB,MAP_FIXED指定時會被忽略,當前這個標志只在x86-64平臺上得到支持,
13 MAP_POPULATE //為檔案映射通過預讀的方式準備好頁表,隨后對映射區的訪問不會被頁違例阻塞,
14 MAP_NONBLOCK //僅和MAP_POPULATE一起使用時才有意義,不執行預讀,只為已存在于記憶體中的頁面建立頁表入口,
-
- fd:有效的檔案描述詞,如果 MAP_ANONYMOUS 被設定,為了兼容問題,其值應為-1;
-
- offset:被映射物件內容的起點,
- 相關函式:
int munmap(void * addr, size_t len)
- 成功執行時,munmap() 回傳0;失敗時,munmap 回傳 -1,error 回傳標志和 mmap 一致;該呼叫在行程地址空間中解除一個映射關系,addr 是呼叫 mmap() 時回傳的地址,len 是映射區的大小;當映射關系解除后,對原來映射地址的訪問將導致段錯誤發生,
int msync(void *addr, size_t len, int flags)
- 一般說來,行程在映射空間的對共享內容的改變并不直接寫回到磁盤檔案中,往往在呼叫 munmap() 后才執行該操作,可以通過呼叫 msync() 實作磁盤上檔案內容與共享記憶體區的內容一致,
- 當映射關系解除后,對原來映射地址的訪問將導致段錯誤發生,
⑥ mmap 使用細節
- 使用 mmap 需要注意的一個關鍵點是,mmap 映射區域大小必須是物理頁大小(page_size)的整倍數(32 位系統中通常是 4k 位元組),這是因為記憶體的最小粒度是頁,而行程虛擬地址空間和記憶體的映射也是以頁為單位,為了匹配記憶體的操作,mmap 從磁盤到虛擬地址空間的映射也必須是頁,
- 內核可以跟蹤被記憶體映射的底層物件(檔案)的大小,行程可以合法的訪問在當前檔案大小以內又在記憶體映射區以內的那些位元組,也就是說,如果檔案的大小一直在擴張,只要在映射區域范圍內的資料,行程都可以合法得到,這和映射建立時檔案的大小無關,
- 映射建立之后,即使檔案關閉,映射依然存在,因為映射的是磁盤的地址,不是檔案本身,和檔案句柄無關,同時可用于行程間通信的有效地址空間不完全受限于被映射檔案的大小,因為是按頁映射,
⑦ 映射區域大小如果不是物理頁的整倍數的具體情況分析
- 情形一:一個檔案的大小是 5000 位元組,mmap 函式從一個檔案的起始位置開始,映射 5000 位元組到虛擬記憶體中,
-
- 分析:因為單位物理頁面的大小是 4096 位元組,雖然被映射的檔案只有 5000 位元組,但是對應到行程虛擬地址區域的大小需要滿足整頁大小,因此 mmap 函式執行后,實際映射到虛擬記憶體區域 8192 個位元組,5000~8191 的位元組部分用零填充,映射后的對應關系如下圖所示:

-
- 此時:
-
-
- 讀/寫前 5000 個位元組(0~4999),會回傳操作檔案內容;
-
-
-
- 讀位元組 5000 ~ 8191 時,結果全為 0, 寫 5000 ~ 8191 時,行程不會報錯,但是所寫的內容不會寫入原檔案中 ;
-
-
-
- 讀/寫 8192 以外的磁盤部分,會回傳一個 SIGSECV 錯誤,
-
- 情形二:一個檔案的大小是 5000 位元組,mmap 函式從一個檔案的起始位置開始,映射 15000 位元組到虛擬記憶體中,即映射大小超過了原始檔案的大小,
-
- 分析:由于檔案的大小是 5000 位元組,和情形一一樣,其對應的兩個物理頁,那么這兩個物理頁都是合法可以讀寫的,只是超出 5000 的部分不會體現在原檔案中,由于程式要求映射 15000 位元組,而檔案只占兩個物理頁,因此 8192 位元組~15000 位元組都不能讀寫,操作時會回傳例外,如下圖所示:

-
- 此時:
-
-
- 行程可以正常讀/寫被映射的前 5000 位元組(0~4999),寫操作的改動會在一定時間后反映在原檔案中;
-
-
-
- 對于 5000~8191 位元組,行程可以進行讀寫程序,不會報錯,但是內容在寫入前均為 0,另外,寫入后不會反映在檔案中;
-
-
-
- 對于 8192~14999 位元組,行程不能對其進行讀寫,會報 SIGBUS 錯誤;
-
-
-
- 對于 15000 以外的位元組,行程不能對其讀寫,會引發 SIGSEGV 錯誤,
-
- 情形三:一個檔案初始大小為 0,使用 mmap 操作映射了 1000*4K 的大小,即 1000 個物理頁大約 4M 位元組空間,mmap 回傳指標 ptr,
-
- 分析:如果在映射建立之初,就對檔案進行讀寫操作,由于檔案大小為 0,并沒有合法的物理頁對應,如同情形二一樣,會回傳 SIGBUS 錯誤,
-
- 但是如果,每次操作 ptr 讀寫前,先增加檔案的大小,那么 ptr 在檔案大小內部的操作就是合法的,例如,檔案擴充 4096 位元組,ptr 就能操作 ptr ~ [ (char)ptr + 4095] 的空間,只要檔案擴充的范圍在 1000 個物理頁(映射范圍)內,ptr 都可以對應操作相同的大小,
-
- 這樣,方便隨時擴充檔案空間,隨時寫入檔案,不造成空間浪費,
三、iOS 中的 mmap
- 以官方的demo為例,其它的代碼很簡明直接,核心就在于 mmap 函式:
/**
* @param start 映射開始地址,設定 NULL 則讓系統決定映射開始地址
* @param length 映射區域的長度,單位是 Byte
* @param prot 映射記憶體的保護標志,主要是讀寫相關,是位運算標志;(記得與下面fd對應句柄打開的設定一致)
* @param flags 映射型別,通常是檔案和共享型別
* @param fd 檔案句柄
* @param off_toffset 被映射物件的起點偏移
*/
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
*outDataPtr = mmap(NULL,
size,
PROT_READ|PROT_WRITE,
MAP_FILE|MAP_SHARED,
fileDescriptor,
0);
- 用官網的代碼做參考,實作一個讀寫的例子:
#import "ViewController.h"
#import <sys/mman.h>
#import <sys/stat.h>
int MapFile(const char * inPathName, void ** outDataPtr, size_t * outDataLength, size_t appendSize) {
int outError;
int fileDescriptor;
struct stat statInfo;
// Return safe values on error.
outError = 0;
*outDataPtr = NULL;
*outDataLength = 0;
// Open the file.
fileDescriptor = open( inPathName, O_RDWR, 0 );
if(fileDescriptor < 0) {
outError = errno;
} else {
// We now know the file exists. Retrieve the file size.
if( fstat( fileDescriptor, &statInfo ) != 0 ) {
outError = errno;
} else {
ftruncate(fileDescriptor, statInfo.st_size + appendSize);
fsync(fileDescriptor);
*outDataPtr = mmap(NULL,
statInfo.st_size + appendSize,
PROT_READ|PROT_WRITE,
MAP_FILE|MAP_SHARED,
fileDescriptor,
0);
if( *outDataPtr == MAP_FAILED ) {
outError = errno;
} else {
// On success, return the size of the mapped file.
*outDataLength = statInfo.st_size;
}
}
// Now close the file. The kernel doesn’t use our file descriptor.
close( fileDescriptor );
}
return outError;
}
void ProcessFile(const char * inPathName) {
size_t dataLength;
void * dataPtr;
char *appendStr = " append_key";
int appendSize = (int)strlen(appendStr);
if( MapFile(inPathName, &dataPtr, &dataLength, appendSize) == 0) {
dataPtr = dataPtr + dataLength;
memcpy(dataPtr, appendStr, appendSize);
// Unmap files
munmap(dataPtr, appendSize + dataLength);
}
}
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString * path = [NSHomeDirectory() stringByAppendingPathComponent:@"test.data"];
NSLog(@"path: %@", path);
NSString *str = @"test str";
[str writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
ProcessFile(path.UTF8String);
NSString *result = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
NSLog(@"result:%@", result);
}
四、MMKV 和 mmap
① MMKV 簡介
- NSUserDefault 是常見的快取工具,但是資料有時會同步不及時,比如說在 crash 前保存的值很容易出現保存失敗的情況,在 App 重新啟動之后讀取不到保存的值,
- MMKV 很好的解決了 NSUserDefault 的局限,但是同樣由于其獨特設計,在資料量較大、操作頻繁的場景下,會產生性能問題,這里的使用給出兩個建議:
-
- 不要全部用 defaultMMKV,根據業務大的型別做聚合,避免某一個 MMKV 資料過大,特別是對于某些只會出現一次的新手引導、紅點之類的邏輯,盡可能按業務聚合,使用多個 MMKV 的物件;
-
- 對于需要頻繁讀寫的資料,可以在記憶體持有一份資料快取,必要時再更新到 MMKV,
② MMKV 原理
- 記憶體準備:通過 mmap 記憶體映射檔案,提供一段可供隨時寫入的記憶體塊,App 只管往里面寫資料,由 iOS 負責將記憶體回寫到檔案,不必擔心 crash 導致資料丟失,
- 資料組織:資料序列化方面選用 protobuf 協議,pb 在性能和空間占用上都有不錯的表現,考慮到要提供的是通用 KV 組件,key 可以限定是 string 字串型別,value 則多種多樣(int/bool/double 等),要做到通用的話,考慮將 value 通過 protobuf 協議序列化成統一的記憶體塊(buffer),然后就可以將這些 KV 物件序列化到記憶體中,
message KV {
string key = 1 ;
buffer value = 2;
}
- (B00L)setInt32:(int32 t)value forKey:(NSString*)key {
auto data = PBEncode(value);
return [self setData:data forKey:key];
}
- (BO0L)setData: (NSData*)data forKey:(NSString*)key {
auto kv = KV[key,data];
auto buf = PBEncode(kv);
return [self write: buf];
}
- 寫入優化:標準 protobuf 不提供增量更新的能力,每次寫入都必須全量寫入,考慮到主要使用場景是頻繁地進行寫入更新,我們需要有增量更新的能力:將增量 kv 物件序列化后,直接 append 到記憶體末尾;這樣同一個 key 會有新舊若干份資料,最新的資料在最后;那么只需在程式啟動第一次打開 mmkv 時,不斷用后讀入的 value 替換之前的值,就可以保證資料是最新有效的,
- 空間增長:使用 append 實作增量更新帶來了一個新的問題,就是不斷 append 的話,檔案大小會增長得不可控,例如同一個 key 不斷更新的話,是可能耗盡幾百 M 甚至上 G 空間,而事實上整個 KV 檔案就這一個 key,不到 1k 空間就存得下,這明顯是不可取的,我們需要在性能和空間上做個折中:以記憶體 pagesize 為單位申請空間,在空間用盡之前都是 append 模式;當 append 到檔案末尾時,進行檔案重整、key 排重,嘗試序列化保存排重結果;排重后空間還是不夠用的話,將檔案擴大一倍,直到空間足夠,
- (B00L)append: (NSData*)data {
if (space >= data.length) {
append(fd, data);
} else {
newData = unique(m_allKV);
if (total_space >= newData.length) {
write(fd, newData);
} else {
while (total_space < newData.length) {
total_ space *= 2;
}
ftruncate(fd, total . space);
write(fd, newData);
}
}
}
- 資料有效性:考慮到檔案系統、作業系統都有一定的不穩定性,另外增加了 crc 校驗,對無效資料進行甄別,
- MMKV 性能:寫個簡單的測驗,將 MMKV、NSUserDefaults 的性能進行對比(回圈寫入1w 次資料,測驗環境:iPhone X 256G, iOS 11.2.6,單位:ms):

- 可見 MMKV 性能遠遠優于 iOS 自帶的 NSUserDefaults,另外,在測驗中發現,NSUserDefaults 在每 2-3 次測驗,就會有 1 次比較耗時的操作,懷疑是觸發了資料 synchronize 重整寫入,對比之下,MMKV 即使觸發資料重整,也保持了性能的穩定高效,
五、NSData 與 mmap
- NSData 有一個靜態方法和 mmap 有關系:
+ (id)dataWithContentsOfFile:(NSString *)path options:(NSDataReadingOptions)readOptionsMask error:(NSError **)errorPtr;
typedef NS_OPTIONS(NSUInteger, NSDataReadingOptions) {
// Hint to map the file in if possible and safe. 在保證安全的前提下使用 mmap
NSDataReadingMappedIfSafe = 1UL << 0,
// Hint to get the file not to be cached in the kernel. 不要快取,如果該檔案只會讀取一次,這個設定可以提高性能
NSDataReadingUncached = 1UL << 1,
// Hint to map the file in if possible. This takes precedence over NSDataReadingMappedIfSafe if both are given. 總使用 mmap
NSDataReadingMappedAlways API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)) = 1UL << 3,
...
};
- Mapped 的意思是使用 mmap,那么 ifSafe 是什么意思呢?NSDataReadingMappedIfSafe 和 NSDataReadingMappedAlways 有什么區別?
- 如果使用 mmap,則在 NSData 的生命周期內,都不能洗掉對應的檔案,
- 如果檔案是在固定磁盤,非可移動磁盤、網路磁盤,則滿足 NSDataReadingMappedIfSafe,對 iOS 而言,這個 NSDataReadingMappedIfSafe = NSDataReadingMappedAlways,
- 那什么情況下應該用對應的引數?
-
- 如果檔案很大,直接使用 dataWithContentsOfFile 方法,會導致 load 整個檔案,出現記憶體占用過多的情況;此時用 NSDataReadingMappedIfSafe,則會使用 mmap 建立檔案映射,減少記憶體的占用,
-
- 使用場景:視頻加載,視頻檔案通常比較大,但是使用的程序中不會同時讀取整個視頻檔案的內容,可以使用 mmap 優化,
六、總結
- mmap 就是檔案的記憶體映射,通常讀取檔案是將檔案讀取到記憶體,會占用真正的物理記憶體;而 mmap 是用行程的記憶體虛擬地址空間去映射實際的檔案中,這個程序由作業系統處理,mmap 不會為檔案分配物理記憶體,而是相當于將記憶體地址指向檔案的磁盤地址,后續對這些記憶體進行的讀寫操作,會由作業系統同步到磁盤上的檔案,
- iOS 中使用 mmap 可以用 c 方法的 mmap(),也可以使用 NSData 的介面帶上NSDataReadingMappedIfSafe 引數,前者自由度更大,后者用于讀取資料,
- mmap 優點:
-
- 對檔案的讀取操作跨過了頁快取,減少了資料的拷貝次數,用記憶體讀寫取代 I/O 讀寫,提高了檔案讀取效率,
-
- 實作了用戶空間和內核空間的高效互動方式,兩空間的各自修改操作可以直接反映在映射的區域內,從而被對方空間及時捕捉,
-
- 提供行程間共享記憶體及相互通信的方式,不管是父子行程還是無親緣關系的行程,都可以將自身用戶空間映射到同一個檔案或匿名映射到同一片區域,從而通過各自對映射區域的改動,達到行程間通信和行程間共享的目的,
-
- 如果行程 A 和行程 B 都映射了區域 C,當 A 第一次讀取 C 時通過缺頁從磁盤復制檔案頁到記憶體中;但當 B 再讀 C 的相同頁面時,雖然也會產生缺頁例外,但是不再需要從磁盤中復制檔案過來,而可直接使用已經保存在記憶體中的檔案資料,
-
- 可用于實作高效的大規模資料傳輸,記憶體空間不足,是制約大資料操作的一個方面,解決方案往往是借助硬碟空間協助操作,補充記憶體的不足,但是進一步會造成大量的檔案 I/O 操作,極大影響效率,這個問題可以通過 mmap 映射很好的解決,換句話說,但凡是需要用磁盤空間代替記憶體的時候,mmap 都可以發揮其功效,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/296625.html
標籤:其他
上一篇:Devc++迷宮小游戲2.0
