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 的空間。一段資料可能會佔用多頁記憶體,所佔用頁總數乘以每頁空間得到的就是這段資料使用的總記憶體。
記憶體頁按照各自的分配和使用狀態,可以被分為 Clean
和 Dirty
兩類。
以上面的程式碼為例,申請一塊長度為 80000 位元組的記憶體空間,按照一頁 16KB 來計算,就需要 6 頁記憶體來儲存。
- 當這些記憶體頁開闢出來的時候,它們都是
Clean
的 - 當向處於第一頁的記憶體寫入資料時,第一頁記憶體會變成
Dirty
- 當向處於最後一頁的記憶體寫入資料時,這一頁也會變成
Dirty
記憶體對映檔案
當 App 訪問一個檔案時,系統核心會負責排程,將磁碟上的檔案載入並對映到記憶體中。如果這是隻讀的檔案,它所佔用到的記憶體頁是 Clean
的。
如下圖所示,一個 50KB 的圖片被載入到記憶體中時,需要分配 4 頁記憶體來儲存。其中第四頁中有 2KB 的空間會被用來儲存這個圖片的資料,剩餘空間可能會被用來儲存其它資料。
典型app記憶體型別
當記憶體不足的時候,系統會按照一定策略來騰出更多空間供使用,比較常見的做法是將一部分低優先順序的資料挪到磁碟上,這個操作稱為 Page Out
。之後當再次訪問到這塊資料的時候,系統會負責將它重新搬回記憶體空間中,這個操作稱為 Page In
。
然而對於移動裝置而言,頻繁對磁碟進行IO操作會降低儲存裝置的壽命。從 iOS7 開始,系統開始採用壓縮記憶體的辦法來釋放記憶體空間,被壓縮的記憶體稱為 Compressed Memory
。下面依次介紹一下 iOS App 通常情況下的三種記憶體型別:Clean Memory
、Dirty 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 造成的,例如在記憶體較小的裝置上,當你接聽電話的時候也有可能發生記憶體警告。按照以往的習慣,你可能會在收到記憶體警告通知的時候去做一些釋放記憶體的事情。然而記憶體壓縮機制會使事情變得複雜。我們來看看這個例子:
假設程式碼中的 cache
已被壓縮過
事實上,當你嘗試去再次訪問 cache 物件的時候,系統會先解壓這塊記憶體
這個過程中記憶體使用會增加,在記憶體吃緊的時候,這並不是我們想要的。隨後,當我們會執行大量工作去清空 cache,最終得到的記憶體空間和記憶體壓縮的結果一樣
所以,相比以往的快取手段,更加建議去調整策略,例如減少快取使用,或者在收到記憶體警告的時候,將這類事情交由系統去處理。
Caching
我們對資料進行快取的目的是想減少 CPU 的壓力,但是過多的快取又會佔用過大的記憶體。由於記憶體壓縮機制的存在,我們需要根據快取資料大小以及重算這些資料的成本,在 CPU 和記憶體之間進行權衡。
在一些需要快取資料的場景下,可以考慮使用 NSCache
代替 NSDictionary
,因為 NSCache
可以自動清理記憶體,在記憶體吃緊的時候會更加合理。
小結
通常情況下,我們所說的記憶體佔用是指 Dirty Memory
和 Compressed Memory
,Clean Memory
不需要過多關心。
App 能使用比較多的記憶體空間,但是上限會根據裝置不同而不同。Extension 能使用的最大記憶體則要低很多,所以當你在開發 Extension 的時候尤其要注意記憶體使用。當使用的記憶體超出限制的時候,系統會丟擲 EXC_RESOURCE_EXCEPTION
異常。
分析記憶體佔用工具
Xcode Memory Gauge
在 Xcode 中,你可以通過 Memory Gauge
工具,很方便快速的檢視 App 執行時的記憶體情況,包括記憶體最高佔用、最低佔用,以及在所有程式中的佔用比例等。如果想要檢視更詳細的資料,就需要用到 Instruments
了。
Instruments
在 Instruments
中,你可以使用 Allocations
、Leaks
、VM Tracker
和 Virtual Memory Trace
對 App 進行多維度分析。
Debug Debugger-Memory Resource Exceptions
當你使用 Xcode 10 以前的版本進行除錯時,在記憶體過大時,debug session 會直接終止,並且在控制檯列印出異常。從 Xcode 10 開始,debugger 會自動捕獲 EXC_RESOURCE RESOURCE_TYPE_MEMORY
異常,並斷點在觸發異常丟擲的地方,十分方便定位問題。
Xcode Memory Debugger
通過這個工具,可以很直觀地檢視記憶體中所有物件的記憶體使用情況,以及相互之間的依賴關係,對定位那些因為迴圈引用導致的記憶體洩露問題十分有幫助。
你也可以點選 File->Export Memory Graph
將其匯出為 memgraph
檔案,在命令列中使用 Developer Tool
對其進行分析。使用這種方式,你可以在任何時候對過去某時的 App 記憶體使用進行分析。
簡單介紹一下相關的命令
vmmap - 檢視虛擬記憶體
檢視詳細報告
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 - 檢視洩漏的記憶體
檢視是否有記憶體洩露
leaks xx.memgraph
檢視某處記憶體的洩漏
leaks --traceTree [記憶體地址] xx.memgraph
更多使用方式請檢視 leaks 的文件
man leaks
heap - 檢視堆區記憶體
檢視所有堆區物件的記憶體使用
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
選項
之後 lldb 會記錄除錯過程中物件建立的堆疊,配合 malloc_history
工具,就可以定位到那些佔用了過大記憶體的物件是哪裡建立的。
malloc_history - 檢視記憶體分配歷史
malloc_history xx.memgraph [address]
malloc_history xx.memgraph --fullStacks [address]
更多使用方式請檢視 malloc_history 的文件
man malloc_history
選擇哪個工具?
上面講述了那麼多的分析工具,那我們應該選擇哪種工具呢?蘋果的工程師幫我們做了如下整理:
大家可以根據上圖所示,根據不同的需要進行選擇。
圖片
對於 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 上會自動選擇最佳的影像格式,可以減少很多記憶體。
另外,如果想修改顏色,可以直接修改 tintColor,不會有額外的記憶體開銷。
Downsampling
當你縮小一幅影像的時候,會按照取平均值的辦法把多個畫素點變成一個畫素點,這個過程稱為 Downsampling
。
UIImage 在設定和調整大小的時候,需要將原始影像加壓到記憶體中,然後對內部座標空間做一系列轉換,整個過程會消耗很多資源。我們可以使用 ImageIO,它可以直接讀取影像大小和後設資料資訊,不會帶來額外的記憶體開銷。
在後臺時,對記憶體優化
假設在 App 裡展示了一張很大圖片,當我們切換到後臺去做其它的操作時,這個圖片還在佔用記憶體。我們應該考慮在合適的時機去回收這類佔用過大的資料。
演示Demo
Demo主要是用實際例子講述了上面的知識點,這裡就不再重複講解了,感興趣的童鞋可以移步 iOS Memory Deep Dive
總結
記憶體是一個有限的共享資源,要學會使用 Xcode 分析記憶體工具,從而瞭解應用程式記憶體佔用情況,並使用一些縮減應用程式記憶體佔用空間的技巧和竅門。
PS:有理解認知不正確的,歡迎指正!