iOS記憶體的深入探究(WWDC 2018 session 416)

weixin_34075551發表於2019-01-08

概述

首先裝置硬體資源是固定的,所以app的記憶體資源是有限的。較低的記憶體佔用可以提高使用者體驗以及效能。如果記憶體佔用過大,可能會被系統殺掉。所以每個開發者都應該注意記憶體問題。本session主要分為以下幾方面:

為什麼要減少記憶體佔用

記憶體佔用

分析記憶體佔用的工具

影象

在後臺時,對記憶體的優化

演示demo

為什麼要減少記憶體佔用?

答案很簡單,為了更好的使用者體驗。減少記憶體佔用能同時減少其對CPU時間維度上的消耗,從而不僅使您所開發的App,其他App以及整個系統也都能表現的更好。

記憶體佔用

並非所有的記憶體佔用都是相等的。而要減少的記憶體佔用其實指的是虛擬記憶體(Virtual Memory) 佔用。

Pages

記憶體是由系統管理,一般以頁為單位來劃分。 

3507944-83dbc420add5d55c.jpg

在iOS 上,每一頁包含16KB的空間。系統會按照頁來分配記憶體,堆上可能會有多個物件在一頁上,也可能一個物件佔用多頁。

所佔用頁總數乘以每頁空間得到的就是這段資料使用的總記憶體。 

3507944-a9124be949ccbee4.jpg

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

舉個例子,如果我申請了一個20000個整型的陣列(80000個位元組)。系統可能會分配給我6頁記憶體。

當我申請空間後,他們都是Clean的

如果我在陣列的第一個位置寫入資料,那麼該頁就會變Dirty了

如果我在陣列最後一個位置寫入資料,那麼該頁就會變Dirty了。

中間的幾頁都是Clean的,因為他們還未被寫入。 

3507944-e8fd3b02edbd2cdd.jpg

記憶體對映檔案

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

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

3507944-7a0479a775ab9335.jpg

典型app記憶體型別

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

Clean Memory

Clean Memory是指那些可以用以Page Out的記憶體,只讀的記憶體對映檔案,或者是App所用到的frameworks。每個frameworks都有_DATA_CONST段,通常他們都是Clean的,但如果用runtime進行swizzling,那麼他們就會變Dirty。

Dirty Memory

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

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

Compressed Memory

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

Compressed memory是在記憶體緊張時能夠將最近使用過的記憶體佔用壓縮至原有大小的一半以下,並且能夠在需要時解壓複用。它在節省記憶體的同時提高了系統的響應速度,特點總結起來如下: 

* Shrinks memory usage 減少了不活躍記憶體佔用 

* Improves power efficiency 改善電源效率,通過壓縮減少磁碟IO帶來的損耗 

* Minimizes CPU usage 壓縮/解壓十分迅速,能夠儘可能減少 CPU 的時間開銷 

* Is multicore aware 支援多核操作

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

Memory Warning

記憶體警告,不一定總是應用自身導致的

記憶體壓縮技術使得釋放記憶體變得複雜 

快取策略

小結

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

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

分析記憶體佔用的工具

Xcode Memory Gauge

3507944-94ba34ec322fd0ee.jpg

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

Instruments

3507944-360e32ad3d60e839.jpg

在 Instruments 中,你可以使用Allocations、Leaks、VM Tracker和 Virtual Memory Trace對App進行多維度分析。

Allocations

Leaks

VM Tracker

3507944-f21100c57615bce8.jpg

Virtual Memory Trace

3507944-c55faf0744aeb301.jpg

Debug Debugger - Memory Resource Exceptions

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

3507944-26c2688e468dd6ca.jpg

Xcode Memory Debugger

Xcode Memory Debugger的記憶體偵錯程式是在Xcode 8中提供的,它可以幫助您跟蹤物件依賴性,週期和洩漏。在Xcode 10中,優化了介面佈局。 

3507944-c82831ed6185833d.jpg

你也可以點選File->Export Memory Graph將其匯出為memgraph檔案,通過命令列對其進行分析。下面說下幾個命令列工具

vmmap

vmmap 能夠列印出程式資訊,所有分配給該程式的 VM區域以及VM區域的種類、記憶體佔用資訊等內容。

利用--summary則能夠根據不同的區域型別列印出詳細的記憶體佔用型別和資訊。這裡需要注意的是 SWAPPED SIZE在iOS上指的是Compressed memory size且其值表示壓縮前的佔用大小

vmmap--summaryApp.memgraph

1

如果您希望檢視更多的資訊,那麼直接呼叫即可。您將獲得所有區域的內容。

vmmap App.memgraph

1

3507944-72054b27daeee771.jpg

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

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

1

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

man vmmap

1

Leak

顧名思義,就是檢視記憶體洩漏的。

leaks xx.memgraph

1

3507944-f5d399ec0513a90e.jpg

更多使用方式也可以檢視man手冊。

heap

檢視堆區記憶體

heap xx.memgraph

1

3507944-19d1cd6145777547.jpg

預設情況下,是按照數量排序的,當然也可以通過引數-sortBySize讓其來按照大小排序。

heap xx.memgraph-sortBySize

1

3507944-28140cfa83bc7075.jpg

排列之後,我們發現了一些巨大的NSConcreteData物件,通過下面的命令,就可以得到每個物件的記憶體地址。

heap xx.memgraph -addresses'NSConcreteData'#得到全部物件的記憶體地址#heap xx.memgraph -addresses all

1

2

3

4

3507944-7f22b7bbf441266a.jpg

有了這些地址呢,我們就可以知道他們是從哪裡來的。有了這些物件的記憶體地址之後,我們還需要另一樣工具幫助我們做下一步分析。

Enabling Malloc Stack Logging

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

3507944-aa63058f8afea4a4.jpg

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

malloc_history

檢視記憶體分配的歷史,使用方法如下

malloc_historyxx.memgraph[address]malloc_historyxx.memgraph--fullStacks[address]

1

2

3

3507944-3be27ba7f3b98d46.jpg

工具的選擇

以上講了很多工具,當遇到記憶體問題時,那我們要如何進行選擇呢?

這裡有三種方法來考慮。您是否想檢視物件的建立?您是否想檢視記憶體中物件的引用或者地址內容?或者您是否想檢視一個例項有多大?

3507944-1865e0746fab5648.jpg

可以根據上圖所示,按照不同情況,來使用不同的工具。

影象

圖片所佔記憶體的大小與圖片的尺寸有關,而不是圖片的檔案大小。 

舉個例子,我們這裡有一張590KB圖片,而它的解析度是2048px * 1536px。它實際使用的記憶體不是590KB,而是2048 * 1536 * 4 = 12 MB。

圖片為什麼會佔這麼多的記憶體?這還要從圖片在iOS上顯示的原理說起。一張圖片檔案從磁碟到展示需要經過三步:

載入

解壓縮

渲染

更多關於影象以及如何優化影象的資訊,請檢視WWDC 2018 Image and Graphics Best Practices,也可以直接閱讀前幾天我們小夥伴釋出的文章影象和圖形的最佳實踐)

影象渲染格式

sRGB格式

Wide格式

亮度和alpha 8格式

alpha 8格式

選擇正確的圖片格式

簡單的回答是:不需要你來選擇格式,而是應該讓格式選擇你。

用UIGraphicsImageRenderer代替UIGraphicsBeginImageContextWithOptions

使用UIGraphicsBeginImageContextWithOptions生成的圖片,每個畫素需要4個位元組表示。建議使用UIGraphicsImageRenderer,這個方法是從iOS 10引入,在iOS 12上會自動選擇最佳的影象格式,可以減少很多記憶體。UIGraphicsImageRenderer可以建立UIImage物件或者進行JPEG/PNG格式的編碼。 

3507944-bded01ef0a80b1be.jpg
3507944-de06747a98110e35.jpg

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

下采樣

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

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

3507944-3f6b4a1c5d4027e1.jpg
3507944-e4bfcbcf67f232e9.jpg

這樣處理,不但記憶體佔用的更低了,而且執行速度也快了50%左右。

在後臺時,對記憶體進行優化

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

監聽UIApplicationWillEnterForeground和UIApplicationDidEnterBackground通知

viewWillAppear和viewDidDisappear方法

3507944-4f4a29db4ca0dbab.jpg
3507944-01a367da25026708.jpg

Demo演示

略過,基本上就是用上面說的命令去除錯一個問題及優化方案去除錯圖片的記憶體問題

總結

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

相關文章