Unity內存優化總結(上)

項目的性能優化主要圍繞CPU、GPU和內存三大方面進行。接上期CPU優化專講,我們本期和大家分享內存方面的優化心得。

無論是遊戲還是VR應用,內存管理都是其研發階段的重中之重。

然而,在我們測評過的大量項目中,90%以上的項目都存在不同程度的內存使用問題。就目前基於Unity引擎開發的移動遊戲和移動VR遊戲而言,內存的開銷無外乎以下三大部分:1.資源內存佔用;2.引擎模塊自身內存佔用;3.託管堆內存佔用。

如果您的項目存在內存問題,一定逃不出以上三種情況。今天,我們就這三種情況逐一進行解釋。

資源內存佔用

在一個較為複雜的大中型項目中,資源的內存佔用往往佔據了總體內存的70%以上。因此,資源使用是否恰當直接決定了項目的內存佔用情況。一般來說,一款遊戲項目的資源主要可分為如下幾種:紋理(Texture)、網格(Mesh)、動畫片段(AnimationClip)、音頻片段(AudioClip)、材質(Material)、著色器(Shader)、字體資源(Font)以及文本資源(Text Asset)等等。其中,紋理、網格、動畫片段和音頻片段則是最容易造成較大內存開銷的資源。

一、紋理

紋理資源可以說是幾乎所有遊戲項目中佔據最大內存開銷的資源。一個6萬面片的場景,網格資源最大才不過10MB,但一個2048x2048的紋理,可能直接就達到16MB。因此,項目中紋理資源的使用是否得當會極大地影響項目的內存佔用。

那麼,紋理資源在使用時應該注意哪些地方呢?

(1) 紋理格式

紋理格式是研發團隊最需要關注的紋理屬性。因為它不僅影響著紋理的內存佔用,同時還決定了紋理的載入效率。一般來說,我們建議開發團隊儘可能根據硬體的種類選擇硬體支持的紋理格式,比如Android平台的ETC、iOS平台的PVRTC、Windows PC上的DXT等等。因此,我們在UWA測評報告中,將紋理格式進行詳細羅列,以便開發團隊進行快速查找,一步定位。

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

  • 色階問題

    由於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)紋理尺寸

一般來說,紋理尺寸越大,則內存佔用越大。所以,儘可能降低紋理尺寸,如果512x512的紋理對於顯示效果已經夠用,那麼就不要使用1024x1024的紋理,因為後者的內存佔用是前者的四倍。因此,我們在UWA測評報告中,將紋理的尺寸進行詳細展示,以便開發團隊進行快速檢測。

(3) Mipmap功能

Mipmap旨在有效降低渲染帶寬的壓力,提升遊戲的渲染效率。但是,開啟Mipmap會將紋理內存提升1.33倍。對於具有較大縱深感的3D遊戲來說,3D場景模型和角色我們一般是建議開啟Mipmap功能的,但是在我們的測評項目中,經常會發現部分UI紋理也開啟了Mipmap功能。這其實就沒有必要的,絕大多數UI均是渲染在屏幕最上層,開啟Mipmap並不會提升渲染效率,反倒會增加無謂的內存佔用。因此,建議研發團隊在UWA的測評報告中通過Mipmap一項進行排序,詳細檢測開啟Mipmap功能的資源是否為UI資源。

(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來添加上此三個頂點屬性,進而造成很大的內存開銷。正因如此,我們在UWA測評報告中為每個Mesh展示了其Normal、Color和Tangent屬性的具體使用情況,研發團隊可以直接針對每種屬性進行排序查看,直接定位出現冗餘數據的資源。

一般來說這些數據主要為Shader所用,來生成較為酷炫的效果。所以,建議研發團隊針對項目中的網格資源進行詳細檢測,查看該模型的渲染Shader中是否需要這些數據進行渲染。

限於篇幅,我們今天只針對紋理和網格資源進行詳細介紹,對於動畫片段、音頻片段等其他資源,建議您直接通過UWA測評報告中進行查看。同時,我們會在後續的資源專題中進行詳細講解,敬請期待。

引擎模塊自身佔用

引擎自身中存在內存開銷的部分紛繁複雜,可以說是由巨量的「微小」內存所累積起來的,比如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中進行使用。這種做法非常適合於內存特別吃緊的項目,即通過本地的磁碟空間來換取內存空間。

注意:關於AssetBundle的詳細管理機制,建議查看我們之前的AssetBundle技術文章。

託管堆內存佔用

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

但是這並不意味著研發團隊可以在代碼中肆無忌憚地開闢託管堆內存,因為目前Unity所使用的Mono版本存在一個很嚴重的問題,即:Mono的堆內存一旦分配,就不會返還給系統。這意味著Mono的堆內存是只升不降的。舉個例子,項目運行時,在場景A中開闢了60MB的託管堆內存,而到下一場景B時,只需要使用20MB的託管堆內存,那麼Mono中將會存在40MB空閑的堆內存,且不會返還給系統。這是我們非常不願意看到的現象,因為對於遊戲(特別是移動遊戲)來說,內存的佔用可謂是寸土寸金的,讓Mono毫無必要地鎖住大量的內存,是一件非常浪費的事情。所以,我們在UWA測評報告中,為研發團隊統計了測試過程中累積的函數堆內存分配量,大家只需要通過查看堆內存分配Top10的函數,即可快速對其底層代碼實現進行查看,定位是否有分配不必要堆內存的代碼存在。

讀到這裡,你可能會產生這樣的疑問:我知道了哪些函數的堆內存分配大了,但是我該如何去進一步定位不必要的堆內存呢?

這是我們經常遇到的問題,所以在我們的深度項目優化服務中,我們都會直接進駐到項目團隊,現場查看項目代碼並對問題代碼進行定位。在經過了大量的深度檢測後,我們發現用戶不必要的堆內存分配主要來自於以下幾個方面:

  • 高頻率地 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,以避免不必要的堆內存分配。對此,我們在UWA測評報告中對Log的輸出進行了詳細的檢測,不僅提供詳細的性能開銷,同時佔用Log輸出的調用路徑。這樣,研發團隊可直接通過報告定位和控制Log的輸出。

  • UIPanel.LateUpdate。這是NGUI中CPU和堆內存開銷最大的函數。它本身只是一個函數,但NGUI的大量使用使它逐漸成為了一個不可忽視規則。該函數的堆內存分配和自身CPU開銷,其根源上是一致的,即是由UI網格的重建造成。因此,其對應的優化方法是直接查看CPU篇中的UI模塊講解。

關於代碼堆內存分配的注意點還有很多,比如String連接、部分引擎API(GetComponent)的使用等等,這些已經是老生常談了,鑒於篇幅限制不在此處多作介紹,大家感興趣可以Google自行搜索。後續也會有專門的代碼效率專題講解,敬請關注。

UWA測評的內存標準

在大家使用過UWA之後,對於UWA推薦的內存標準值提出了很大的疑惑。在這裡,我們也分享下UWA內存標準的制定規則。

(1)150MB的總體內存標準主要由以下兩個因素得出:

  • 經過了大量的項目優化後總結而得。其實,對於目前市場主流的Unity遊戲來說,其內存佔用主要集中在120~200MB。同時,顧及到iPhone4和512MB/768MB等低端Android機型,其應用的自身總體內存佔用不可超過200MB(iPhone4的安全線應該在180MB左右),所以我們將Reserved Total設定在150MB,這是Unity引擎的自身內存分配,以保證App在使用到的系統庫後,其OS中的整體內存也在200MB以下。
  • 某些渠道對Android遊戲的PSS內存進行了嚴格的限制。一般要求遊戲的PSS內存在200MB以下。這是我們將Reserved Total內存設定在150MB的另外一個重要原因。

(2)當總體內存設定為150MB後,我們進一步對其具體分配進行了設定。但需要說明的是,這裡的內存分配其實並沒有嚴格的公式來進行論證,僅是我們在大量的項目優化工作中提煉出的經驗值。目前,項目較為合理的內存分配如下:

  • 紋理資源: 50 MB
  • 網格資源: 20 MB
  • 動畫片段: 15 MB
  • 音頻片段: 15 MB
  • Mono堆內存: 40 MB
  • 其他: 10 MB

需要指出的是,150MB中並沒有涵蓋較為複雜的字體文件(比如微軟雅黑)以及Text Asset,這些需要根據遊戲需求而定。

(3)目前的UWA內存標準是較為苛刻的,對於中高端設備而言,其內容允許量其實要比150MB要大得多。但我們堅持認為,在研發過程中,一個嚴苛的標準對於一個項目來說是一件好事。至少,它可以為大家提個醒,讓大家時刻關注自己的問題。據我們了解,目前的三到五線城市,其低端手機的覆蓋率還是相當高的。同時,對於中高端移動設備,我們仍在不斷試驗和研究中。我們希望在不久的將來可以做到針對各種不同檔次的機型都給出一個更為合理的推薦值,從而讓大家更為簡單地對內存進行管理。

以上所說的是遊戲項目中主要的內存分配情況,希望讀到此處的你,可以更加了解Unity項目的內存開銷和潛在問題,並對自己的項目進行更有針對性的檢測。

除以上內容外,還有兩個更為重要的地方需要研發團隊關註:內存泄露資源冗餘。我們將在下一篇內存優化文章中為您帶來相關分享。同時,不同的項目遇到的問題不盡相同,歡迎大家加入到UWA的產品QQ群(465082844),就相關內容來進一步討論。


推薦閱讀:

掘金·翻譯計劃|如何在移動 APP 中設計輸入框
APP連接伺服器,所需要知道的一切事情
mixpanel為什麼能比talkingdata和友盟估值高出3-10倍?
談談學習方法
這個項目碉堡了

TAG:Unity游戏引擎 | 移动开发 | 手机游戏开发 |