WWDC 2018:iOS 內存深刻研究

WWDC 2018 Session 416:iOS Memory Deep Dive緩存

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

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

引言

對於咱們的 App 所依賴的設備而言,內存資源是有限的。下降 App 所使用的內存能夠提升性能和體驗,相反,過大的內存佔用可能會致使 App 被系統強制退出。因此每一個 iOS 開發者都應該關注內存問題。這一節新的內容很少,基本上都是一些老的知識點。app

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

  • 爲何要減小內存使用
  • 內存佔用
  • 分析內存佔用工具
  • 圖像
  • 在後臺時,對內存優化
  • 演示 Demo

那咱們就按順序開始啦!工具

爲何要減小內存

在探討內存以前,咱們要知道爲何要減小內存。簡單的回答是能夠有更好的用戶體驗:更快的啓動速度,不會由於內存過大而致使 Crash,可讓 App 存活更久等。post

內存佔用

並不是全部 App 的內存佔用都是相同的。在繼續探討 iOS 上 App 的內存使用以前,咱們先來聊一下Pages Memory性能

Pages Memory

內存是由系統管理,通常以頁爲單位來劃分。在 iOS 上,每一頁包含 16KB 的空間。一段數據可能會佔用多頁內存,所佔用頁總數乘以每頁空間獲得的就是這段數據使用的總內存。優化

內存頁按照各自的分配和使用狀態,能夠被分爲 CleanDirty 兩類。spa

以上面的代碼爲例,申請一塊長度爲 80000 字節的內存空間,按照一頁 16KB 來計算,就須要 6 頁內存來存儲。

  • 當這些內存頁開闢出來的時候,它們都是 Clean
  • 當向處於第一頁的內存寫入數據時,第一頁內存會變成 Dirty
  • 當向處於最後一頁的內存寫入數據時,這一頁也會變成 Dirty

內存映射文件

當 App 訪問一個文件時,系統內核會負責調度,將磁盤上的文件加載並映射到內存中。若是這是隻讀的文件,它所佔用到的內存頁是 Clean 的。

以下圖所示,一個 50KB 的圖片被加載到內存中時,須要分配 4 頁內存來存儲。其中第四頁中有 2KB 的空間會被用來存儲這個圖片的數據,剩餘空間可能會被用來存儲其它數據。

典型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 形成的,例如在內存較小的設備上,當你接聽電話的時候也有可能發生內存警告。按照以往的習慣,你可能會在收到內存警告通知的時候去作一些釋放內存的事情。然而內存壓縮機制會使事情變得複雜。咱們來看看這個例子:

假設代碼中的 cache 已被壓縮過

事實上,當你嘗試去再次訪問 cache 對象的時候,系統會先解壓這塊內存

這個過程當中內存使用會增長,在內存吃緊的時候,這並非咱們想要的。隨後,當咱們會執行大量工做去清空 cache,最終獲得的內存空間和內存壓縮的結果同樣

因此,相比以往的緩存手段,更加建議去調整策略,例如減小緩存使用,或者在收到內存警告的時候,將這類事情交由系統去處理。

Caching

咱們對數據進行緩存的目的是想減小 CPU 的壓力,可是過多的緩存又會佔用過大的內存。因爲內存壓縮機制的存在,咱們須要根據緩存數據大小以及重算這些數據的成本,在 CPU 和內存之間進行權衡。

在一些須要緩存數據的場景下,能夠考慮使用 NSCache 代替 NSDictionary,由於 NSCache 能夠自動清理內存,在內存吃緊的時候會更加合理。

小結

一般狀況下,咱們所說的內存佔用是指 Dirty MemoryCompressed MemoryClean Memory 不須要過多關心。

App 能使用比較多的內存空間,可是上限會根據設備不一樣而不一樣。Extension 能使用的最大內存則要低不少,因此當你在開發 Extension 的時候尤爲要注意內存使用。當使用的內存超出限制的時候,系統會拋出 EXC_RESOURCE_EXCEPTION 異常。

分析內存佔用工具

Xcode Memory Gauge

在 Xcode 中,你能夠經過 Memory Gauge 工具,很方便快速的查看 App 運行時的內存狀況,包括內存最高佔用、最低佔用,以及在全部進程中的佔用比例等。若是想要查看更詳細的數據,就須要用到 Instruments 了。

Instruments

Instruments 中,你可使用 AllocationsLeaksVM TrackerVirtual 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:有理解認知不正確的,歡迎指正!

相關文章
相關標籤/搜索