iOS Memory Deep Dive

前言

僅以此文解答自己大學(xué)以來多年對(duì)內(nèi)存管理的疑惑。


經(jīng)典操作系統(tǒng)的虛擬內(nèi)存

為什么要有虛擬內(nèi)存?

隨著計(jì)算機(jī)的發(fā)展,我們的計(jì)算機(jī)處理的任務(wù)也變得越來越繁多,但是對(duì)于某臺(tái)固定的計(jì)算機(jī),CPU 和 Memory 都是固定的,如果有些直接使用物理內(nèi)存地址的話會(huì)帶來很多問題, 首先編譯器不能以一種抽象的角度來描繪內(nèi)存,在執(zhí)行的過程中如果某個(gè)進(jìn)程占據(jù)的內(nèi)存過大,這個(gè)進(jìn)程可能就無法運(yùn)行,即便運(yùn)行了內(nèi)存相對(duì)來說是非常不安全的,一個(gè)不小心操作到了別的進(jìn)程的內(nèi)存,可能導(dǎo)致進(jìn)程的崩潰,如果寫入了內(nèi)核使用的內(nèi)存可能導(dǎo)致操作系統(tǒng)的崩潰。

現(xiàn)代操作系統(tǒng)的內(nèi)存管理是非常多計(jì)算機(jī)科學(xué)家智慧的結(jié)晶,這種管理方式就是 虛擬內(nèi)存 (Virtual Memory/VM). VM是一些列技術(shù)的總稱,包括硬件異常,物理之地翻譯,主存,磁盤文件,操作系統(tǒng)內(nèi)核軟件的內(nèi)存管理。

虛擬內(nèi)存提供了三大重要的特性

  1. 它將主存看做在存儲(chǔ)在磁盤上的地址空間的高速緩存,利用程序的局部性原理,只將活躍的內(nèi)存加載到主存中,提高了主存的利用率。
  2. 為每個(gè)進(jìn)程提高了一個(gè)抽象的統(tǒng)一的連續(xù)的私有的地址空間。簡(jiǎn)化了內(nèi)存管理方式。
  3. 對(duì)內(nèi)測(cè)進(jìn)行分段(segment)提供權(quán)限能力,保護(hù)每個(gè)進(jìn)程的地址空間不會(huì)被其他進(jìn)程影響。

尋址方式

在一些早期的操作系統(tǒng)和一些嵌入式操作系統(tǒng)中,內(nèi)存管理使用的地址是物理地址,
現(xiàn)代操作系統(tǒng)基本使用的是 虛擬地址(Virtual Addressing)的尋址方式,使用 虛擬地址時(shí) CPU將 VA 送到MMU中去翻譯為物理地址。

注: MMU (Memory Management Unit) 內(nèi)存管理單元一般是一個(gè)CPU上的專用芯片,是一個(gè)硬件。
結(jié)合操作系統(tǒng)共同完成地址翻譯工作。

地址空間

通常來說地址空間是線性的 假設(shè)我們有 {0, 1, 2, ..N-1 } 個(gè)內(nèi)存地址,我們可以用n位二進(jìn)制來表示內(nèi)存地址,那么我們就叫這個(gè)地址空間為n位地址空間, 現(xiàn)代操作系統(tǒng)通常是 32 或者 64(但是很多操作系統(tǒng)只用了48位尋址)的 。

2^10 = 1k
2^20 = 1M
2^30 = 1G
2^40 = 1T
2^50 = 1P
2^60 = 1E

這么看來大家能理解為什么32位的操作系統(tǒng)最大只支持4G內(nèi)存空間了。

分頁

現(xiàn)代操作系統(tǒng)將內(nèi)存劃分為頁,來簡(jiǎn)化內(nèi)存管理,一個(gè)頁其實(shí)就是一段連續(xù)的內(nèi)存地址的集合,通常有4k和16k(iOS 64位是16K)的,成為 Virtual Page 虛擬頁。與之對(duì)應(yīng)的物理內(nèi)存被稱為Physical Page 物理頁。

注意 虛擬頁的個(gè)數(shù)可能和物理頁個(gè)數(shù)不一樣 比如說一個(gè) 64位操作系統(tǒng)中使用48位地址空間的 虛擬頁大小為16K,那么其虛擬頁可數(shù)可達(dá)到(248/214 = 16M個(gè))假設(shè)物理內(nèi)存只有 4G 那么物理頁可能只有 (232/214 = 256k個(gè))

操作系統(tǒng)將虛擬頁和物理頁的映射關(guān)系稱為頁表(Page Table),每個(gè)映射叫 頁表?xiàng)l目(Page Table Entry/Item),操作系統(tǒng)為每個(gè)進(jìn)程提供一個(gè)頁表放在主存中,CPU在使用虛擬地址時(shí)交給MMU去翻譯地址,MMU去查詢?cè)谥鞔嬷械捻摫韥矸g。

缺頁處理

每個(gè) Page Table Entry 都包含了一些描述信息,比如前頁的狀態(tài){未分配, 緩存的,未緩存的}。

  1. 未分配的不用多說代表未使用的內(nèi)存
  2. 緩存的代表已經(jīng)加載進(jìn)物理內(nèi)存了
  3. 未緩存的代表還沒放在物理內(nèi)存。

當(dāng)CPU要讀取一個(gè)頁時(shí),檢查標(biāo)記發(fā)現(xiàn)當(dāng)前的頁是未緩存的,會(huì)觸發(fā)一個(gè)(Page Falut)缺頁中斷,這時(shí)內(nèi)核、、操作系統(tǒng)的缺頁異常處理程序,去選擇一個(gè)犧牲頁(有時(shí)候內(nèi)存夠用不用置換別的界面),然后檢查這個(gè)頁面是否有修改,有修改先寫會(huì)磁盤,然后將需要使用到的內(nèi)存加載到物理內(nèi)存中,然后更新PTE 隨后操作系統(tǒng)重新把虛擬地址發(fā)送到地址翻譯硬件去重新處理。

注: 有些操作系統(tǒng)無 虛擬虛擬內(nèi)存置換邏輯,如iOS,取而代之的是內(nèi)存壓縮,和收到內(nèi)存警告時(shí)殺死進(jìn)程的行為。

虛擬內(nèi)存帶來的好處

  1. 簡(jiǎn)化鏈接過程,允許每個(gè)進(jìn)程都提供統(tǒng)一的內(nèi)存地址的抽象,獨(dú)立與物理內(nèi)存。
  2. 簡(jiǎn)化加載,操作系統(tǒng)加載可執(zhí)行文件和共享文件時(shí),只是創(chuàng)建了 頁表,待訪問到缺頁時(shí),操作系統(tǒng)再去加載。
  3. 簡(jiǎn)化共享,不同進(jìn)程的PT中的PTE 可以執(zhí)行相同的物理地址,如動(dòng)態(tài)庫的代碼。
  4. 內(nèi)存保護(hù),PT中的PTE中描述了一個(gè)虛擬頁的權(quán)限信息,(R, W, X)、指令如果違反了這些權(quán)限信息,就會(huì)造成(Segment Fault)

地址翻譯

虛擬地址翻譯到物理地址是軟硬件結(jié)合實(shí)現(xiàn)的。我們通常幾個(gè)方面來描述。

如何索引

現(xiàn)代操作系統(tǒng)將地址分為兩部分,頁號(hào)和片了(是不是很類型網(wǎng)絡(luò)號(hào)和主機(jī)號(hào)),由于虛擬頁和物理頁的大小是相同的,頁偏移可以看做虛擬頁和物理頁的頁內(nèi)地址,且相同,頁號(hào)則做為PT的索引查找到對(duì)應(yīng)的PTE,然后查找對(duì)應(yīng)的物理頁地址。

提高效率

是不是像前面所說的簡(jiǎn)單的劃分位兩部分就足夠了呢?

舉個(gè)例子:

  1. 我們假設(shè)一臺(tái)電腦是 32 位的,分頁大小位 4k 也就說頁內(nèi)地址占據(jù)了 12 位,頁號(hào)地址位 20 位
  2. 我們假設(shè)一臺(tái)電腦是 64 位的,地址空間 48 位,分頁大小位 16k 也就說頁內(nèi)地址占據(jù)了 14 位,頁號(hào)地址位 34 位

我們粗略估算一個(gè)PTE為4KB 對(duì)于 32位的操作系統(tǒng)每個(gè)進(jìn)程的頁表需要 2^20 = 4M 個(gè)頁表項(xiàng)常駐內(nèi)存尚可接受
但是對(duì)于尋址為48位的操作系統(tǒng)來說,每個(gè)進(jìn)程的頁表為需要 2^32 = 4G個(gè)頁表項(xiàng),這是無法接受的。

計(jì)算機(jī)的世界所有的難題都可以同加一次的辦法來解決,所以現(xiàn)代操作系統(tǒng)通常都使用多級(jí)頁表,減少頁表項(xiàng)的個(gè)數(shù)。將虛擬地址分為多端,代表了一級(jí) 二級(jí) 多級(jí)頁表。通過多級(jí)頁表可以大大較少內(nèi)存占用。

減少內(nèi)存

眾所周知CPU要比Memory快10^3個(gè)數(shù)量級(jí),即便CPU中的L3Cache 也比Memory快很多,如果MMU美的地址翻譯都要去查找多級(jí)PT,這個(gè)開銷就會(huì)非常巨大,但是所幸 程序的局部性原理能夠解救我們,MMU芯片內(nèi)置一個(gè) 翻譯后備緩沖器(Transalation Lookaside Buffer TLB )的硬件來充當(dāng)緩存,加快地址翻譯的效率.

現(xiàn)代 OS 虛擬內(nèi)存系統(tǒng)

操作系統(tǒng)為每個(gè)進(jìn)程維護(hù)一個(gè)單獨(dú)的虛擬地址空間,分為兩部分。

  1. 內(nèi)核虛擬內(nèi)存,包含內(nèi)核中的代碼和數(shù)據(jù)結(jié)構(gòu),還有一些被映射到所有進(jìn)程共享的內(nèi)存頁面。還有一些頁表,內(nèi)核在進(jìn)程上下文中執(zhí)行代碼使用的棧。
  2. 進(jìn)程虛擬內(nèi)存。OS將內(nèi)存組織為一些區(qū)域(Sement)的集合,代碼端,數(shù)據(jù)端,共享庫端,線程棧都是不同的區(qū)域,分段的原因是便于管理內(nèi)存的權(quán)限,如果了解過Mach-O文件或者ELF文件的讀者可以看到相同的Segment里面的內(nèi)存權(quán)限是相同的,每個(gè)Segment再劃分不同的內(nèi)容為section。

在內(nèi)核中描述一個(gè)進(jìn)程的數(shù)據(jù)結(jié)構(gòu) 概略為如下

pgb指向第一級(jí)頁表的基址

                                            進(jìn)程虛擬地址
                        vm_area_struct      |----------------|
----    ----     |---->  vm_end------|      |                |
 mm ---> pgb     |       vm_start ---|---|  |                |
----    mmap  ---        vm_prot     |   |->|----------------|
        ----             vm_flags    |      |      共享庫     |
                      -- vm_next     |----->|----------------|
                    |                       |
                    |                       |
                    |->  vm_end-----|  |--> |----------------|
                         vm_start---|--|    |     數(shù)據(jù)段      |
                         vm_prot    |------>|----------------|
                         vm_flags           |

每個(gè)區(qū)域的描述主要有一下幾個(gè)
vm_start 指向這個(gè)區(qū)域的起始處
vm_end 指向這個(gè)區(qū)域的結(jié)束出
vm_prot 內(nèi)存區(qū)域的讀寫權(quán)限
vm_flasg 一些標(biāo)志位 私有的還是共享的
vm_next 指向下一個(gè)vm_area_struct的描述

內(nèi)存映射 MMAP

類Unix操作系統(tǒng)可以荀彧映射一個(gè)普通磁盤上的文件的連續(xù)部分到一個(gè)固定的內(nèi)存取區(qū)域。操作系統(tǒng)會(huì)會(huì)自動(dòng)管理映射的內(nèi)容。

內(nèi)存映射允許不同的進(jìn)程映射不同的虛擬內(nèi)存到同一塊物理內(nèi)容上,他們可以是共享的也可以是私有的。

對(duì)于共享的,通常多個(gè)進(jìn)程映射到相同的共享對(duì)象上,

對(duì)與私有的,不同進(jìn)程初始映射的時(shí)候操作系統(tǒng)為了節(jié)省資源,并沒有產(chǎn)生真的副本,知道某個(gè)進(jìn)程修改了這個(gè)私有對(duì)象,操作系統(tǒng)運(yùn)用copy on write技術(shù)在此時(shí)才發(fā)生真正的文件拷貝。

mmap在類unix操作系統(tǒng)上作為一個(gè)系統(tǒng)調(diào)用存在,函數(shù)簽名如下

void *
     mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
     addr 代表要從那塊虛擬地址開始映射,通??梢圆挥弥付▊鬟fNULL讓操作系統(tǒng)自己給我們選擇
     len 映射多少長(zhǎng)度的內(nèi)容
        prot 映射文件的訪問權(quán)限 讀寫可執(zhí)行權(quán)限等
        PROT_EXEFC 可執(zhí)行權(quán)限
        PROT_READ 可讀權(quán)限
        PROT_WRITE 可寫權(quán)限
        PROT_NONE 無法訪問權(quán)限
    flags 訪問文件的標(biāo)記
        MAP_SHARED 共享的
        MAP_PRIVATE私有的
        MAP_ANON 私有的

舉個(gè)例子將任意文件映射到stdout


#include <sys/mman.h>

int main(int argc, const char * argv[]) {
    struct stat stat;
    int fd;
    if (argc != 2) {
        printf("must pass file path");
        return 1;
    }
    fd = open(argv[1], O_RDONLY, 0);
    fstat(fd, &stat);
    char *buffer = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    printf("%s", buffer);
    return 0;
}

MMAP在iOS中的用處

  1. mmap讓讀寫一個(gè)文件像操作一個(gè)內(nèi)存地址一樣簡(jiǎn)單方便,
  2. mmap效率極高,不用將一個(gè)內(nèi)容從磁盤讀入內(nèi)核態(tài)再拷貝至用戶態(tài)
  3. mmap映射的文件由操作系統(tǒng)接管,如果進(jìn)程Crash 操作系統(tǒng)會(huì)保證文件刷新回磁盤

動(dòng)態(tài)內(nèi)存分配

雖然可以使用上面的低級(jí)API去映射內(nèi)存,但是需要?jiǎng)討B(tài)申請(qǐng)內(nèi)存用來做變量處理的時(shí)候就需要?jiǎng)討B(tài)內(nèi)存分配器(Dunamic memory allocator)簡(jiǎn)單理解為 malloc calloc realloc free等函數(shù)來自的庫就稱為DMA.
動(dòng)態(tài)內(nèi)存分配器將一個(gè)內(nèi)存的區(qū)域(Heao)分為不同的大小的塊(block),這些塊要不然就是分配的,要不然就是空閑的。
如何設(shè)計(jì)分配器又是一個(gè)大難題。 幾乎所有的計(jì)算機(jī)語言都采用以下兩種。

  1. 顯式分配器(手動(dòng)管理內(nèi)容)
  2. 隱式分配器(GC)

隱式內(nèi)存分配器

通常比較知名的語言 Java javaScript Ruby 等都使用GC,最早的GC只是使用標(biāo)記清除算法來管理內(nèi)容,通過幾十年的迭代,早已更新出了數(shù)種算法共同參與的GC。這里就不再贅述了

顯式內(nèi)存分配器

C語言提供了一些列的方法來管理動(dòng)態(tài)內(nèi)存。如

  1. malloc 申請(qǐng)內(nèi)容并返回初始化的內(nèi)存首地址
  2. calloc 同malloc一致,并且會(huì)將申請(qǐng)到的內(nèi)存全置為0、
  3. realloc,重新分配原本已經(jīng)申請(qǐng)的內(nèi)存空間。
  4. free 釋放內(nèi)容空間
  5. sbrk 擴(kuò)展收縮堆

如何實(shí)現(xiàn)一個(gè)自己的顯式內(nèi)存分配器

首先我們要明確內(nèi)存分配器的需求

  1. 處理任意順序的申請(qǐng)內(nèi)存和釋放內(nèi)存
  2. 立即響應(yīng),不應(yīng)為了性能二重新排列或者緩存請(qǐng)求
  3. 所有內(nèi)容都在heap里存放
  4. 對(duì)齊塊,使之可以存放任意類型的數(shù)據(jù)
  5. 不修改已分配的內(nèi)存塊

鑒于對(duì)齊和處理任意順序內(nèi)存管理的需求,堆利用效率可能會(huì)降低,主要會(huì)產(chǎn)生內(nèi)存碎片(Fragmentation) 內(nèi)存碎片分為兩種。

  1. 內(nèi)部碎片,通常是指一個(gè)分配過的塊數(shù)據(jù)并不是全部塊的內(nèi)容,通常有元信息,對(duì)齊的字節(jié)等。
  2. 外部碎片是指不連續(xù)的可用的塊,通常外部碎片過多會(huì)產(chǎn)生,所有空白塊相加可以滿足申請(qǐng)的資源,但是他們不連續(xù)。需要整理碎片。

實(shí)現(xiàn)顯式內(nèi)存分配器的重點(diǎn)

  1. 空閑塊組織
  2. 如何分配新申請(qǐng)的塊
  3. 如果組織空閑快的剩余部分
  4. 如何合并剛釋放的塊

顯式內(nèi)存分配器的實(shí)現(xiàn)方案

隱式空閑鏈表

這種方式在malloc申請(qǐng)內(nèi)存的時(shí)候,實(shí)際上申請(qǐng)的是實(shí)際所需內(nèi)存加上部門元信息大小的塊,然后返回指針是有效數(shù)據(jù)的首地址,元信息直接存在數(shù)據(jù)塊中,所以稱為隱式空閑鏈表。

隱式鏈表需要處理如何分割空閑庫和合并空閑快

顯式空閑鏈表

由于隱式空閑鏈表的搜索效率角度,其實(shí)是不適用通用的內(nèi)存分配的??梢允褂媚撤N形式的數(shù)據(jù)結(jié)構(gòu)去管理這些內(nèi)存塊。
基本分為 幾種,

  1. 簡(jiǎn)單分離器存儲(chǔ)
  2. 分離適配法
  3. 伙伴系統(tǒng)法

關(guān)于詳細(xì)的設(shè)計(jì)需要讀者查看更多算法知識(shí)的文檔。

顯式內(nèi)存分配器的實(shí)現(xiàn)

顯式內(nèi)存分配器的需求已經(jīng)很清晰,下面有個(gè)簡(jiǎn)單的例子可以參考,這時(shí)候?qū)τ?C 類語言的內(nèi)存管理應(yīng)該不會(huì)太過恐懼了,
C++實(shí)現(xiàn)一個(gè)簡(jiǎn)易的內(nèi)存池分配器,
畢竟源碼面前了無秘密。

iOS的虛擬內(nèi)存

iOS 內(nèi)存的分頁大小

在arm64之后的芯片,操作系統(tǒng)通常使用16KB作為頁大小,我們寫的程序中的虛擬內(nèi)存地址右移動(dòng)14位則可得到頁編號(hào)。MMU通過TLB和固定在內(nèi)存進(jìn)程虛擬區(qū)域的頁表來翻譯來物理地址。
下面一份代碼可以獲取頁大小。


int main(int argc, char * argv[]) {
    //    獲取虛擬內(nèi)存分頁數(shù)據(jù) 14為頁內(nèi)地址
    printf("page-size%ld mask:%ld, shift%d \n", vm_kernel_page_size, vm_kernel_page_mask, vm_kernel_page_shift);
    printf("%ld\n", sysconf(_SC_PAGE_SIZE));
    printf("%d\n", getpagesize());
    printf("%d\n", PAGE_SIZE); // 編譯時(shí)確定不建議使用
    return 0;
}

在觀察Crash日志的時(shí)候 有時(shí)候注意崩潰的頁號(hào)可以幫助我們尋找崩潰的原因。

頁面的類型

當(dāng)操作系統(tǒng)分配一個(gè)頁面時(shí),內(nèi)存被稱為Clean的,以為這這個(gè)內(nèi)存頁面沒有使用,是可以被釋放或者重建的,但是一旦寫入,操作系統(tǒng)會(huì)將其標(biāo)記為Dirty,這意味著磁盤或者其他地方?jīng)]有此內(nèi)存頁面的備份,無法恢復(fù)它。

由于iPhone設(shè)備為了減少閃存的壽命,并沒有在閃存上使用交換分區(qū),因此無論使用多少,在內(nèi)存壓力高緊時(shí),操作系統(tǒng)不會(huì)將Dirty寫好磁盤,而是釋放Clean的頁面如,可執(zhí)行代碼(Mach-O)的映射和內(nèi)存映射文件,或者是kill掉進(jìn)程。
因此使用dirty的內(nèi)存越多,對(duì)我們的進(jìn)程的穩(wěn)定性越差。

iOS內(nèi)存的優(yōu)化

在其他常見的操作系統(tǒng)上,由于局部性原理,OS會(huì)將不常用的內(nèi)存頁面寫會(huì)磁盤,但是iOS沒有交換空間,取而代之的是內(nèi)存壓縮技術(shù),iOS 將不常用到的dirty頁面壓縮以減少頁面占用量,在再次訪問到的時(shí)候重新解壓縮。這些都在操作系統(tǒng)層面實(shí)現(xiàn),對(duì)進(jìn)程無感知,有趣的是如果當(dāng)前進(jìn)程收到了 memoryWarning, 進(jìn)程這時(shí)候準(zhǔn)備釋放大量的誤用內(nèi)存,如果訪問到過多的壓縮內(nèi)存,再解壓縮內(nèi)存的時(shí)候反而會(huì)導(dǎo)致內(nèi)存壓力更大,然后被OS kill掉。

iOS 進(jìn)程中的堆和棧

需要注意的是通常操作系統(tǒng)書籍中描述的進(jìn)程虛擬內(nèi)存模型都是這樣的

[圖片上傳失敗...(image-a1d525-1538764389522)]

這實(shí)際是個(gè)用于解析給讀者的簡(jiǎn)化模型,對(duì)于多線程程序來說,每個(gè)線程都有自己的線程棧,在iOS上通常主線程線程棧大小為1MB,子線程棧大小為 512KB,如果你有一臺(tái)越獄機(jī) 可以試驗(yàn) ulimt -a 命令觀察棧大小的默認(rèn)參數(shù)。

iOS平臺(tái)上的常見編程語言的內(nèi)存管理方式

iOS 上常用的Swift 和 Objective-C ,C , C++ 都使用顯式的內(nèi)存管理?策略,比如 malloc 和 free, new 和delete alloc 和 dealloc,在Objective-C和Swift通常使用一種叫做引用計(jì)數(shù)的簡(jiǎn)化模型來管理堆內(nèi)存?,F(xiàn)代Clang已經(jīng)支持ARC的技術(shù)幫助程序員解脫內(nèi)存管理的困擾,但是本質(zhì)上還是顯式內(nèi)存管理。

建議讀者可以讀一下ARC的參考文檔,
順便提一下Xcode10 版本中的Clang已經(jīng)支持在C結(jié)構(gòu)體中對(duì)于Objective-C對(duì)象的ARC管理,請(qǐng)參看 whats_new_in_llvm

內(nèi)存分類

要想合理的使用內(nèi)存必須要掌握不同類型內(nèi)存的區(qū)別,才能更合理的使用內(nèi)存并且在內(nèi)存資源匱乏的低端機(jī)器上寫出“高內(nèi)存性能”的應(yīng)用。

首先在 Apple 的官方文檔中內(nèi)存主要分為以下幾類。

  1. Free Memory 當(dāng)前空閑的memory
  2. Used Mamory 當(dāng)前正在使用的內(nèi)存

我們最關(guān)心的當(dāng)然是 Used Memory,它又分為以下幾類。

  1. Wired Memory。 一般是內(nèi)核占用的常駐內(nèi)存,比如可執(zhí)行文件的鏡像 Image,內(nèi)核所有的數(shù)據(jù)等,無法釋放,在OS運(yùn)行期間必須常駐內(nèi)存。
  2. Active Memory 活躍的內(nèi)存,當(dāng)前正在使用的內(nèi)存.
  3. Inactive Memory。不活躍的內(nèi)存,最近用過,但是現(xiàn)在不怎么用了,按照局部性原則可以被置換出物理內(nèi)存的內(nèi)存。
  4. Purgeable Memory??舍尫诺膬?nèi)存,通常在Foundation中是 NSDiscardableContent 的子類,或者是 NSCache等。

等等。上面說的好像跟沒說一樣/(ㄒoㄒ)/~。我們換種方式從物理內(nèi)存和虛擬內(nèi)存的層面來解釋。


首先我們的虛擬內(nèi)存使用的是Page來描述的。 一個(gè)Page 有兩種狀態(tài) Dirty 和 Clean。在iOS中Clean是可以被回收的。

Virtual Memory分類

  1. Clean Memory 主要包括 system framework、binary executable 、memory mapped files
  2. Dirty Memory 括Heap allocation、caches、decompressed images等。

(每個(gè)進(jìn)程擁有一份獨(dú)立的 Virtual memory pace)Virtual Memory = clean Memory

PhySical Memory

物理內(nèi)存是指真正加載在主存中的內(nèi)存,所以實(shí)際了解上真正的物理內(nèi)存占用才對(duì)我們內(nèi)存管理幫助更大。

  1. DirtyMemory
  2. Clean Memory but loaded。
  3. Page Table
  4. ComPressed memory
  5. IOKit Used
  6. Purgeable

內(nèi)存測(cè)量工具

了解到前面說的內(nèi)存分類之后我們應(yīng)該怎么測(cè)量我們的內(nèi)存分布呢。主要有幾種工具,命令行工具,Xcode工具,代碼工具等。

命令行工具

如果你開發(fā)的是 Mac 程序,Mac OS 自帶的有一下幾種。

  1. top 程序
  2. heap 程序
  3. leaks 程序
  4. vmmap 程序

這些工具讀者查看 Man Page 即可。

需要注意的是。以上工具分析的大多是虛擬內(nèi)存,也就是說對(duì)于 桌面級(jí)程序更適合,但是對(duì)于iOS中沒有交換空間,且擁有Jetsam監(jiān)控程序的設(shè)備,可能還需要更精準(zhǔn)的測(cè)量工具。

順便提一句。一個(gè)堆區(qū)上malloc的程序如果并沒有使用,雖然它是Clean的,但是也會(huì)被程序統(tǒng)計(jì)到,理論上 malloc 可以申請(qǐng)到的虛擬內(nèi)存大小非常接近 virtual Memory Space 的大?。ㄟ@么說的原因是 前文也提到了 malloc 實(shí)際上是動(dòng)態(tài)分配器程序提供的一些列函數(shù),為了性能,大多數(shù)動(dòng)態(tài)分配器都講堆分為好幾塊用來做不同大小虛擬內(nèi)存的管理,因此malloc可以申請(qǐng)到的虛擬內(nèi)存大小實(shí)際決定于動(dòng)分配器代碼的實(shí)現(xiàn)。有興趣的讀者可以讀一下。)

Xcode 提供的工具

  1. Xcode Debug Area
  2. Instruments
  3. DebugMemoryGraph
Memory Report

instruments

DebugMemoryGraph

Scheme

Tips 配置了 MallocStackLogging 的話甚至可以追蹤每個(gè) 虛擬內(nèi)存中的對(duì)象申請(qǐng)堆棧,便于我們更好的發(fā)現(xiàn)問題。

注意點(diǎn):所有Xcode提供的工具必須使用真機(jī)測(cè)試才能最難接近用戶的使用環(huán)境

代碼工具

我們通過開發(fā)工具可以用來測(cè)量我們的內(nèi)存,但是到了線上這些都用不了,能精準(zhǔn)的測(cè)量APP用到的物理內(nèi)存才比較重要。

大部分的代碼測(cè)量?jī)?nèi)存是通過拿到Mach內(nèi)核提供的 task_info 來測(cè)量的,但是這個(gè)信息更多的是虛擬內(nèi)存層面的信息不能正確的衡量物理內(nèi)存。


#include <malloc/malloc.h>
#include <mach/mach_host.h>
#include <mach/task.h>

int main(int argc, char * argv[]) {
    @autoreleasepool {
        // method 1
        struct mstats currentStat = mstats();
        printf("Freed Bytes:%ld, Used Bytes:%ld Total Bytes:%ld", currentStat.bytes_free, currentStat.bytes_used, currentStat.bytes_total);
        // method 2
        vm_statistics_data_t vmStats;
        mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
        kern_return_t kernReturn = host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount);
        printf("free: %lu\nactive: %lu\ninactive: %lu\nwire: %lu\nzero fill: %lu\nreactivations: %lu\npageins: %lu\npageouts: %lu\nfaults: %u\ncow_faults: %u\nlookups: %u\nhits: %u",
              vmStats.free_count * vm_page_size,
              vmStats.active_count * vm_page_size,
              vmStats.inactive_count * vm_page_size,
              vmStats.wire_count * vm_page_size,
              vmStats.zero_fill_count * vm_page_size,
              vmStats.reactivations * vm_page_size,
              vmStats.pageins * vm_page_size,
              vmStats.pageouts * vm_page_size,
              vmStats.faults,
              vmStats.cow_faults,
              vmStats.lookups,
              vmStats.hits
              );
        // method3
        task_basic_info_data_t taskInfo;
        infoCount = TASK_BASIC_INFO_COUNT;
        kernReturn = task_info(mach_task_self(),
                                             TASK_BASIC_INFO,
                                             (task_info_t)&taskInfo,
                                             &infoCount);
        
        if (kernReturn == KERN_SUCCESS) {
            printf("resdientSize is :%ld", taskInfo.resident_size);
        }
        return 0;
    }
}

其中尤其是和Xcode Debug Area的差距較大有時(shí)候可能會(huì)偏差 50M-100M ,于是有大佬拔出了Xcode 的 DebugServer 和 WebKit 中的的物理內(nèi)存計(jì)算方式(2018WWDC 蘋果也說了 footPrint才是真正的物理內(nèi)存使用ios_memory_deep_dive
代碼如下


std::optional<size_t> memoryFootprint()
{
    task_vm_info_data_t vmInfo;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
    if (result != KERN_SUCCESS)
        return std::nullopt;
    return static_cast<size_t>(vmInfo.phys_footprint);
}

線上檢查工具

線上檢查內(nèi)存通常會(huì)檢查內(nèi)存泄漏,一般有開源的工具

  1. MLeaksFinder
  2. FBRetainCycleDetector

高性能使用內(nèi)存

了解完那么多原理和分析的工具,那么在日常使用中有沒有什么指導(dǎo)原則可以幫助我們來寫出更快,內(nèi)存占用更低的代碼呢?

  1. 首先熟讀 ARCMenual ,大部分iOS開發(fā)者其實(shí)是完全不清楚ARC是怎么實(shí)現(xiàn)的,還有相對(duì)于的原則,尤其是Autorelease 修飾的指針,還有在多線程情況下的原則。
  2. 用weak修飾替換unsafe_unretain
  3. 使用weak strong dance 來解決block中的循環(huán)引用問題。需要注意的是大部分人都以為使用了weak指針就可以了。其實(shí)不然,在block內(nèi)必須使用 strong 重新綁定變量,避免在多線程情況下weak變量為空導(dǎo)致Crash,使用strong指針前判斷是否為空
    例:

- (void)test {
    weak __typeof(self) weakSelf = self;
    [xxobjc onCompleate:^(){
        strong __typeof(self) self = weakSelf;
        if (!self) { return; }
        [xx moreCompleate:&(){
            strong __typeof(self) self = weakSelf;
            if (!self) { return; }
            // do something
        }];
    }];
}
  1. 小心方法中的self,在Objective-C的方法中 隱含的 self 是 __unsafed_unretain的。
  2. 使用Autoreleasepool來降低循環(huán)中的內(nèi)存峰值,避免OOM。
  3. 要處理 Memory Warning
  4. C/C++ new 出來的要delete malloc 的要 free。
  5. UITableView/UICollectionView 的重用(不單單是cell重用 cell 使用的子view也要重用。
  6. [UIImage imageNamed:] 適合于UI界面中的貼圖的讀取,較大的資源文件應(yīng)該盡量避免使用。
  7. WKWebView是跨進(jìn)程通信的,不會(huì)占用我們的APP使用的物理內(nèi)存量。
  8. try_catch_finally 一定要清理資源
  9. 盡量少引用 performaSelector: 會(huì)對(duì)ARC的內(nèi)存管理產(chǎn)生錯(cuò)誤,導(dǎo)致內(nèi)存泄漏。
  10. lazy load 那些大的內(nèi)存對(duì)象, 尤其是需要保證線程安全,可以參考 java中的懶漢式Double Check 寫法。
  11. 需要在收到內(nèi)存警告的時(shí)候釋放的Cache 用 NSCache 代替 NSDictionary, 使用 NSPurgableData代替NSData.

前文中我們說到iOS的沒有交換分區(qū)的概念,取而代之的是壓縮內(nèi)存的辦法,倘若在使用NSDictionary的時(shí)候收到內(nèi)存警告,然后去釋放這個(gè)NSDictionary,如果占據(jù)的內(nèi)存過大,很可能在解壓的過程中就被JetSem Kill 掉,如果你的內(nèi)存只是誤用的緩存或者是可重建的數(shù)據(jù),就把NSCache當(dāng)初NSDictionary用吧。同理 NSPurableData也是。

  1. 不要使用像素過大的圖片文件,即便一個(gè)圖片在磁盤中很小,但是因?yàn)閳D片像素寬高很大也會(huì)占據(jù)更多的內(nèi)存,這里有個(gè)公式可以計(jì)算widthPx * HeightPx * 4Bytes per pixel(alpha red green blue).即便在iOS12中已經(jīng)可以優(yōu)化單色圖的內(nèi)存占用,可畢竟是iOS12,現(xiàn)在好多公司還在支持iOS8 ~~
  2. 使用 NSData和UIImage 的 mmap加載選型來加載那些可以被重建的數(shù)據(jù)。
  3. 在子線程手動(dòng)申請(qǐng)(maloc)大內(nèi)存的的時(shí)候ping一下主線程,因?yàn)樽泳€程無法收到內(nèi)存警告的傳遞

- (void)test {
    // current on sub Thread
    // if main thread is memory warning it will blocked
    dispatch_sync(dispatch_get_main_queue(), ^{
        [some description]
    });
    malloc(huge memory);
}

參考

  1. 深入理解計(jì)算機(jī)系統(tǒng)
  2. 高性能iOS應(yīng)用開發(fā)
  3. iOS和macOS性能優(yōu)化:Cocoa、Cocoa Touch、Objective-C和Swift
  4. WWDC iOS Memory Deep Dive
  5. C++實(shí)現(xiàn)一個(gè)簡(jiǎn)易的內(nèi)存池分配器
  6. ARC的參考文檔
  7. whats_new_in_llvm
  8. 先弄清楚這里的學(xué)問,再來談 iOS 內(nèi)存管理與優(yōu)化(一)
  9. 先弄清楚這里的學(xué)問,再來談 iOS 內(nèi)存管理與優(yōu)化(二)
  10. 讓人懵逼的 iOS 系統(tǒng)內(nèi)存分配問題
  11. 探索iOS內(nèi)存分配
  12. iOS內(nèi)存深入探索之VM Tracker
  13. iOS內(nèi)存深入探索之Leaks
  14. iOS內(nèi)存深入探索之內(nèi)存用量
  15. iOS筆記-記錄一次內(nèi)存泄漏發(fā)現(xiàn)過程
  16. iOS 內(nèi)存管理及優(yōu)化
  17. Memory Usage Performance Guidelines
  18. Performance Overview
  19. Debugging with Xcode
  20. Threading Programming Guide
  21. LLDB Quick Start Guide
  22. LLDB Debugging Guide
  23. Instruments Help Topics
  24. Advanced Memory Management Programming Guide
  25. Exception Programming Topics
  26. 小試Xcode逆向:app內(nèi)存監(jiān)控原理初探
  27. osfmk/kern/task.c
  28. MacOSX/MachTask.mm
  29. No pressure, Mon!

寫在最后

現(xiàn)在做 iOS 開發(fā)太難了

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

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