WWDC 2018:iOS 記憶體深入研究

知識小集發表於2018-06-16

WWDC 2018 Session 416:iOS Memory Deep Dive

檢視更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄

作者:高老師,純潔善良有理想的 iOS 開發一枚

引言

對於我們的 App 所依賴的裝置而言,記憶體資源是有限的。降低 App 所使用的記憶體可以提高效能和體驗,相反,過大的記憶體佔用可能會導致 App 被系統強制退出。所以每個 iOS 開發者都應該關注記憶體問題。這一節新的內容不多,基本上都是一些老的知識點。

按照 Session 的套路,我們先看一下提綱:

  • 為什麼要減少記憶體使用
  • 記憶體佔用
  • 分析記憶體佔用工具
  • 影像
  • 在後臺時,對記憶體優化
  • 演示 Demo

那我們就按順序開始啦!

為什麼要減少記憶體

在探討記憶體之前,我們要知道為什麼要減少記憶體。簡單的回答是可以有更好的使用者體驗:更快的啟動速度,不會因為記憶體過大而導致 Crash,可以讓 App 存活更久等。

記憶體佔用

並非所有 App 的記憶體佔用都是相同的。在繼續探討 iOS 上 App 的記憶體使用之前,我們先來聊一下Pages Memory

Pages Memory

記憶體是由系統管理,一般以頁為單位來劃分。在 iOS 上,每一頁包含 16KB 的空間。一段資料可能會佔用多頁記憶體,所佔用頁總數乘以每頁空間得到的就是這段資料使用的總記憶體。

WWDC 2018:iOS 記憶體深入研究

記憶體頁按照各自的分配和使用狀態,可以被分為 CleanDirty 兩類。

WWDC 2018:iOS 記憶體深入研究

以上面的程式碼為例,申請一塊長度為 80000 位元組的記憶體空間,按照一頁 16KB 來計算,就需要 6 頁記憶體來儲存。

  • 當這些記憶體頁開闢出來的時候,它們都是 Clean
  • 當向處於第一頁的記憶體寫入資料時,第一頁記憶體會變成 Dirty
  • 當向處於最後一頁的記憶體寫入資料時,這一頁也會變成 Dirty

WWDC 2018:iOS 記憶體深入研究

記憶體對映檔案

當 App 訪問一個檔案時,系統核心會負責排程,將磁碟上的檔案載入並對映到記憶體中。如果這是隻讀的檔案,它所佔用到的記憶體頁是 Clean 的。

如下圖所示,一個 50KB 的圖片被載入到記憶體中時,需要分配 4 頁記憶體來儲存。其中第四頁中有 2KB 的空間會被用來儲存這個圖片的資料,剩餘空間可能會被用來儲存其它資料。

WWDC 2018:iOS 記憶體深入研究

典型app記憶體型別

當記憶體不足的時候,系統會按照一定策略來騰出更多空間供使用,比較常見的做法是將一部分低優先順序的資料挪到磁碟上,這個操作稱為 Page Out。之後當再次訪問到這塊資料的時候,系統會負責將它重新搬回記憶體空間中,這個操作稱為 Page In

然而對於移動裝置而言,頻繁對磁碟進行IO操作會降低儲存裝置的壽命。從 iOS7 開始,系統開始採用壓縮記憶體的辦法來釋放記憶體空間,被壓縮的記憶體稱為 Compressed Memory。下面依次介紹一下 iOS App 通常情況下的三種記憶體型別:Clean MemoryDirty Memory以及Compressed Memory

Clean Memory

Clean Memory 是指那些可以用以 Page Out 的記憶體,包括已被載入到記憶體中的檔案,或者是 App 所用到的 frameworks。每個 frameworks 都有 _DATA_CONST 段,當 App 在執行時使用到了某個 framework,它所對應的 _DATA_CONST 的記憶體就會由 Clean 變為 Dirty。

Dirty Memory

Dirty Memory 是指那些被 App 寫入過資料的記憶體,包括所有堆區的物件、影像解碼緩衝區,同時,類似 Clean memory,也包括 App 所用到的 frameworks。每個 framework 都會有 _DATA 段和 _DATA_DIRTY 段,它們的記憶體是 Dirty 的。

值得注意的是,在使用 framework 的過程中會產生 Dirty Memory,使用單例或者全域性初始化方法是減少 Dirty Memory 不錯的方法,因為單例一旦建立就不會銷燬,全域性初始化方法會在 class 載入時執行。

Compressed Memory

當記憶體吃緊的時候,系統會將不使用的記憶體進行壓縮,直到下一次訪問的時候進行解壓。

例如,當我們使用 Dictionary 去快取資料的時候,假設現在已經使用了 3 頁記憶體,當不訪問的時候可能會被壓縮為 1 頁,再次使用到時候又會解壓成 3 頁。

Memory Warnings

並非所有記憶體警告都是由 App 造成的,例如在記憶體較小的裝置上,當你接聽電話的時候也有可能發生記憶體警告。按照以往的習慣,你可能會在收到記憶體警告通知的時候去做一些釋放記憶體的事情。然而記憶體壓縮機制會使事情變得複雜。我們來看看這個例子:

WWDC 2018:iOS 記憶體深入研究

假設程式碼中的 cache 已被壓縮過

WWDC 2018:iOS 記憶體深入研究

事實上,當你嘗試去再次訪問 cache 物件的時候,系統會先解壓這塊記憶體

WWDC 2018:iOS 記憶體深入研究

這個過程中記憶體使用會增加,在記憶體吃緊的時候,這並不是我們想要的。隨後,當我們會執行大量工作去清空 cache,最終得到的記憶體空間和記憶體壓縮的結果一樣

WWDC 2018:iOS 記憶體深入研究

所以,相比以往的快取手段,更加建議去調整策略,例如減少快取使用,或者在收到記憶體警告的時候,將這類事情交由系統去處理。

Caching

我們對資料進行快取的目的是想減少 CPU 的壓力,但是過多的快取又會佔用過大的記憶體。由於記憶體壓縮機制的存在,我們需要根據快取資料大小以及重算這些資料的成本,在 CPU 和記憶體之間進行權衡。

在一些需要快取資料的場景下,可以考慮使用 NSCache 代替 NSDictionary,因為 NSCache 可以自動清理記憶體,在記憶體吃緊的時候會更加合理。

小結

通常情況下,我們所說的記憶體佔用是指 Dirty MemoryCompressed MemoryClean Memory 不需要過多關心。

WWDC 2018:iOS 記憶體深入研究

App 能使用比較多的記憶體空間,但是上限會根據裝置不同而不同。Extension 能使用的最大記憶體則要低很多,所以當你在開發 Extension 的時候尤其要注意記憶體使用。當使用的記憶體超出限制的時候,系統會丟擲 EXC_RESOURCE_EXCEPTION 異常。

分析記憶體佔用工具

Xcode Memory Gauge

WWDC 2018:iOS 記憶體深入研究

在 Xcode 中,你可以通過 Memory Gauge 工具,很方便快速的檢視 App 執行時的記憶體情況,包括記憶體最高佔用、最低佔用,以及在所有程式中的佔用比例等。如果想要檢視更詳細的資料,就需要用到 Instruments 了。

Instruments

WWDC 2018:iOS 記憶體深入研究

Instruments 中,你可以使用 AllocationsLeaksVM TrackerVirtual Memory Trace 對 App 進行多維度分析。

Debug Debugger-Memory Resource Exceptions

當你使用 Xcode 10 以前的版本進行除錯時,在記憶體過大時,debug session 會直接終止,並且在控制檯列印出異常。從 Xcode 10 開始,debugger 會自動捕獲 EXC_RESOURCE RESOURCE_TYPE_MEMORY 異常,並斷點在觸發異常丟擲的地方,十分方便定位問題。

WWDC 2018:iOS 記憶體深入研究

Xcode Memory Debugger

WWDC 2018:iOS 記憶體深入研究

通過這個工具,可以很直觀地檢視記憶體中所有物件的記憶體使用情況,以及相互之間的依賴關係,對定位那些因為迴圈引用導致的記憶體洩露問題十分有幫助。

你也可以點選 File->Export Memory Graph 將其匯出為 memgraph 檔案,在命令列中使用 Developer Tool 對其進行分析。使用這種方式,你可以在任何時候對過去某時的 App 記憶體使用進行分析。

簡單介紹一下相關的命令

vmmap - 檢視虛擬記憶體

WWDC 2018:iOS 記憶體深入研究

檢視詳細報告

vmmap xx.memgraph

檢視摘要報告

vmmap --summary xx.memgraph

配合管道命令檢視所有動態庫的Ditry Pages的總和

vmmap -pages xxx.memgraph | grep '.dylib' | awk '{sum += $6} END { print "Total Dirty Pages:"sum}'

只顯示CG image相關的資料

vmmap xx.memgraph | grep 'CG image'

更多使用方式請檢視vmmap的文件

man vmmap

leaks - 檢視洩漏的記憶體

WWDC 2018:iOS 記憶體深入研究

檢視是否有記憶體洩露

leaks xx.memgraph

檢視某處記憶體的洩漏

leaks --traceTree [記憶體地址] xx.memgraph

更多使用方式請檢視 leaks 的文件

man leaks

heap - 檢視堆區記憶體

WWDC 2018:iOS 記憶體深入研究

檢視所有堆區物件的記憶體使用

heap xx.memgraph

預設情況下是按照物件數量進行排序,通常情況下它們不會造成什麼記憶體問題。我們需要關心的是那些為數不多,卻佔用了大量記憶體的物件,這時候就可以增加引數 -sortBySize,按照記憶體佔用大小順序來檢視所有堆區物件的記憶體使用

heap xx.memgraph -sortBySize

當確定是哪個型別的物件佔用了太多記憶體之後,可以得到每個物件的記憶體地址

heap xx.memgraph -addresses all | 'XXBigData'

更多使用方式請檢視 heap 的文件

man heap

有了這些物件的記憶體地址之後,我們還需要另一樣工具幫助我們做下一步分析。

Enabling Malloc Stack Logging

Product -> Scheme -> Edit Scheme -> Diagnostics 中,開啟 Malloc Stack 功能,建議使用 Live Allocations Only 選項

WWDC 2018:iOS 記憶體深入研究

之後 lldb 會記錄除錯過程中物件建立的堆疊,配合 malloc_history 工具,就可以定位到那些佔用了過大記憶體的物件是哪裡建立的。

malloc_history - 檢視記憶體分配歷史

WWDC 2018:iOS 記憶體深入研究

malloc_history xx.memgraph [address]

malloc_history xx.memgraph --fullStacks [address]

更多使用方式請檢視 malloc_history 的文件

man malloc_history

選擇哪個工具?

上面講述了那麼多的分析工具,那我們應該選擇哪種工具呢?蘋果的工程師幫我們做了如下整理:

WWDC 2018:iOS 記憶體深入研究

大家可以根據上圖所示,根據不同的需要進行選擇。

圖片

對於 iOS 系統而言,絕大部分場景下哪類資料佔記憶體最多呢?當然是圖片!需要注意的是,圖片所佔記憶體的大小與圖片的尺寸有關,而不是圖片的檔案大小。

例如:有一個 590KB 的圖片,解析度是 2048px * 1536px,它實際使用的記憶體不是 590KB,而是2048 * 1536 * 4 = 12 MB。。

圖片為什麼會佔用這麼大的記憶體呢,這還要從圖片在 iOS 上顯示的原理說起,具體可移步到 WWDC 2018 Session 219:Image and Graphics Best Practices,也可以直接閱讀小夥伴前幾天剛釋出的文章 WWDC2018 影像最佳實踐

圖片的格式

  • sRGB:這個是目前比較通用的全色彩影像色域,每個畫素佔 4 個位元組

  • Wide:每個畫素佔 8 個位元組,相比 sRGB 能表示的顏色更多

還有佔記憶體更小的格式:

  • 亮度和 alpha 8 格式:每畫素 2 個位元組,單色影像和 alpha,metal 著色器。

  • Alpha 8 格式:每個畫素 1 個位元組,用於單色影像,比 SRGB 小 75%

選擇正確的格式可以減少了記憶體的使用。簡單總結一下:

一個位元組:Alpha 8
兩個位元組:亮度和alpha 8
四個位元組:SRGB
八個位元組:Wide 格式
複製程式碼

那下一個話題來了,如何選擇正確的格式呢?

選擇正確的格式

簡單的回答是:不需要你來選擇格式,而是應該讓格式選擇你。是不是覺得一下子鬆了一口氣?哈哈?

用 UIGraphicsImageRenderer 代替 UIGraphicsBeginImageContextWithOptions

使用 UIGraphicsBeginImageContextWithOptions 生成的圖片,每個畫素需要 4 個位元組表示。建議使用 UIGraphicsImageRenderer,這個方法是從 iOS 10 引入,在 iOS 12 上會自動選擇最佳的影像格式,可以減少很多記憶體。

WWDC 2018:iOS 記憶體深入研究

WWDC 2018:iOS 記憶體深入研究

另外,如果想修改顏色,可以直接修改 tintColor,不會有額外的記憶體開銷。

Downsampling

當你縮小一幅影像的時候,會按照取平均值的辦法把多個畫素點變成一個畫素點,這個過程稱為 Downsampling

UIImage 在設定和調整大小的時候,需要將原始影像加壓到記憶體中,然後對內部座標空間做一系列轉換,整個過程會消耗很多資源。我們可以使用 ImageIO,它可以直接讀取影像大小和後設資料資訊,不會帶來額外的記憶體開銷。

WWDC 2018:iOS 記憶體深入研究

WWDC 2018:iOS 記憶體深入研究

在後臺時,對記憶體優化

假設在 App 裡展示了一張很大圖片,當我們切換到後臺去做其它的操作時,這個圖片還在佔用記憶體。我們應該考慮在合適的時機去回收這類佔用過大的資料。

WWDC 2018:iOS 記憶體深入研究

WWDC 2018:iOS 記憶體深入研究

演示Demo

Demo主要是用實際例子講述了上面的知識點,這裡就不再重複講解了,感興趣的童鞋可以移步 iOS Memory Deep Dive

總結

記憶體是一個有限的共享資源,要學會使用 Xcode 分析記憶體工具,從而瞭解應用程式記憶體佔用情況,並使用一些縮減應用程式記憶體佔用空間的技巧和竅門。

PS:有理解認知不正確的,歡迎指正!

相關文章