深入解析iOS內存 iOS Memory Deep Dive

Session 416 由三位蘋果軟件工程師 Kyle Howarth, James Snee, Kris Markel 為我們帶來 iOS 內存相關的一些內容

  • 在 Memory Usage Performance Guidelines 不再更新之後,這個 Session 簡單介紹了一下 iOS 的虛擬內存機制的變化,如 Compressed memory 的使用等,分析了開發者應該減少哪部分內存佔用。
  • Xcode 10 現在可以捕獲內存超限的 EXC_RESOURCE_EXCEPTION 事件,其底層記錄內存信息的 memgraph 文件與命令行工具的結合使用,使得內存相關的調試更加靈活高效。
  • 推薦開發者通過新的 API 讓系統選擇最佳的圖片渲染格式來合理使用內存;相比於UIImage的繪製,圖片下采樣時使用ImageIO來減少損耗。
  • 減少應用處在後臺時較大的內存佔用,主要通過監聽 App 生命週期的通知或利用VC的生命週期方法實現,使系統或其他進程獲得更多的可用內存。

1. Why Reduce Memory?

開門見山,我們為什麼要減少內存(佔用)?

為了更好的用戶體驗

內存是有限且系統共享的資源,一個程序佔用更多,系統和其他程序所能用的就更少。程序啟動前都需要先加載到內存中,並且在程序運行過程中的數據操作也需要佔用一定的內存資源。減少內存佔用也能同時減少其對 CPU 時間維度上的消耗,從而使不僅你所開發的 App,其他 App 以及整個系統也都能表現的更好。

2. Memory Footprint

我們需要明確的是,這裡的減少內存指減少 iOS App 的虛擬內存(Virtual Memory) 佔用。

iOS 以及 macOS 都採用了虛擬內存技術來突破物理內存(RAM) 的大小限制,每個進程都擁有一段由多個大小相同的 page 所構成的邏輯地址空間。處理器和內存管理單元 MMU(Memory Management Unit) 維護著由邏輯地址空間到物理地址的 page 映射表,當程序訪問邏輯內存地址時由 MMU 根據映射表將邏輯地址轉換為真實的物理地址。在早期的蘋果設備中,每個 page 的大小為 4KB;基於 A7 和 A8 處理器的系統為 64 位程序提供了 16KB 的虛擬內存分頁和 4KB 的物理內存分頁;而在A9之後,虛擬內存和物理內存的分頁大小都達到了 16KB。

虛擬內存分頁(Virtual Page, VP) 有兩種類型:

1.Clean - Data that can be paged out of memory

指的是能夠被系統清理出內存且在需要時能重新加載的數據,包括:

  • Memory mapped files
  • Frameworks 中的 __DATA_CONST 部分
  • 應用的二進制可執行文件

2.Dirty - Any memory that has been written to by your app

指的是不能被系統回收的內存佔用,包括

  • 所有堆上的對象
  • 圖片解碼緩衝數據(Decoded image buffers)
  • Frameworks 中的 __DATA 和 __DATA_DIRTY部分

Frameworks you link actually use clean memory and dirty memory

由於閃存容量和讀寫壽命的限制,iOS 上沒有Disk swap機制,取而代之使用 Compressed memory。

Disk swap 是指在 macOS 以及一些其他桌面操作系統中,當內存可用資源緊張時,系統將內存中的內容寫入磁盤中的backing store (Swapping out),並且在需要訪問時從磁盤中再讀入 RAM (Swapping in)。與大多數 UNIX 系統不同的是,macOS 沒有預先分配磁盤中的一部分作為 backing store,而是利用引導分區所有可用的磁盤空間。

蘋果最初只是公開了從 OS X Mavericks 開始使用 Compressed memory 技術,但 iOS 系統也從 iOS 7 開始悄悄地使用。從 OS X Mavericks Core Technology Overview 文檔中可以瞭解到該技術在內存緊張時能夠將最近使用過的內存佔用壓縮至原有大小的一半以下,並且能夠在需要時解壓複用。它在節省內存的同時提高了系統的響應速度,其特點可以歸結為:

  • Shrinks memory usage 減少了不活躍內存佔用
  • Improves power efficiency 改善電源效率,通過壓縮減少磁盤IO帶來的損耗
  • Minimizes CPU usage 壓縮/解壓十分迅速,能夠儘可能減少 CPU 的時間開銷
  • Is multicore aware 支持多核操作

本質上,Compressed memory 也是 Dirty memory

因此, memory footprint = dirty size + compressed size ,這也就是我們需要並且能夠嘗試去減少的內存佔用。

深入解析iOS內存 iOS Memory Deep Dive

當 memory footprint 超過一定值時(這裡給出了不同機型的測試結果),就會收到內存警告(Memory Warnings)。對於Extension來說,限制值更小,因此使用也需要更加謹慎。⚠️一些情況下,如果內存使用增長過快,App 有可能在尚未響應內存警告的情況下就已經被系統殺掉進程了。

Kyle 在這一部分給出了幾點關於內存警告的看法:

(1).你的 App 不一定是真正的“兇手”

在一些 RAM 容量較低的機型上,App 使用過程中接到一個電話,也有可能觸發內存警告。

(2).內存壓縮技術的存在使得釋放內存變得複雜

假設一個 App 的 Dirty memory 中有一個 NSDictionary 對象佔用了3個 page 的內存空間,當 App 處於非活躍狀態時系統將其壓縮至1個 page 的壓縮大小,系統獲得了2個 page 大小的可用內存。

深入解析iOS內存 iOS Memory Deep Dive

深入解析iOS內存 iOS Memory Deep Dive

但是,如果這時因為一些原因收到內存警告,我們可能會決定將 NSDictionary 中的一些數據移除,這時我們重新訪問了壓縮後的page,它被解壓 - 釋放對象 - 然後內存佔用又回到了1個page大小。也就是說,我們努力釋放了一些對象卻沒有增加可用內存空間,甚至可能會加劇內存緊張的態勢,也增加了 CPU 的時間開銷。

深入解析iOS內存 iOS Memory Deep Dive

(3).緩存策略

緩存選擇實際上是 CPU 和內存性能開銷的博弈,相比於使用字典緩存,Kyle 更推薦使用NSCache。NSCache 分配的內存實際上是 Purgeable Memory,可以由系統自動釋放。這點在 Effective Objective 2.0 一書中也有推薦,NSCache 與 NSPureableData 的結合使用既能讓系統根據情況回收內存,也可以在內存清理的同時移除相關對象。

3. Tools for profiling footprint

(1).為了更好尋找能夠減少的內存佔用,Xcode 和 Instruments 一直以來提供了一系列工具幫助我們進行 Debug:

  • Xcode memory gauge
  • 在 Xcode 的 Debug navigator 中可以通過 Xcode memory gauge 直接看到正在 debug 程序的內存佔用情況,以及其他程序佔用內存和系統總內存。為了查看更為詳細的內存佔用變化,可以使用 Instruments 相關工具。
  • Allocations
  • 追蹤程序的虛擬內存佔用和堆信息,提供對象的類名、大小以及調用棧等信息。
  • Leaks
  • 用於檢測程序運行過程中的內存洩露,並記錄對象的歷史信息。

在檢測內存洩露方面,三方庫 MLeaksFinder 較為流行,能夠不入侵代碼且不用打開 Instruments,自動檢測 UIViewController 和 UIView 對象的內存洩露,而且也可以擴展以檢測其它類型的對象。

  • VM Tracker
  • 能夠區分程序運行時前文所述的各種內存類型佔用情況,Instruments User Guide 中給出了各個參數的具體定義。
  • Virtual Memory Trace
  • 隱藏在 System Trace 中的 Virtual Memory Trace 工具能夠從 page 層面更深層次剖析應用程序的虛擬內存操作。 Syetem Trace in Depth-WWDC 2016 中給出了詳細的介紹。
深入解析iOS內存 iOS Memory Deep Dive

(2).在 Xcode 10 中,內存佔用觸發限制時,會有 EXC_RESOURCE_EXCEPTION 事件被捕捉到,繼而可以利用各種手段分析研究內存佔用情況,更有助於尋找問題根源。此外,從 Xcode 8 開始引入的 Debug memory graph 也更新了更好的佈局方式。

深入解析iOS內存 iOS Memory Deep Dive

(3).Xcode 使用 memgraph 的文件格式來儲存應用程序的佔用信息,因此導出 memgraph 文件可以結合命令行工具進行分析。能夠雖然可視化工具已經能夠直觀的表現我們想要了解的內存佔用信息,但是在終端中不僅可以靈活地利用各種命令和 flag 突出我們想要的內容,更可以快速的實現信息查找和文本化交互。這一部分蘋果工程師為我們介紹了4個常用命令:

深入解析iOS內存 iOS Memory Deep Dive

  • vmmap
深入解析iOS內存 iOS Memory Deep Dive

深入解析iOS內存 iOS Memory Deep Dive

vmmap 能夠打印出進程信息,所有分配給該進程的 VMRegions 以及 VMRegion 的種類、內存佔用信息等內容。利用 --summary 則能夠根據不同的 region type 打印出詳細的內存佔用類型和信息。這裡需要注意的是 SWAPPED SIZE 在 iOS 上指的是 Compressed memory size 且其值表示壓縮前的佔用大小。

系統中將一系列連續的內存頁關聯到一個 VMObject 進行管理,VMRegion 即 VMObject 所管理IDE區域。 Finding iOS Memory 中對每種 VMRegion 作出了詳細的解釋。

此外,結合 grep 和 awk 命令,還可以進行制定 VMRegion 的信息查找。例如,以下命令以 page 而非字節為單位打印 App 中所有動態庫所佔內存大小。

深入解析iOS內存 iOS Memory Deep Dive

深入解析iOS內存 iOS Memory Deep Dive

  • leaks
深入解析iOS內存 iOS Memory Deep Dive

深入解析iOS內存 iOS Memory Deep Dive

leaks 追蹤堆中的對象,打印出進程中內存洩露情況、調用堆棧以及循環引用信息。利用 --traceTree 和指定對象的地址,leaks還能以樹形結構打印出對象的相關引用。

深入解析iOS內存 iOS Memory Deep Dive

  • heap
深入解析iOS內存 iOS Memory Deep Dive

深入解析iOS內存 iOS Memory Deep Dive

heap 會打印出所有在堆上的對象信息,默認按類數量排序,也可以通過 -sortBySize 按大小排序,對於追蹤堆中較大的對象十分有幫助。找到目標對象後,通過 -address 獲得所有/指定類的地址,繼而可以利用 malloc_history 尋找其調用堆棧信息。

深入解析iOS內存 iOS Memory Deep Dive

  • malloc_history
深入解析iOS內存 iOS Memory Deep Dive

使用上述命令能夠獲得我們知道地址的對象的調用堆棧信息,它能夠得到的比 memory inspector 中 Backtrace 更加詳細。但是需要開啟 Dignostics 中的 Malloc Stack 選項,才能通過 malloc_history 獲得 memgraph 記錄的調用堆棧信息。

深入解析iOS內存 iOS Memory Deep Dive

  • To see object creation: malloc_history
  • To see what references an object in memory: leaks
  • To see how large a region or an instanceSize: heap & vmmap

接下來兩部分,蘋果工程師針對內存的使用給出了一些建議。

4. Images

圖片在內存使用上很容易產生較大的佔用,如下圖所示,一個圖片文件從硬盤到展示需要經歷加載-解碼-渲染三步。以一個590KB大小、2048 * 1536 像素的圖片為例,在3x設備上解碼後的內存佔用能夠達到10MB(2048 * 3 * 1536 * 3 * 4 Bytes/pixel)之多。更深層次的圖像相關實踐在 Image and Graphics Best Practices 中介紹,這裡我們需要知道:

Memory use of an image is related to the dimensions, not the file size in disk

深入解析iOS內存 iOS Memory Deep Dive

因此,在解碼圖片時要注意所選擇的圖片分辨率大小,對於一些分辨率過大的圖片,可以先進行下采樣降低分辨率再進行解碼渲染等。在 iOS 設備上支持四種圖片渲染格式,每種格式有著不同的 bitsPerComponent 和適用場景:

  • SRGB format :每個像素佔用 4 字節,分別表示紅、綠、藍通道以及 alpha 通道
  • Wide format:iOS 硬件設備支持的更生動的色域的渲染格式,每個通道佔用 2 字節,每像素佔用 8 字節。iOS 7 以上的設備可以拍攝這類照片,他們可以栩栩如生地還原美好。但是因為其較大的內存開銷需要謹慎使用
  • Luminance and alpha 8 format:每像素佔用 2 字節,分別表示灰度和透明度,適用於 Metal Apps 中的陰影等
  • Alpha 8 format:每像素只佔用 1 字節,單色,適用於如陰影、無emoji文字等
  • 那麼我們該如何選擇合適的渲染格式呢?

Don’t pick the format, let the format pick you

相比於總是使用默認的 SRGB 格式的 UIGraphicsBeginImageContextWithOptions 方法,Kyle 建議我們使用在 iOS 10 引入的 UIGraphicsImageRenderer 類完成繪製任務,它在 iOS 12 中會根據場景自動選擇最合適的渲染格式,更加合理地使用內存。UIGraphicsImageRenderer 可以創建 UIImage 對象或者進行 JPEG/PNG 格式的編碼。

此外,關於下采樣(downsampling),雖然上述 API 能夠合理使用渲染方案,但 UIImage 在修改圖片尺寸時的性能遜於 ImageIO。

  • UIImage 會首先把圖片解碼加載到內存,內部空間座標轉換也會帶來巨大損耗
深入解析iOS內存 iOS Memory Deep Dive

  • ImageIO 能夠在不產生 dirty memory 的情況下讀取到圖片尺寸和元信息,其內存損耗等於縮減後的圖片尺寸產生的內存佔用
深入解析iOS內存 iOS Memory Deep Dive

5. Optimizing when in background

最後Kyle給出了一點建議就是優化 App 的後臺相關行為,即在 App 進入後臺時釋放內存佔用較大的資源,進入前臺時重新加載。這裡的實現有兩種方式:

(1)App life cycle - 對於一些正在顯示的view對象,可以監聽 UIApplicationDidEnterBackground 和 UIApplicationDidEnterForeground 系統通知

深入解析iOS內存 iOS Memory Deep Dive

(2)UIViewController appearance cycle - 利用 VC 的生命週期方法,更適用於UITabBarController、UINavigationController 等有多個子vc的場景,因為你可能會有多個同一層級的 vc,但同一時間內又只有一個頁面在展示

深入解析iOS內存 iOS Memory Deep Dive

兩種方式都可以在用戶沒有感知的情況下減少後臺行為下的內存佔用,讓系統能夠獲得更多可用內存。除了蘋果工程師為我們提供的建議外,內存佔用也還有更多的優化可能。在對進行現有問題的追蹤優化基礎上,開發應用的過程中,我們更要注意對對象和文件的使用方式,避免引入顯而易見的內存問題。

參考

  • iOS Memory Deep Dive
  • Memory Usage Performance Guidelines
  • OS X Mavericks Core Technology Overview
  • Understanding iOS Memory
  • Instruments User Guide
  • iOS App Performance: Memory
  • Handling low memory conditions in iOS and Mavericks
  • Finding iOS Memory


分享到:


相關文章: