iOS Memory Deep Dive
前言
僅以此文解答自己大學以來多年對記憶體管理的疑惑。
經典作業系統的虛擬記憶體
為什麼要有虛擬記憶體?
隨著計算機的發展,我們的計算機處理的任務也變得越來越繁多,但是對於某臺固定的計算機,CPU 和 Memory 都是固定的,如果有些直接使用實體記憶體地址的話會帶來很多問題。首先編譯器不能以一種抽象的角度來描繪記憶體,在執行的過程中如果某個程序佔據的記憶體過大,這個程序可能就無法執行,即便運行了,記憶體相對來說是非常不安全的,一個不小心操作到了別的程序的記憶體,可能導致程序的崩潰,如果寫入了核心使用的記憶體可能導致作業系統的崩潰。
現代作業系統的記憶體管理是非常多電腦科學家智慧的結晶,這種管理方式就是 虛擬記憶體 (Virtual Memory/VM) 。VM 是一系列技術的總稱,包括硬體異常,實體地址,主存,磁碟檔案,作業系統核心軟體的記憶體管理。
虛擬記憶體提供了三大重要的特性:
-
它將主存看做在儲存在磁碟上的地址空間的快取記憶體,利用程式的區域性性原理,只將活躍的記憶體載入到主存中,提高了主存的利用率;
-
為每個程序提供了一個抽象的統一的連續的私有的地址空間。簡化了記憶體管理方式;
-
對記憶體進行分段(segment)提供許可權能力,保護每個程序的地址空間不會被其他程序影響;
定址方式
在一些早期的作業系統和一些嵌入式作業系統中,記憶體管理使用的地址是實體地址,現代作業系統基本使用的是 虛擬地址(Virtual Addressing)
的定址方式,使用 虛擬地址 時 CPU 將 VA
送到 MMU
中去翻譯為實體地址。
注: MMU (Memory Management Unit) 記憶體管理單元一般是一個 CPU 上的專用晶片,是一個硬體。結合作業系統共同完成地址翻譯工作。
地址空間
通常來說地址空間是 線性的 。假設我們有 {0, 1, 2, ..N-1 } 個記憶體地址,我們可以用 n 位二進位制來表示記憶體地址,那麼我們就叫這個地址空間為 n 位地址空間。現代作業系統通常是 32 或者 64(但是很多作業系統只用了48位定址)的。
2^10 = 1k 2^20 = 1M 2^30 = 1G 2^40 = 1T 2^50 = 1P 2^60 = 1E
這麼看來大家能理解為什麼 32位 的作業系統最大隻支援 4G 記憶體空間了。
分頁
現代作業系統將記憶體劃分為頁,來簡化記憶體管理,一個頁其實就是一段連續的記憶體地址的集合,通常有 4k 和 16k(iOS 64位是16K)的,成為 Virtual Page
虛擬頁。與之對應的實體記憶體被稱為 Physical Page
物理頁。
注意:虛擬頁的個數可能和物理頁個數不一樣。比如說一個 64 位作業系統中,使用 48 位地址空間的虛擬頁大小為 16K,那麼其虛擬頁數可達到(2^48/2^14 = 16M個),假設實體記憶體只有 4G,那麼物理頁可能只有 (2^32/2^14 = 256k個)
作業系統將虛擬頁和物理頁的對映關係稱為 頁表 (Page Table),每個對映叫 頁表條目 (Page Table Entry/Item),作業系統為每個程序提供一個頁表放在主存中,CPU 在使用虛擬地址時交給 MMU 去翻譯地址,MMU 去查詢在主存中的頁表來翻譯。
缺頁處理
每個 Page Table Entry
都包含了一些描述資訊,比如當前頁的狀態{未分配,快取的,未快取的}。
-
未分配的不用多說代表未使用的記憶體;
-
快取的代表已經載入進實體記憶體了;
-
未快取的代表還沒放在實體記憶體。
當 CPU 要讀取一個頁時,檢查標記發現當前的頁是未快取的,會觸發一個(Page Falut) 缺頁中斷 ,這時核心、作業系統的缺頁異常處理程式,去選擇一個犧牲頁(有時候記憶體夠用不用置換別的介面),然後檢查這個頁面是否有修改,有修改會先寫磁碟,然後將需要使用到的記憶體載入到實體記憶體中,然後更新 PTE
,隨後作業系統重新把虛擬地址傳送到地址翻譯硬體去重新處理。
注:有些作業系統無虛擬虛擬記憶體置換邏輯,如 iOS,取而代之的是記憶體壓縮和收到記憶體警告時殺死程序的行為。
虛擬記憶體帶來的好處
-
簡化連結過程,允許每個程序都提供統一的記憶體地址的抽象,獨立於實體記憶體;
-
簡化載入,作業系統載入可執行檔案和共享檔案時,只是建立了 頁表 ,待訪問到缺頁時,作業系統再去載入;
-
簡化共享,不同程序的 PT 中的 PTE 可以執行相同的實體地址,如動態庫的程式碼;
-
記憶體保護,PT 中的 PTE 中描述了一個虛擬頁的許可權資訊,(R, W, X),指令如果違反了這些許可權資訊,就會造成 Segment Fault
地址翻譯
虛擬地址翻譯到實體地址是軟硬體結合實現的。我們通常幾個方面來描述。
如何索引
現代作業系統將地址分為兩部分, 頁號 和 片 了(是不是很型別網路號和主機號),由於虛擬頁和物理頁的大小是相同的,頁偏移可以看做虛擬頁和物理頁的頁內地址,且相同;頁號則做為 PT 的索引查詢到對應的 PTE,然後查詢對應的物理頁地址。
提高效率
是不是像前面所說的簡單的劃分為兩部分就足夠了呢?
舉個例子:
-
我們假設一臺電腦是 32 位的,分頁大小位 4k,也就說頁內地址佔據了 12 位,頁號地址位 20 位;
-
我們假設一臺電腦是 64 位的,地址空間 48 位,分頁大小為 16k,也就說頁內地址佔據了 14 位,頁號地址位 34 位
我們粗略估算一個 PTE 為 4KB,對於 32位 的作業系統每個程序的頁表需要 2^20 = 4M 個頁表項常駐記憶體尚可接受,但是對於定址為 48位 的作業系統來說,每個程序的頁表為需要 2^32 = 4G 個頁表項,這是無法接受的。
計算機的世界所有的難題都可以用多加一層的辦法來解決,所以現代作業系統通常都使用多級頁表,減少頁表項的個數。將虛擬地址分為多端,代表了一級、二級、多級頁表。通過多級頁表可以大大減少記憶體佔用。
減少記憶體
眾所周知 CPU 要比 Memory 快 10^3
個數量級,即便 CPU 中的 L3Cache
也比 Memory 快很多,如果 MMU 的地址翻譯都要去查詢多級 PT,這個開銷就會非常巨大,但是所幸程式的區域性性原理能夠解救我們。MMU 晶片內建一個 翻譯後備緩衝器 (Transalation Lookaside Buffer TLB )的硬體來充當快取,加快地址翻譯的效率.
現代 OS 虛擬記憶體系統
作業系統為每個程序維護一個單獨的虛擬地址空間,分為兩部分:
-
核心虛擬記憶體:包含核心中的程式碼和資料結構,還有一些被對映到所有程序共享的記憶體頁面。還有一些頁表,核心在程序上下文中執行程式碼使用的棧。
-
程序虛擬記憶體:OS 將記憶體組織為一些區域(Segment)的集合,程式碼端,資料端,共享庫端,執行緒棧都是不同的區域,分段的原因是便於管理記憶體的許可權,如果瞭解過 Mach-O 檔案或者 ELF 檔案的讀者可以看到相同的 Segment 裡面的記憶體許可權是相同的,每個 Segment 再劃分不同的內容為 section。
在核心中描述一個程序的資料結構概略為如下
pgb 指向第一級頁表的基址
每個區域的描述主要有以下幾個
-
vm_start指向這個區域的起始處
-
vm_end指向這個區域的結束出
-
vm_prot記憶體區域的讀寫許可權
-
vm_flasg一些標誌位 私有的還是共享的
-
vm_next指向下一個 vm_area_struct 的描述
記憶體對映 MMAP
類 Unix 作業系統可以對映一個普通磁碟上的檔案的連續部分到一個固定的記憶體區域。作業系統會自動管理對映的內容。
記憶體對映允許不同的程序對映不同的虛擬記憶體到同一塊物理內容上,他們可以是共享的也可以是私有的。
對於共享的,通常多個程序對映到相同的共享物件上。對與私有的,不同程序初始對映的時候作業系統為了節省資源,並沒有產生真的副本,直到某個程序修改了這個私有物件,作業系統運用 copy on write
技術在此時才發生真正的檔案拷貝。
mmap 在類 unix 作業系統上作為一個系統呼叫存在,函式簽名如下
void * mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); addr 代表要從那塊虛擬地址開始對映,通常可以不用指定傳遞NULL讓作業系統自己給我們選擇 len 對映多少長度的內容 prot 對映檔案的訪問許可權 讀寫可執行許可權等 PROT_EXEFC 可執行許可權 PROT_READ 可讀許可權 PROT_WRITE 可寫許可權 PROT_NONE 無法訪問許可權 flags 訪問檔案的標記 MAP_SHARED 共享的 MAP_PRIVATE私有的 MAP_ANON 私有的
舉個例子將任意檔案對映到 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 中的用處
-
mmap 讓讀寫一個檔案像操作一個記憶體地址一樣簡單方便;
-
mmap 效率極高,不用將一個內容從磁碟讀入核心態再拷貝至使用者態;
-
mmap 對映的檔案由作業系統接管,如果程序 Crash 作業系統會保證檔案重新整理回磁碟;
動態記憶體分配
雖然可以使用上面的低階 API 去對映記憶體,但是需要動態申請記憶體用來做變數處理的時候就需要 動態記憶體分配器 (Dunamic memory allocator),簡單理解為 malloc calloc realloc free
等函式來自的庫就稱為 DMA
。動態記憶體分配器將一個記憶體的區域(Heao)分為不同的大小的塊(block),這些塊要不然就是分配的,要不然就是空閒的。
如何設計分配器又是一個大難題。 幾乎所有的計算機語言都採用以下兩種:
-
顯式分配器(手動管理內容)
-
隱式分配器(GC)
隱式記憶體分配器
通常比較知名的語言 Java javaScript Ruby
等都使用 GC,最早的 GC 只是使用標記清除演算法來管理內容,通過幾十年的迭代,早已更新出了數種演算法共同參與的 GC。這裡就不再贅述了。
顯式記憶體分配器
C 語言提供了一些列的方法來管理動態記憶體。如
-
malloc申請內容並返回初始化的記憶體首地址;
-
calloc同 malloc 一致,並且會將申請到的記憶體全置為0;
-
realloc重新分配原本已經申請的記憶體空間;
-
free釋放內容空間;
-
sbrk擴充套件收縮堆
如何實現一個自己的顯式記憶體分配器
首先我們要明確記憶體分配器的需求
-
處理任意順序的申請記憶體和釋放記憶體;
-
立即響應,不應為了效能而重新排列或者快取請求;
-
所有內容都在 heap 裡存放;
-
對齊塊,使之可以存放任意型別的資料;
-
不修改已分配的記憶體塊;
鑑於對齊和處理任意順序記憶體管理的需求,堆利用效率可能會降低,主要會產生記憶體碎片(Fragmentation) 。記憶體碎片分為兩種:
-
內部碎片:通常是指一個分配過的塊資料並不是全部塊的內容,通常有元資訊,對齊的位元組等;
-
外部碎片:是指不連續的可用的塊,通常外部碎片過多會產生所有空白塊相加可以滿足申請的資源,但是他們不連續。需要整理碎片。
實現顯式記憶體分配器的重點
-
空閒塊組織
-
如何分配新申請的塊
-
如何組織空閒快的剩餘部分
-
如何合併剛釋放的塊
顯式記憶體分配器的實現方案
隱式空閒連結串列
這種方式在 malloc 申請記憶體的時候,實際上申請的是實際所需記憶體加上部門元資訊大小的塊,然後返回指標是有效資料的首地址,元資訊直接存在資料塊中,所以稱為隱式空閒連結串列。
隱式連結串列需要處理如何分割空閒塊和合並空閒塊。
顯式空閒連結串列
由於隱式空閒連結串列的搜尋效率較低,其實是不適用通用的記憶體分配的。可以使用某種形式的資料結構去管理這些記憶體塊。基本分為幾種:
-
簡單分離器儲存
-
分離適配法
-
夥伴系統法
關於詳細的設計需要讀者檢視更多演算法知識的文件。
顯式記憶體分配器的實現
顯式記憶體分配器的需求已經很清晰,下面有個簡單的例子可以參考,這時候對於 C 類語言的記憶體管理應該不會太過恐懼了,
-
C++實現一個簡易的記憶體池分配器https://blog.csdn.net/oyoung_2012/article/details/78874869
畢竟原始碼面前了無祕密。
iOS的虛擬記憶體
iOS 記憶體的分頁大小
在 arm64 之後的晶片,作業系統通常使用 16KB 作為頁大小,我們寫的程式中的虛擬記憶體地址右移動 14位 則可得到頁編號。MMU 通過 TLB 和固定在記憶體程序虛擬區域的頁表來翻譯來實體地址。
下面一份程式碼可以獲取頁大小。
int main(int argc, char * argv[]) { // 獲取虛擬記憶體分頁資料 14為頁內地址 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); // 編譯時確定不建議使用 return 0; }
在觀察 Crash 日誌的時候,有時候注意崩潰的頁號可以幫助我們尋找崩潰的原因。
頁面的型別
當作業系統分配一個頁面時,記憶體被稱為 Clean
的,表示這個記憶體頁面沒有使用,是可以被釋放或者重建的,但是一旦寫入,作業系統會將其標記為 Dirty
,這意味著磁碟或者其他地方沒有此記憶體頁面的備份,無法恢復它。
由於 iPhone 裝置為了減少快閃記憶體的壽命,並沒有在快閃記憶體上使用交換分割槽,因此無論使用多少,在記憶體壓力高緊時,作業系統不會將 Dirty 寫好磁碟,而是釋放 Clean 的頁面。如可執行程式碼(Mach-O)的對映和記憶體對映檔案,或者是 kill 掉程序。
因此使用 dirty 的記憶體越多,對我們的程序的穩定性越差。
iOS 記憶體的優化
在其他常見的作業系統上,由於區域性性原理,OS 會將不常用的記憶體頁面寫入磁碟,但是 iOS 沒有交換空間,取而代之的是記憶體壓縮技術。iOS 將不常用到的 dirty 頁面壓縮以減少頁面佔用量,在再次訪問到的時候重新解壓縮。這些都在作業系統層面實現,對程序無感知,有趣的是如果當前程序收到了 memoryWarning
,程序這時候準備釋放大量的誤用記憶體,如果訪問到過多的壓縮記憶體,再解壓縮記憶體的時候反而會導致記憶體壓力更大,然後被 OS kill 掉。
iOS 程序中的堆和棧
需要注意的是通常作業系統書籍中描述的程序虛擬記憶體模型都是這樣的
Process Virtual Memory
這實際是個用於解析給讀者的簡化模型,對於多執行緒程式來說,每個執行緒都有自己的執行緒棧。在iOS上通常主執行緒執行緒棧大小為 1MB,子執行緒棧大小為 512KB,如果你有一臺越獄機,可以試驗 ulimt -a
命令觀察棧大小的預設引數。
iOS平臺上的常見程式語言的記憶體管理方式
iOS 上常用的 Swift、Objective-C、C、C++ 都使用顯式的記憶體管理策略,比如 malloc 和 free,new 和 delete,alloc 和 dealloc。在 Objective-C 和 Swift 通常使用一種叫做引用計數的簡化模型來管理堆記憶體。現代 Clang 已經支援 ARC 的技術幫助程式設計師解脫記憶體管理的困擾,但是本質上還是顯式記憶體管理。
建議讀者可以讀一下 ARC的參考文件
http://clang.llvm.org/docs/AutomaticReferenceCounting.html
順便提一下 Xcode10 版本中的 Clang 已經支援在 C 結構體中對於 Objective-C 物件的 ARC 管理,請參看 whats_new_in_llvm
記憶體分類
要想合理的使用記憶體,必須要掌握不同型別記憶體的區別,才能更合理的使用記憶體並且在記憶體資源匱乏的低端機器上寫出“高記憶體效能”的應用。
首先在 Apple 的官方文件中記憶體主要分為以下幾類。
-
Free Memory當前空閒的memory
-
Used Mamory當前正在使用的記憶體
我們最關心的當然是 Used Memory,它又分為以下幾類。
-
Wired Memory:一般是核心佔用的常駐記憶體,比如可執行檔案的映象 Image,核心所有的資料等,無法釋放,在OS執行期間必須常駐記憶體;
-
Active Memory活躍的記憶體,當前正在使用的記憶體;
-
Inactive Memory不活躍的記憶體,最近用過,但是現在不怎麼用了,按照區域性性原則可以被置換出實體記憶體的記憶體;
-
Purgeable Memory 可釋放的記憶體,通常在 Foundation 中是
NSDiscardableContent
的子類,或者是NSCache
等。
等等~。上面說的好像跟沒說一樣/(ㄒoㄒ)/~~。我們換種方式從實體記憶體和虛擬記憶體的層面來解釋。
首先我們的虛擬記憶體使用的是 Page 來描述的。一個 Page 有兩種狀態 Dirty 和 Clean。在 iOS 中 Clean 是可以被回收的。
Virtual Memory 分類
-
Clean Memory主要包括 system framework、binary executable 、memory mapped files
-
Dirty Memory包括 Heap allocation、caches、decompressed images 等。
(每個程序擁有一份獨立的 Virtual memory pace) Virtual Memory = clean Memory
PhySical Memory
實體記憶體是指真正載入在主存中的記憶體,所以實際瞭解真正的實體記憶體佔用才對我們記憶體管理幫助更大。
-
DirtyMemory
-
Clean Memory but loaded。
-
Page Table
-
ComPressed memory
-
IOKit Used
-
Purgeable
記憶體測量工具
瞭解到前面說的記憶體分類之後我們應該怎麼測量我們的記憶體分佈呢。主要有幾種工具:命令列工具、Xcode工具、程式碼工具等。
命令列工具
如果你開發的是 Mac 程式,Mac OS 自帶的有一下幾種。
-
top 程式
-
heap 程式
-
leaks 程式
-
vmmap 程式
這些工具讀者檢視 Man Page 即可。
需要注意的是。以上工具分析的大多是虛擬記憶體,也就是說對於桌面級程式更適合,但是對於 iOS 中沒有交換空間,且擁有 Jetsam 監控程式的裝置,可能還需要更精準的測量工具。
順便提一句。一個堆區上 malloc 的程式如果並沒有使用,雖然它是 Clean 的,但是也會被程式統計到。理論上 malloc 可以申請到的虛擬記憶體大小非常接近 Virtual Memory Space
的大小(這麼說的原因是 前文也提到了 malloc 實際上是動態分配器程式提供的一些列函式,為了效能,大多數動態分配器都講堆分為好幾塊用來做不同大小虛擬記憶體的管理,因此malloc可以申請到的虛擬記憶體大小實際決定於動分配器程式碼的實現。有興趣的讀者可以讀一下。)
Xcode 提供的工具
-
Xcode Debug Area
-
Instruments
-
DebugMemoryGraph
Memory Report
instruments
DebugMemoryGraph
Scheme
Tips:配置了 MallocStackLogging
的話甚至可以追蹤每個虛擬記憶體中的物件申請堆疊,便於我們更好的發現問題。
注意點:所有Xcode提供的工具必須使用真機測試才能最難接近使用者的使用環境
程式碼工具
我們通過開發工具可以用來測量我們的記憶體,但是到了線上這些都用不了,能精準的測量 APP 用到的實體記憶體才比較重要。
大部分的程式碼測量記憶體是通過拿到 Mach 核心提供的 task_info
來測量的,但是這個資訊更多的是虛擬記憶體層面的資訊,不能正確的衡量實體記憶體。
#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 的差距較大,有時候可能會偏差 50M-100M
,於是有大佬拔出了 Xcode 的 DebugServer
和 WebKit
中的的實體記憶體計算方式(2018WWDC 蘋果也說了 footPrint才是真正的實體記憶體使用 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); }
線上檢查工具
線上檢查記憶體通常會檢查記憶體洩漏,一般有開源的工具
-
MLeaksFinder
-
FBRetainCycleDetector
高效能使用記憶體
瞭解完那麼多原理和分析的工具,那麼在日常使用中有沒有什麼指導原則可以幫助我們來寫出更快,記憶體佔用更低的程式碼呢?
-
首先熟讀
ARCMenual
,大部分 iOS 開發者其實是完全不清楚 ARC 是怎麼實現的,還有相對於的原則,尤其是 Autorelease 修飾的指標,還有在多執行緒情況下的原則。 -
用
weak
修飾替換unsafe_unretain
-
使用
weak strong dance
來解決 block 中的迴圈引用問題。需要注意的是大部分人都以為使用了 weak 指標就可以了。其實不然,在block 內必須使用 strong 重新繫結變數,避免在多執行緒情況下weak
變數為空導致 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 }]; }]; }
-
小心方法中的self,在 Objective-C的方法中 隱含的 self 是
__unsafed_unretain
的; -
使用
Autoreleasepool
來降低迴圈中的記憶體峰值,避免 OOM; -
要處理
Memory Warning
; -
C/C++ new 出來的要 delete,malloc 的要 free;
-
UITableView/UICollectionView 的重用(不單單是cell重用,cell 使用的子view也要重用。);
-
[UIImage imageNamed:] 適合於 UI 介面中的貼圖的讀取,較大的資原始檔應該儘量避免使用;
-
WKWebView 是跨程序通訊的,不會佔用我們的 APP 使用的實體記憶體量;
-
try_catch_finally
一定要清理資源; -
儘量少引用
performaSelector:
會對 ARC 的記憶體管理產生錯誤,導致記憶體洩漏; -
lazy load 那些大的記憶體物件,尤其是需要保證執行緒安全,可以參考 java 中的懶漢式
Double Check
寫法; -
需要在收到記憶體警告的時候釋放的 Cache,用 NSCache 代替 NSDictionary,使用 NSPurgableData 代替NSData.
前文中我們說到 iOS 的沒有交換分割槽的概念,取而代之的是壓縮記憶體的辦法,倘若在使用 NSDictionary 的時候收到記憶體警告,然後去釋放這個NSDictionary,如果佔據的記憶體過大,很可能在解壓的過程中就被 JetSem Kill 掉,如果你的記憶體只是誤用的快取或者是可重建的資料,就把 NSCache 當初 NSDictionary 用吧。同理 NSPurableData 也是。
-
不要使用畫素過大的圖片檔案,即便一個圖片在磁碟中很小,但是因為圖片畫素寬高很大也會佔據更多的記憶體,這裡有個公式可以計算
widthPx * HeightPx * 4Bytes per pixel(alpha red green blue)
。即便在 iOS 12 中已經可以優化單色圖的記憶體佔用,可畢竟是 iOS 12,現在好多公司還在支援iOS8 ~~ -
使用 NSData 和 UIImage 的 mmap 載入選型來載入那些可以被重建的資料;
-
在子執行緒手動申請(maloc)大記憶體的的時候 ping 一下主執行緒,因為子執行緒無法收到記憶體警告的傳遞;
- (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); }
參考
-
深入理解計算機系統
https://item.jd.com/12006637.html -
高效能iOS應用開發
https://item.jd.com/12173816.html -
iOS和macOS效能優化:Cocoa、Cocoa Touch、Objective-C和Swift
https://item.jd.com/12385127.html -
WWDC iOS Memory Deep Dive
https://devstreaming-cdn.apple.com/videos/wwdc/2018/416n2fmzz0fz88f/416/416_ios_memory_deep_dive.pdf -
C++實現一個簡易的記憶體池分配器
https://blog.csdn.net/oyoung_2012/article/details/78874869 -
ARC的參考文件
http://clang.llvm.org/docs/AutomaticReferenceCounting.html -
whats_new_in_llvm
https://devstreaming-cdn.apple.com/videos/wwdc/2018/409t8zw7rumablsh/409/409_whats_new_in_llvm.pdf -
先弄清楚這裡的學問,再來談 iOS 記憶體管理與優化(一)
https://www.jianshu.com/p/deab6550553a -
先弄清楚這裡的學問,再來談 iOS 記憶體管理與優化(二)
https://www.jianshu.com/p/f95b9bfda4a0 -
讓人懵逼的 iOS 系統記憶體分配問題
https://www.jianshu.com/p/fcbb9a472633 -
探索iOS記憶體分配
https://www.jianshu.com/p/553f34b03624 -
iOS記憶體深入探索之VM Tracker
https://www.jianshu.com/p/f82e2b378455 -
iOS記憶體深入探索之Leaks
https://www.jianshu.com/p/12cadd05e370 -
iOS記憶體深入探索之記憶體用量
https://www.jianshu.com/p/827996b7aed0 -
iOS筆記-記錄一次記憶體洩漏發現過程
https://www.jianshu.com/p/006f4e3202fb -
iOS 記憶體管理及優化
https://www.imooc.com/video/11076 -
Memory Usage Performance Guidelines
https://developer.apple.com/library/archive/documentation/Performance/Conceptual/ManagingMemory/Articles/Articles/MemoryAlloc.html#//apple_ref/doc/uid/20001881-CJBCFDGA -
Performance Overview
https://developer.apple.com/library/archive/documentation/Performance/Conceptual/PerformanceOverview/Introduction/Introduction.html#//apple_ref/doc/uid/TP40001410-CH202-SW1 -
Debugging with Xcode
https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/debugging_with_xcode/chapters/about_debugging_w_xcode.html -
Threading Programming Guide
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/Introduction/Introduction.html -
LLDB Quick Start Guide
https://developer.apple.com/library/archive/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide/document/Introduction.html -
LLDB Debugging Guide
https://developer.apple.com/library/archive/documentation/General/Conceptual/lldb-guide/chapters/Introduction.html -
Instruments Help Topics
https://developer.apple.com/library/archive/documentation/AnalysisTools/Conceptual/instruments_help-collection/Chapter/Chapter.html -
Advanced Memory Management Programming Guide
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html -
Exception Programming Topics
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Exceptions/Tasks/HandlingExceptions.html -
小試Xcode逆向:app記憶體監控原理初探
http://ddrccw.github.io/2017/12/30/2017-12-30-reverse-xcode-with-lldb-and-hopper-disassembler/ -
osfmk/kern/task.c
https://raw.githubusercontent.com/apple/darwin-xnu/master/osfmk/kern/task.c -
MacOSX/MachTask.mm
https://github.com/llvm-mirror/lldb/blob/master/tools/debugserver/source/MacOSX/MachTask.mm -
No pressure, Mon!
http://www.newosxbook.com/articles/MemoryPressure.html
寫在最後
現在做 iOS 開發太難了