1. 程式人生 > 其它 >效能優化-記憶體篇

效能優化-記憶體篇

近期由於專案中有存在記憶體洩漏,所以還是得去定位這些問題的存在,雖然QA組已經有了資料採集和問題分析和問題定位。

目前比較明顯的一點就是記憶體洩漏的問題非常的大。於是乎開始了一波效能優化的學習。目前先總結一下記憶體優化相關注意的事項。會持續更新相關的知識點。

目前總結下來,記憶體的開銷無外乎以下三大部分:

1、資源記憶體佔用

2、引擎模組自身記憶體佔用

3、託管堆記憶體佔用

現在逐一解釋一下:

A、資源記憶體佔用

資源使用是否恰當決定了專案的記憶體佔用情況。

資源主要可以分為幾種:紋理(Texture)、網格(Mesh)、動畫片段(AnimationClip)、音訊片段(AudioClip)、材質(Material)、著色器(Shader)、

字型資源(Font)以及文字資源(Text Asset)等等。其中,紋理、網格、動畫片段和音訊片段則是最容易造成較大記憶體開銷的資源。

一、紋理

紋理資源基本是所有遊戲中佔據最大記憶體開銷的資源。

一個6萬面片的場景,網格資源最大才不過10MB,但一個2048*2048的紋理,可能直接達到16MB。因此,專案中紋理資源的使用是否得當會極大地影響專案的記憶體佔用。

那麼應該注意哪些地方呢?

1、紋理格式

紋理格式是研發團隊中最需要關注的紋理熟悉。不僅影響著紋理的記憶體佔用,同時還決定了紋理的載入效率。

一般來說都是儘可能根據硬體的種類選擇硬體支援的紋理格式。

在使用硬體支援的紋理格式時,你可能會遇到以下幾個問題:

  • 色階問題
    由於ETC、PVRTC等格式均為有失真壓縮,因此,當紋理色差範圍跨度較大時,均不可避免地造成不同程度的“階梯”狀的色階問題。因此,很多研發團隊使用RGBA32/ARGB32格式來實現更好的效果。但是,這種做法將造成很大的記憶體佔用。比如,同樣一張1024x1024的紋理,如果不開啟Mipmap,並且為PVRTC格式,則其記憶體佔用為512KB,而如果轉換為RGBA32位,則很可能佔用達到4MB。所以,研發團隊在使用RGBA32或ARGB32格式的紋理時,一定要慎重考慮,更為明智的選擇是儘量減少紋理的色差範圍,使其儘可能使用硬體支援的壓縮格式進行儲存。
  • ETC1 不支援透明通道問題
    在Android平臺上,對於使用OpenGL ES 2.0的裝置,其紋理格式僅能支援ETC1格式,該格式有個較為嚴重的問題,即不支援Alpha透明通道,使得透明貼圖無法直接通過ETC1格式來進行儲存。對此,我們建議研發團隊將透明貼圖儘可能分拆成兩張,即一張RGB24位紋理記錄原始紋理的顏色部分和一張Alpha8紋理記錄原始紋理的透明通道部分。然後,將這兩張貼圖分別轉化為ETC1格式的紋理,並通過特定的Shader來進行渲染,從而來達到支援透明貼圖的效果。該種方法不僅可以極大程度上逼近RGBA透明貼圖的渲染效果,同時還可以降低紋理的記憶體佔用,是我們非常推薦的使用方式。

當然,目前已經有越來越多的裝置支援了OpenGL ES 3.0,這樣Android平臺上你可以進一步使用ETC2甚至ASTC,這些紋理格式均為支援透明通道且壓縮比更為理想的紋理格式。如果你的遊戲適合人群為中高階裝置使用者,那麼不妨直接使用這兩種格式來作為紋理的主要儲存格式。

2、紋理尺寸

紋理尺寸越大,記憶體佔用越大。

儘可能降低紋理尺寸,如果512*512的紋理對顯示效果已經夠用了,就不要用1024*1024的紋理。

因為後者的記憶體佔用是前者的四倍。

3、Mipmap功能

Mipmap旨在有效降低渲染頻寬的壓力,提升遊戲的渲染效率。但是,開啟Mipmap會將紋理記憶體提升1.33倍。對於具有較大縱深感的3D遊戲來說,3D場景模型和角色我們一般是建議開啟Mipmap功能的,但是在我們的測評專案中,經常會發現部分UI紋理也開啟了Mipmap功能。這其實就沒有必要的,絕大多數UI均是渲染在螢幕最上層,開啟Mipmap並不會提升渲染效率,反倒會增加無謂的記憶體佔用。

4、Read & Write

一般情況下,紋理資源的“Read & Write”功能在Unity引擎中是預設關閉的。建議研發團隊密切關注紋理資源中該選項的使用,因為開啟該選項將會使紋理記憶體增大一倍。

二、網格

網格資源在較為複雜的遊戲中,往往佔據較高的記憶體。對於網格資源來說,它在使用時應該注意哪些方面呢?

(1) Normal、Color和Tangent

在我們深度優化過的大量專案中,Mesh資源的資料中經常會含有大量的Color資料、Normal資料和Tangent資料。這些資料的存在將大幅度增加Mesh資源的檔案體積和記憶體佔用。其中,Color資料和Normal資料主要為3DMax、Maya等建模軟體匯出時設定所生成,而Tangent一般為匯入引擎時生成。

更為麻煩的是,如果專案對Mesh進行Draw Call Batching操作的話,那麼將很有可能進一步增大總體記憶體的佔用。比如,100個Mesh進行拼合,其中99個Mesh均沒有Color、Tangent等屬性,剩下一個則包含有Color、Normal和Tangent屬性,那麼Mesh拼合後,CombinedMesh中將為每個Mesh來新增上此三個頂點屬性,進而造成很大的記憶體開銷。

B、引擎模組自身記憶體佔用

引擎自身中存在記憶體開銷的部分紛繁複雜,可以說是由巨量的“微小”記憶體所累積起來的,比如GameObject及其各種Component(最大量的Component應該算是Transform了)、ParticleSystem、MonoScript以及各種各樣的模組Manager(SceneManager、CanvasManager、PersistentManager等)...

一般情況下,上面所指出的引擎各組成部分的記憶體開銷均比較小,真正佔據較大記憶體開銷的是這兩處:WebStreamSerializedFile。其絕大部分的記憶體分配則是由AssetBundle載入資源所致。簡單言之,當您使用new WWW或CreateFromMemory來載入AssetBundle時,Unity引擎會載入原始資料到記憶體中並對其進行解壓,而WebStream的大小則是AssetBundle原始檔案大小 + 解壓後的資料大小 + DecompressionBuffer(0.5MB)。同時,由於Unity 5.3版本之前的AssetBundle檔案為LZMA壓縮,其壓縮比類似於Zip(20%-25%),所以對於一個1MB的原始AssetBundle檔案,其載入後WebStream的大小則可能是5~6MB,因此,當專案中存在通過new WWW載入多個AssetBundle檔案,且AssetBundle又無法及時釋放時,WebStream的記憶體可能會很大,這是研發團隊需要時刻關注的。

對於SerializedFile,則是當你使用LoadFromCacheOrDownload、CreateFromFile或new WWW本地AssetBundle檔案時產生的序列化檔案。

對於WebStream和SerializedFile,你需要關注以下兩點:

  • 是否存在AssetBundle沒有被清理乾淨的情況。開發團隊可以通過Unity Profiler直接檢視其使用具體的使用情況,並確定Take Sample時AssetBundle的存在是否合理;
  • 對於佔用WebStream較大的AssetBundle檔案(如UI Atlas相關的AssetBundle檔案等),建議使用LoadFromCacheOrDownLoad或CreateFromFile來進行替換,即將解壓後的AssetBundle資料儲存於本地Cache中進行使用。這種做法非常適合於記憶體特別吃緊的專案,即通過本地的磁碟空間來換取記憶體空間。

C、託管堆記憶體佔用

對於目前絕大多數基於Unity引擎開發的專案而言,其託管堆記憶體是由Mono分配和管理的。“託管” 的本意是Mono可以自動地改變堆的大小來適應你所需要的記憶體,並且適時地呼叫垃圾回收(Garbage Collection)操作來釋放已經不需要的記憶體,從而降低開發人員在程式碼記憶體管理方面的門檻。

但是這並不意味著研發團隊可以在程式碼中肆無忌憚地開闢託管堆記憶體,因為目前Unity所使用的Mono版本存在一個很嚴重的問題,即:Mono的堆記憶體一旦分配,就不會返還給系統。這意味著Mono的堆記憶體是隻升不降的。舉個例子,專案執行時,在場景A中開闢了60MB的託管堆記憶體,而到下一場景B時,只需要使用20MB的託管堆記憶體,那麼Mono中將會存在40MB空閒的堆記憶體,且不會返還給系統。這是我們非常不願意看到的現象,因為對於遊戲(特別是移動遊戲)來說,記憶體的佔用可謂是寸土寸金的,讓Mono毫無必要地鎖住大量的記憶體,是一件非常浪費的事情。

疑問:我知道了哪些函式的堆記憶體分配大了,但是我該如何去進一步定位不必要的堆記憶體呢?

  • 高頻率地 New Class/Container/Array等。研發團隊切記不要在Update、FixUpdate或較高呼叫頻率的函式中開闢堆記憶體,這會對你的專案記憶體和效能均造成非常大的傷害。做個簡單的計算,假設你的專案中某一函式每一幀只分配100B的堆記憶體,幀率是1秒30幀,那麼1秒鐘遊戲的堆記憶體分配則是3KB,1分鐘的堆記憶體分配就是180KB,10分鐘後就已經分配了1.8MB。如果你有10個這樣的函式,那麼10分鐘後,堆記憶體的分配就是18MB,這期間,它可能會造成Mono的堆記憶體峰值升高,同時又可能引起了多次GC的呼叫。在我們的測評專案中,一個函式在10分鐘內分配上百MB的情況比比皆是,有時候甚至會分配上GB的堆記憶體。
  • Log輸出。在大量的專案中,仍然存在大量Log輸出的情況。建議研發團隊對自身Log的輸出進行嚴格的控制,僅保留關鍵Log,以避免不必要的堆記憶體分配。