WWDC 2018 Session 416:iOS Memory Deep Dive緩存
查看更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄bash
做者:高老師,純潔善良有理想的 iOS 開發一枚session
對於咱們的 App 所依賴的設備而言,內存資源是有限的。下降 App 所使用的內存能夠提升性能和體驗,相反,過大的內存佔用可能會致使 App 被系統強制退出。因此每一個 iOS 開發者都應該關注內存問題。這一節新的內容很少,基本上都是一些老的知識點。app
按照 Session 的套路,咱們先看一下提綱:ide
那咱們就按順序開始啦!工具
在探討內存以前,咱們要知道爲何要減小內存。簡單的回答是能夠有更好的用戶體驗:更快的啓動速度,不會由於內存過大而致使 Crash,可讓 App 存活更久等。post
並不是全部 App 的內存佔用都是相同的。在繼續探討 iOS 上 App 的內存使用以前,咱們先來聊一下Pages Memory
。性能
內存是由系統管理,通常以頁爲單位來劃分。在 iOS 上,每一頁包含 16KB 的空間。一段數據可能會佔用多頁內存,所佔用頁總數乘以每頁空間獲得的就是這段數據使用的總內存。優化
內存頁按照各自的分配和使用狀態,能夠被分爲 Clean
和 Dirty
兩類。spa
以上面的代碼爲例,申請一塊長度爲 80000 字節的內存空間,按照一頁 16KB 來計算,就須要 6 頁內存來存儲。
Clean
的Dirty
Dirty
當 App 訪問一個文件時,系統內核會負責調度,將磁盤上的文件加載並映射到內存中。若是這是隻讀的文件,它所佔用到的內存頁是 Clean
的。
以下圖所示,一個 50KB 的圖片被加載到內存中時,須要分配 4 頁內存來存儲。其中第四頁中有 2KB 的空間會被用來存儲這個圖片的數據,剩餘空間可能會被用來存儲其它數據。
當內存不足的時候,系統會按照必定策略來騰出更多空間供使用,比較常見的作法是將一部分低優先級的數據挪到磁盤上,這個操做稱爲 Page Out
。以後當再次訪問到這塊數據的時候,系統會負責將它從新搬回內存空間中,這個操做稱爲 Page In
。
然而對於移動設備而言,頻繁對磁盤進行IO操做會下降存儲設備的壽命。從 iOS7 開始,系統開始採用壓縮內存的辦法來釋放內存空間,被壓縮的內存稱爲 Compressed Memory
。下面依次介紹一下 iOS App 一般狀況下的三種內存類型:Clean Memory
、Dirty Memory
以及Compressed Memory
。
Clean Memory
是指那些能夠用以 Page Out
的內存,包括已被加載到內存中的文件,或者是 App 所用到的 frameworks。每一個 frameworks 都有 _DATA_CONST
段,當 App 在運行時使用到了某個 framework,它所對應的 _DATA_CONST
的內存就會由 Clean 變爲 Dirty。
Dirty Memory
是指那些被 App 寫入過數據的內存,包括全部堆區的對象、圖像解碼緩衝區,同時,相似 Clean memory
,也包括 App 所用到的 frameworks。每一個 framework 都會有 _DATA
段和 _DATA_DIRTY
段,它們的內存是 Dirty
的。
值得注意的是,在使用 framework 的過程當中會產生 Dirty Memory
,使用單例或者全局初始化方法是減小 Dirty Memory
不錯的方法,由於單例一旦建立就不會銷燬,全局初始化方法會在 class 加載時執行。
當內存吃緊的時候,系統會將不使用的內存進行壓縮,直到下一次訪問的時候進行解壓。
例如,當咱們使用 Dictionary
去緩存數據的時候,假設如今已經使用了 3 頁內存,當不訪問的時候可能會被壓縮爲 1 頁,再次使用到時候又會解壓成 3 頁。
並不是全部內存警告都是由 App 形成的,例如在內存較小的設備上,當你接聽電話的時候也有可能發生內存警告。按照以往的習慣,你可能會在收到內存警告通知的時候去作一些釋放內存的事情。然而內存壓縮機制會使事情變得複雜。咱們來看看這個例子:
假設代碼中的 cache
已被壓縮過
事實上,當你嘗試去再次訪問 cache 對象的時候,系統會先解壓這塊內存
這個過程當中內存使用會增長,在內存吃緊的時候,這並非咱們想要的。隨後,當咱們會執行大量工做去清空 cache,最終獲得的內存空間和內存壓縮的結果同樣
因此,相比以往的緩存手段,更加建議去調整策略,例如減小緩存使用,或者在收到內存警告的時候,將這類事情交由系統去處理。
咱們對數據進行緩存的目的是想減小 CPU 的壓力,可是過多的緩存又會佔用過大的內存。因爲內存壓縮機制的存在,咱們須要根據緩存數據大小以及重算這些數據的成本,在 CPU 和內存之間進行權衡。
在一些須要緩存數據的場景下,能夠考慮使用 NSCache
代替 NSDictionary
,由於 NSCache
能夠自動清理內存,在內存吃緊的時候會更加合理。
一般狀況下,咱們所說的內存佔用是指 Dirty Memory
和 Compressed Memory
,Clean Memory
不須要過多關心。
App 能使用比較多的內存空間,可是上限會根據設備不一樣而不一樣。Extension 能使用的最大內存則要低不少,因此當你在開發 Extension 的時候尤爲要注意內存使用。當使用的內存超出限制的時候,系統會拋出 EXC_RESOURCE_EXCEPTION
異常。
在 Xcode 中,你能夠經過 Memory Gauge
工具,很方便快速的查看 App 運行時的內存狀況,包括內存最高佔用、最低佔用,以及在全部進程中的佔用比例等。若是想要查看更詳細的數據,就須要用到 Instruments
了。
在 Instruments
中,你可使用 Allocations
、Leaks
、VM Tracker
和 Virtual Memory Trace
對 App 進行多維度分析。
當你使用 Xcode 10 之前的版本進行調試時,在內存過大時,debug session 會直接終止,而且在控制檯打印出異常。從 Xcode 10 開始,debugger 會自動捕獲 EXC_RESOURCE RESOURCE_TYPE_MEMORY
異常,並斷點在觸發異常拋出的地方,十分方便定位問題。
經過這個工具,能夠很直觀地查看內存中全部對象的內存使用狀況,以及相互之間的依賴關係,對定位那些由於循環引用致使的內存泄露問題十分有幫助。
你也能夠點擊 File->Export Memory Graph
將其導出爲 memgraph
文件,在命令行中使用 Developer Tool
對其進行分析。使用這種方式,你能夠在任什麼時候候對過去某時的 App 內存使用進行分析。
簡單介紹一下相關的命令
查看詳細報告
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 xx.memgraph
查看某處內存的泄漏
leaks --traceTree [內存地址] xx.memgraph
更多使用方式請查看 leaks 的文檔
man leaks
查看全部堆區對象的內存使用
heap xx.memgraph
默認狀況下是按照對象數量進行排序,一般狀況下它們不會形成什麼內存問題。咱們須要關心的是那些爲數很少,卻佔用了大量內存的對象,這時候就能夠增長參數 -sortBySize
,按照內存佔用大小順序來查看全部堆區對象的內存使用
heap xx.memgraph -sortBySize
當肯定是哪一個類型的對象佔用了太多內存以後,能夠獲得每一個對象的內存地址
heap xx.memgraph -addresses all | 'XXBigData'
更多使用方式請查看 heap 的文檔
man heap
有了這些對象的內存地址以後,咱們還須要另外一樣工具幫助咱們作下一步分析。
在 Product -> Scheme -> Edit Scheme -> Diagnostics
中,開啓 Malloc Stack
功能,建議使用 Live Allocations Only
選項
以後 lldb 會記錄調試過程當中對象建立的堆棧,配合 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 格式
複製代碼
那下一個話題來了,如何選擇正確的格式呢?
簡單的回答是:不須要你來選擇格式,而是應該讓格式選擇你。是否是以爲一會兒鬆了一口氣?哈哈😆
使用 UIGraphicsBeginImageContextWithOptions
生成的圖片,每一個像素須要 4 個字節表示。建議使用 UIGraphicsImageRenderer
,這個方法是從 iOS 10 引入,在 iOS 12 上會自動選擇最佳的圖像格式,能夠減小不少內存。
另外,若是想修改顏色,能夠直接修改 tintColor,不會有額外的內存開銷。
當你縮小一幅圖像的時候,會按照取平均值的辦法把多個像素點變成一個像素點,這個過程稱爲 Downsampling
。
UIImage 在設置和調整大小的時候,須要將原始圖像加壓到內存中,而後對內部座標空間作一系列轉換,整個過程會消耗不少資源。咱們可使用 ImageIO,它能夠直接讀取圖像大小和元數據信息,不會帶來額外的內存開銷。
假設在 App 裏展現了一張很大圖片,當咱們切換到後臺去作其它的操做時,這個圖片還在佔用內存。咱們應該考慮在合適的時機去回收這類佔用過大的數據。
Demo主要是用實際例子講述了上面的知識點,這裏就再也不重複講解了,感興趣的童鞋能夠移步 iOS Memory Deep Dive
內存是一個有限的共享資源,要學會使用 Xcode 分析內存工具,從而瞭解應用程序內存佔用狀況,並使用一些縮減應用程序內存佔用空間的技巧和竅門。
PS:有理解認知不正確的,歡迎指正!