游戏性能优化技术干货分享——内存管理
項目的性能優(yōu)化主要圍繞CPU、GPU和內(nèi)存三大方面進行。接上期CPU優(yōu)化專講,我們本期和大家分享內(nèi)存方面的優(yōu)化心得。
無論是游戲還是VR應(yīng)用,內(nèi)存管理都是其研發(fā)階段的重中之重。
然而,在我們測評過的大量項目中,90%以上的項目都存在不同程度的內(nèi)存使用問題。就目前基于Unity引擎開發(fā)的移動游戲和移動VR游戲而言,內(nèi)存的開銷無外乎以下三大部分:1.資源內(nèi)存占用;2.引擎模塊自身內(nèi)存占用;3.托管堆內(nèi)存占用。
如果您的項目存在內(nèi)存問題,一定逃不出以上三種情況。今天,我們就這三種情況逐一進行解釋。
資源內(nèi)存占用
在一個較為復(fù)雜的大中型項目中,資源的內(nèi)存占用往往占據(jù)了總體內(nèi)存的70%以上。因此,資源使用是否恰當(dāng)直接決定了項目的內(nèi)存占用情況。一般來說,一款游戲項目的資源主要可分為如下幾種:紋理(Texture)、網(wǎng)格(Mesh)、動畫片段(AnimationClip)、音頻片段(AudioClip)、材質(zhì)(Material)、著色器(Shader)、字體資源(Font)以及文本資源(Text Asset)等等。其中,紋理、網(wǎng)格、動畫片段和音頻片段則是最容易造成較大內(nèi)存開銷的資源。
一、紋理
紋理資源可以說是幾乎所有游戲項目中占據(jù)最大內(nèi)存開銷的資源。一個6萬面片的場景,網(wǎng)格資源最大才不過10MB,但一個2048x2048的紋理,可能直接就達(dá)到16MB。因此,項目中紋理資源的使用是否得當(dāng)會極大地影響項目的內(nèi)存占用。
那么,紋理資源在使用時應(yīng)該注意哪些地方呢?
(1) 紋理格式
紋理格式是研發(fā)團隊最需要關(guān)注的紋理屬性。因為它不僅影響著紋理的內(nèi)存占用,同時還決定了紋理的加載效率。一般來說,我們建議開發(fā)團隊盡可能根據(jù)硬件的種類選擇硬件支持的紋理格式,比如Android平臺的ETC、iOS平臺的PVRTC、Windows PC上的DXT等等。因此,我們在UWA測評報告中,將紋理格式進行詳細(xì)羅列,以便開發(fā)團隊進行快速查找,一步定位。
?
在使用硬件支持的紋理格式時,你可能會遇到以下幾個問題:
色階問題
由于ETC、PVRTC等格式均為有損壓縮,因此,當(dāng)紋理色差范圍跨度較大時,均不可避免地造成不同程度的“階梯”狀的色階問題。因此,很多研發(fā)團隊使用RGBA32/ARGB32格式來實現(xiàn)更好的效果。但是,這種做法將造成很大的內(nèi)存占用。比如,同樣一張1024x1024的紋理,如果不開啟Mipmap,并且為PVRTC格式,則其內(nèi)存占用為512KB,而如果轉(zhuǎn)換為RGBA32位,則很可能占用達(dá)到4MB。所以,研發(fā)團隊在使用RGBA32或ARGB32格式的紋理時,一定要慎重考慮,更為明智的選擇是盡量減少紋理的色差范圍,使其盡可能使用硬件支持的壓縮格式進行儲存。
ETC1 不支持透明通道問題
在Android平臺上,對于使用OpenGL ES 2.0的設(shè)備,其紋理格式僅能支持ETC1格式,該格式有個較為嚴(yán)重的問題,即不支持Alpha透明通道,使得透明貼圖無法直接通過ETC1格式來進行儲存。對此,我們建議研發(fā)團隊將透明貼圖盡可能分拆成兩張,即一張RGB24位紋理記錄原始紋理的顏色部分和一張Alpha8紋理記錄原始紋理的透明通道部分。然后,將這兩張貼圖分別轉(zhuǎn)化為ETC1格式的紋理,并通過特定的Shader來進行渲染,從而來達(dá)到支持透明貼圖的效果。該種方法不僅可以極大程度上逼近RGBA透明貼圖的渲染效果,同時還可以降低紋理的內(nèi)存占用,是我們非常推薦的使用方式。
當(dāng)然,目前已經(jīng)有越來越多的設(shè)備支持了OpenGL ES 3.0,這樣Android平臺上你可以進一步使用ETC2甚至ASTC,這些紋理格式均為支持透明通道且壓縮比更為理想的紋理格式。如果你的游戲適合人群為中高端設(shè)備用戶,那么不妨直接使用這兩種格式來作為紋理的主要存儲格式。
(2)紋理尺寸
一般來說,紋理尺寸越大,則內(nèi)存占用越大。所以,盡可能降低紋理尺寸,如果512x512的紋理對于顯示效果已經(jīng)夠用,那么就不要使用1024x1024的紋理,因為后者的內(nèi)存占用是前者的四倍。因此,我們在UWA測評報告中,將紋理的尺寸進行詳細(xì)展示,以便開發(fā)團隊進行快速檢測。
?
(3) Mipmap功能
Mipmap旨在有效降低渲染帶寬的壓力,提升游戲的渲染效率。但是,開啟Mipmap會將紋理內(nèi)存提升1.33倍。對于具有較大縱深感的3D游戲來說,3D場景模型和角色我們一般是建議開啟Mipmap功能的,但是在我們的測評項目中,經(jīng)常會發(fā)現(xiàn)部分UI紋理也開啟了Mipmap功能。這其實就沒有必要的,絕大多數(shù)UI均是渲染在屏幕最上層,開啟Mipmap并不會提升渲染效率,反倒會增加無謂的內(nèi)存占用。因此,建議研發(fā)團隊在UWA的測評報告中通過Mipmap一項進行排序,詳細(xì)檢測開啟Mipmap功能的資源是否為UI資源。
?
(4) Read & Write
一般情況下,紋理資源的“Read & Write”功能在Unity引擎中是默認(rèn)關(guān)閉的。但是,我們?nèi)匀辉陧椖可疃葍?yōu)化時發(fā)現(xiàn)了不少項目的紋理資源會開啟該選項。對此,我們建議研發(fā)團隊密切關(guān)注紋理資源中該選項的使用,因為開啟該選項將會使紋理內(nèi)存增大一倍。
?
二、網(wǎng)格
網(wǎng)格資源在較為復(fù)雜的游戲中,往往占據(jù)較高的內(nèi)存。對于網(wǎng)格資源來說,它在使用時應(yīng)該注意哪些方面呢?
(1) Normal、Color和Tangent
在我們深度優(yōu)化過的大量項目中,Mesh資源的數(shù)據(jù)中經(jīng)常會含有大量的Color數(shù)據(jù)、Normal數(shù)據(jù)和Tangent數(shù)據(jù)。這些數(shù)據(jù)的存在將大幅度增加Mesh資源的文件體積和內(nèi)存占用。其中,Color數(shù)據(jù)和Normal數(shù)據(jù)主要為3DMax、Maya等建模軟件導(dǎo)出時設(shè)置所生成,而Tangent一般為導(dǎo)入引擎時生成。
更為麻煩的是,如果項目對Mesh進行Draw Call Batching操作的話,那么將很有可能進一步增大總體內(nèi)存的占用。比如,100個Mesh進行拼合,其中99個Mesh均沒有Color、Tangent等屬性,剩下一個則包含有Color、Normal和Tangent屬性,那么Mesh拼合后,CombinedMesh中將為每個Mesh來添加上此三個頂點屬性,進而造成很大的內(nèi)存開銷。正因如此,我們在UWA測評報告中為每個Mesh展示了其Normal、Color和Tangent屬性的具體使用情況,研發(fā)團隊可以直接針對每種屬性進行排序查看,直接定位出現(xiàn)冗余數(shù)據(jù)的資源。
?
一般來說這些數(shù)據(jù)主要為Shader所用,來生成較為酷炫的效果。所以,建議研發(fā)團隊針對項目中的網(wǎng)格資源進行詳細(xì)檢測,查看該模型的渲染Shader中是否需要這些數(shù)據(jù)進行渲染。
限于篇幅,我們今天只針對紋理和網(wǎng)格資源進行詳細(xì)介紹,對于動畫片段、音頻片段等其他資源,建議您直接通過UWA測評報告中進行查看。同時,我們會在后續(xù)的資源專題中進行詳細(xì)講解,敬請期待。
引擎模塊自身占用
引擎自身中存在內(nèi)存開銷的部分紛繁復(fù)雜,可以說是由巨量的“微小”內(nèi)存所累積起來的,比如GameObject及其各種Component(最大量的Component應(yīng)該算是Transform了)、ParticleSystem、MonoScript以及各種各樣的模塊Manager(SceneManager、CanvasManager、PersistentManager等)...
一般情況下,上面所指出的引擎各組成部分的內(nèi)存開銷均比較小,真正占據(jù)較大內(nèi)存開銷的是這兩處:WebStream 和 SerializedFile。其絕大部分的內(nèi)存分配則是由AssetBundle加載資源所致。簡單言之,當(dāng)您使用new WWW或CreateFromMemory來加載AssetBundle時,Unity引擎會加載原始數(shù)據(jù)到內(nèi)存中并對其進行解壓,而WebStream的大小則是AssetBundle原始文件大小 + 解壓后的數(shù)據(jù)大小 + DecompressionBuffer(0.5MB)。同時,手游出售由于Unity 5.3版本之前的AssetBundle文件為LZMA壓縮,其壓縮比類似于Zip(20%-25%),所以對于一個1MB的原始AssetBundle文件,其加載后WebStream的大小則可能是5~6MB,因此,當(dāng)項目中存在通過new WWW加載多個AssetBundle文件,且AssetBundle又無法及時釋放時,WebStream的內(nèi)存可能會很大,這是研發(fā)團隊需要時刻關(guān)注的。
對于SerializedFile,則是當(dāng)你使用LoadFromCacheOrDownload、CreateFromFile或new WWW本地AssetBundle文件時產(chǎn)生的序列化文件。
對于WebStream和SerializedFile,你需要關(guān)注以下兩點:
是否存在AssetBundle沒有被清理干凈的情況。開發(fā)團隊可以通過Unity Profiler直接查看其使用具體的使用情況,并確定Take Sample時AssetBundle的存在是否合理;
對于占用WebStream較大的AssetBundle文件(如UI Atlas相關(guān)的AssetBundle文件等),建議使用LoadFromCacheOrDownLoad或CreateFromFile來進行替換,即將解壓后的AssetBundle數(shù)據(jù)存儲于本地Cache中進行使用。這種做法非常適合于內(nèi)存特別吃緊的項目,即通過本地的磁盤空間來換取內(nèi)存空間。
注意:關(guān)于AssetBundle的詳細(xì)管理機制,建議查看我們之前的AssetBundle技術(shù)文章。
托管堆內(nèi)存占用
對于目前絕大多數(shù)基于Unity引擎開發(fā)的項目而言,其托管堆內(nèi)存是由Mono分配和管理的。“托管” 的本意是Mono可以自動地改變堆的大小來適應(yīng)你所需要的內(nèi)存,并且適時地調(diào)用垃圾回收(Garbage Collection)操作來釋放已經(jīng)不需要的內(nèi)存,從而降低開發(fā)人員在代碼內(nèi)存管理方面的門檻。
但是這并不意味著研發(fā)團隊可以在代碼中肆無忌憚地開辟托管堆內(nèi)存,因為目前Unity所使用的Mono版本存在一個很嚴(yán)重的問題,即:Mono的堆內(nèi)存一旦分配,就不會返還給系統(tǒng)。這意味著Mono的堆內(nèi)存是只升不降的。舉個例子,項目運行時,在場景A中開辟了60MB的托管堆內(nèi)存,而到下一場景B時,只需要使用20MB的托管堆內(nèi)存,那么Mono中將會存在40MB空閑的堆內(nèi)存,且不會返還給系統(tǒng)。這是我們非常不愿意看到的現(xiàn)象,因為對于游戲(特別是移動游戲)來說,內(nèi)存的占用可謂是寸土寸金的,讓Mono毫無必要地鎖住大量的內(nèi)存,是一件非常浪費的事情。所以,我們在UWA測評報告中,為研發(fā)團隊統(tǒng)計了測試過程中累積的函數(shù)堆內(nèi)存分配量,大家只需要通過查看堆內(nèi)存分配Top10的函數(shù),即可快速對其底層代碼實現(xiàn)進行查看,定位是否有分配不必要堆內(nèi)存的代碼存在。
?
讀到這里,你可能會產(chǎn)生這樣的疑問:我知道了哪些函數(shù)的堆內(nèi)存分配大了,但是我該如何去進一步定位不必要的堆內(nèi)存呢?
這是我們經(jīng)常遇到的問題,所以在我們的深度項目優(yōu)化服務(wù)中,我們都會直接進駐到項目團隊,現(xiàn)場查看項目代碼并對問題代碼進行定位。在經(jīng)過了大量的深度檢測后,我們發(fā)現(xiàn)用戶不必要的堆內(nèi)存分配主要來自于以下幾個方面:
高頻率地 New Class/Container/Array等。研發(fā)團隊切記不要在Update、FixUpdate或較高調(diào)用頻率的函數(shù)中開辟堆內(nèi)存,這會對你的項目內(nèi)存和性能均造成非常大的傷害。做個簡單的計算,假設(shè)你的項目中某一函數(shù)每一幀只分配100B的堆內(nèi)存,幀率是1秒30幀,那么1秒鐘游戲的堆內(nèi)存分配則是3KB,1分鐘的堆內(nèi)存分配就是180KB,10分鐘后就已經(jīng)分配了1.8MB。如果你有10個這樣的函數(shù),那么10分鐘后,堆內(nèi)存的分配就是18MB,這期間,它可能會造成Mono的堆內(nèi)存峰值升高,同時又可能引起了多次GC的調(diào)用。在我們的測評項目中,一個函數(shù)在10分鐘內(nèi)分配上百MB的情況比比皆是,有時候甚至?xí)峙渖螱B的堆內(nèi)存。
Log輸出。我們發(fā)現(xiàn)在大量的項目中,仍然存在大量Log輸出的情況。建議研發(fā)團隊對自身Log的輸出進行嚴(yán)格的控制,僅保留關(guān)鍵Log,以避免不必要的堆內(nèi)存分配。對此,我們在UWA測評報告中對Log的輸出進行了詳細(xì)的檢測,不僅提供詳細(xì)的性能開銷,同時占用Log輸出的調(diào)用路徑。這樣,研發(fā)團隊可直接通過報告定位和控制Log的輸出。
?
UIPanel.LateUpdate。這是NGUI中CPU和堆內(nèi)存開銷最大的函數(shù)。它本身只是一個函數(shù),但NGUI的大量使用使它逐漸成為了一個不可忽視規(guī)則。該函數(shù)的堆內(nèi)存分配和自身CPU開銷,其根源上是一致的,即是由UI網(wǎng)格的重建造成。因此,其對應(yīng)的優(yōu)化方法是直接查看CPU篇中的UI模塊講解。
?
關(guān)于代碼堆內(nèi)存分配的注意點還有很多,比如String連接、部分引擎API(GetComponent)的使用等等,這些已經(jīng)是老生常談了,鑒于篇幅限制不在此處多作介紹,大家感興趣可以Google自行搜索。后續(xù)也會有專門的代碼效率專題講解,敬請關(guān)注。
UWA測評的內(nèi)存標(biāo)準(zhǔn)
在大家使用過UWA之后,對于UWA推薦的內(nèi)存標(biāo)準(zhǔn)值提出了很大的疑惑。在這里,我們也分享下UWA內(nèi)存標(biāo)準(zhǔn)的制定規(guī)則。
(1)150MB的總體內(nèi)存標(biāo)準(zhǔn)主要由以下兩個因素得出:
經(jīng)過了大量的項目優(yōu)化后總結(jié)而得。其實,對于目前市場主流的Unity游戲來說,其內(nèi)存占用主要集中在120~200MB。同時,顧及到iPhone4和512MB/768MB等低端Android機型,其應(yīng)用的自身總體內(nèi)存占用不可超過200MB(iPhone4的安全線應(yīng)該在180MB左右),所以我們將Reserved Total設(shè)定在150MB,這是Unity引擎的自身內(nèi)存分配,以保證App在使用到的系統(tǒng)庫后,其OS中的整體內(nèi)存也在200MB以下。
某些渠道對Android游戲的PSS內(nèi)存進行了嚴(yán)格的限制。一般要求游戲的PSS內(nèi)存在200MB以下。這是我們將Reserved Total內(nèi)存設(shè)定在150MB的另外一個重要原因。
(2)當(dāng)總體內(nèi)存設(shè)定為150MB后,我們進一步對其具體分配進行了設(shè)定。但需要說明的是,這里的內(nèi)存分配其實并沒有嚴(yán)格的公式來進行論證,僅是我們在大量的項目優(yōu)化工作中提煉出的經(jīng)驗值。目前,項目較為合理的內(nèi)存分配如下:
紋理資源: 50 MB
網(wǎng)格資源: 20 MB
動畫片段: 15 MB
音頻片段: 15 MB
Mono堆內(nèi)存: 40 MB
其他: 10 MB
需要指出的是,150MB中并沒有涵蓋較為復(fù)雜的字體文件(比如微軟雅黑)以及Text Asset,這些需要根據(jù)游戲需求而定。
(3)目前的UWA內(nèi)存標(biāo)準(zhǔn)是較為苛刻的,對于中高端設(shè)備而言,其內(nèi)容允許量其實要比150MB要大得多。但我們堅持認(rèn)為,在研發(fā)過程中,一個嚴(yán)苛的標(biāo)準(zhǔn)對于一個項目來說是一件好事。至少,它可以為大家提個醒,讓大家時刻關(guān)注自己的問題。據(jù)我們了解,目前的三到五線城市,其低端手機的覆蓋率還是相當(dāng)高的。同時,對于中高端移動設(shè)備,我們?nèi)栽诓粩嘣囼灪脱芯恐小N覀兿M诓痪玫膶砜梢宰龅结槍Ω鞣N不同檔次的機型都給出一個更為合理的推薦值,從而讓大家更為簡單地對內(nèi)存進行管理。
內(nèi)存泄露
內(nèi)存泄露是開發(fā)人員在項目研發(fā)過程中最常見也最不愿遇到的問題。就目前來看,大家對于判斷項目是否存在內(nèi)存泄露仍然存在一些誤區(qū):
誤區(qū)一
我的項目進出場景前后內(nèi)存回落不一致,比如進入場景后,內(nèi)存增加40MB,出來后下降30MB,仍有10MB內(nèi)存沒有返回給系統(tǒng),即說明內(nèi)存存在泄露情況。
誤區(qū)二
我的項目在進出場景前后,Unity Profiler中內(nèi)存回落正常,但Android的PSS數(shù)值并沒有完全回落(出場景后的PSS值高于進場景前的PSS值),即說明內(nèi)存存在泄露情況。
以上是我們遇到的開發(fā)團隊反饋給我們的典型問題。相信大多數(shù)開發(fā)團隊都會遇到類似的情況。在此有必要說明一下,以上兩種情況均不能表明內(nèi)存存在泄漏問題。即便內(nèi)存在一段時間始終保持增長的趨勢,也不能簡單地判定其存在內(nèi)存泄露。因為造成內(nèi)存不能完全回落的情況有很多,比如資源加載后常駐內(nèi)存以備后續(xù)使用、Mono堆內(nèi)存的只升不降等等,這些均可造成內(nèi)存無法完全回落。一般來說,我們推薦的判斷內(nèi)存是否泄漏的方法如下:
一、檢查資源的使用情況,特別是紋理、網(wǎng)格等資源的使用在我們進行過的項目深度優(yōu)化過程中,資源泄漏是內(nèi)存泄露的主要表現(xiàn)形式,其具體原因是用戶對加載后的資源進行了儲存(比如放到Container中),但在場景切換時并沒有將其Remove或Clear,從而無論是引擎本身還是手動調(diào)用Resources.UnloadUnusedAssets等相關(guān)API均無法對其進行卸載,進而造成了資源泄露。對于這種情況的排查相當(dāng)困難,這是因為項目中的資源量過于巨大,泄露資源往往很難定位。因此,我們在UWA測評報告中對項目中的每個資源都進行了詳細(xì)的監(jiān)控,并通過“生命周期”這一衡量指標(biāo)讓大家可以清楚地了解到每個資源在項目運行過程中的使用范圍。
?
這樣,大家可以通過資源的“生命周期”屬性來快速查看有哪些資源是“常駐”內(nèi)存的,并且判斷該資源是“預(yù)加載”資源還是“泄露”資源。
同時,項目中所使用的總資源數(shù)量往往是成百上千的,讓大家逐個資源檢查過來是一件很費力的事情。所以,我們推出了資源的“場景比較”功能。建議大家通過以下兩種方式進行資源比較,以便更快地找到存在“泄露”問題的資源:
?
一般來說,同種場景或同一場景的資源使用應(yīng)該是較為固定的,比如游戲項目中的主城場景或主界面場景。通過比較不同時刻同一場景的資源信息,可以快速幫你找到其資源使用的差異情況。這樣,你只需判斷這些“差異”資源的存在是否合理,即可快速判定是否存在資源泄露,已經(jīng)具體的泄露資源。
?
除一些常駐資源外,不同類型的場景,其資源使用是完全不同的。比如,游戲中主城和戰(zhàn)斗副本的資源,除少部分常駐內(nèi)存的資源外,二者使用的絕大部分資源應(yīng)該是不一致的。所以,通過比較兩種不同類型的場景,你可以直接查看比較結(jié)果中的“共同資源”,并判斷其是否確實為預(yù)先設(shè)定好的常駐資源。如果不是,則它很可能是“泄露”資源,需要你進一步查看項目的資源管理是否存在漏洞。
?
二、通過Profiler來檢測WebStream或SerializedFile的使用情況AssetBundle的管理不當(dāng)也會造成一定的內(nèi)存泄露,即上一場景中使用的AssetBundle在場景切換時沒有被卸載掉,而被帶入到了下一場場景中。對于這種情況,建議直接通過Profiler Memory中的Take Sample來對其進行檢測,通過直接查看WebStream或SerializedFile中的AssetBundle名稱,即可判斷是否存在“泄露”情況。
?
三、通過Android PSS/iOS Instrument反饋的App線程內(nèi)存來查看承接上述“誤區(qū)二”中的說法,“Unity Profiler中內(nèi)存回落正常,但Android的PSS數(shù)值并沒有完全回落”是有可能的,這是因為Unity Profiler反饋的是引擎的真實分配的物理內(nèi)存,而PSS中記錄的則包括系統(tǒng)的部分緩存。一般情況下,Android或iOS并不會及時將所有App卸載數(shù)據(jù)進行清理,為了保證下次使用時的流暢性,OS會將部分?jǐn)?shù)據(jù)放入到緩存,待自身內(nèi)存不足時,OS Kernel會啟動類似LowMemoryKiller的機制來查詢緩存甚至殺死一些進程來釋放內(nèi)存。因此,并不能通過一兩次的PSS內(nèi)存沒有完全回落來說明內(nèi)存泄露問題。
我們推薦的測試方式是在兩個場景之間來回不停切換,比如主城和戰(zhàn)斗副本間。理論上來說,多次切換同樣的場景,如果Profiler中顯示的Unity內(nèi)存回落正常,那么其PSS/Instrument的內(nèi)存數(shù)值波動范圍也是趨于穩(wěn)定的,但如果出現(xiàn)了PSS/Instrument內(nèi)存持續(xù)增長的情況,則需要大家注意了。這可能有兩種可能:
Unity引擎自身的內(nèi)存泄露問題。這種概率很小,之前僅在少數(shù)版本中出現(xiàn)過。
第三方插件在使用時出現(xiàn)了內(nèi)存泄露。這種概率較大,因為Profiler僅能對Unity自身的內(nèi)存進行監(jiān)控,而無法檢測到第三方庫的內(nèi)存分配情況。因此,在出現(xiàn)上述內(nèi)存問題時,建議大家先對自身使用的第三方庫進行排查。
無效的Mono堆內(nèi)存開銷
目前,Unity所使用的Mono版本中存在一個較大的問題,即內(nèi)存一旦分配,則不會再返回給系統(tǒng)。這就衍生出另外一個問題—— 無效的Mono堆內(nèi)存。它是Mono所分配的堆內(nèi)存,但卻沒有被真正利用上,因此稱之為“無效”。那么,如何查看我的項目中是否存在較大量的“無效堆內(nèi)存”呢?
在UWA測評報告中,我們提供了內(nèi)存隨項目運行的分配情況,如下圖所示。其中,藍(lán)線和紫線的分離情況,反映了無效堆內(nèi)存的分配大小。比如,圖中所選中時刻,藍(lán)線的Reserved Total為當(dāng)前項目所占據(jù)的總物理內(nèi)存,而紫線的Used Total為當(dāng)前項目所使用的總物理內(nèi)存,這說明當(dāng)前項目中的空閑內(nèi)存為57.1MB(200.4-143.3),而這其中主要由兩部分組成,空閑的Unity引擎內(nèi)存和無效的Mono堆內(nèi)存。其中,空閑的Unity內(nèi)存為17.1MB(92.0-74.9),所以當(dāng)前所選幀的無效Mono堆內(nèi)存為40.0MB。并且,從圖中可以看出,藍(lán)線和紫線在運行過程中一直分得較開,這說明一直存在不小的Mono堆內(nèi)存處于“無效”狀態(tài)。這是一件很浪費的事情,特別是對于內(nèi)存寸土寸金的移動設(shè)備而言。
?
那么,我們應(yīng)該如何避免或減少過多“無效堆內(nèi)存”的分配呢?我們推薦的做法如下:
避免一次性堆內(nèi)存的過大分配。Mono的堆內(nèi)存也是“按需”逐步進行分配的。但如果一次性開辟過大堆內(nèi)存,比如New一個較大Container、加載一個過大配置文件等,則勢必會造成Mono的堆內(nèi)存直接沖高,所以研發(fā)團隊對堆內(nèi)存的分配需要時刻注意;避免不必要的堆內(nèi)存開銷。UWA測評報告中將項目運行過程中堆內(nèi)存分配Top10函數(shù)進行羅列,限于篇幅,我們不再此處進行一一贅述,研發(fā)團隊可以直接查看之前一篇的內(nèi)存優(yōu)化相關(guān)文章。
資源冗余
在內(nèi)存管理方面,還有一個大家必須關(guān)注的話題——資源冗余。在我們測評過的大量項目中,95%以上的項目均存在不同程度的資源冗余情況。所謂“資源冗余”,是指在某一時刻內(nèi)存中存在兩份甚至多份同樣的資源。導(dǎo)致這種情況的出現(xiàn)主要有兩種原因:
一、AssetBundle打包機制出現(xiàn)問題
同一份資源被打入到多份AssetBundle文件中。舉個例子,同一張紋理被不同的NPC所使用,同時每個NPC被制作成獨立的AssetBundle文件,那么在沒有針對紋理進行依賴打包的前提下,就會出現(xiàn)該張紋理出現(xiàn)在不同的NPC AssetBundle文件中。當(dāng)這些AssetBundle先后被加載到內(nèi)存后,內(nèi)存中即會出現(xiàn)紋理資源冗余的情況。對此,我們建議研發(fā)團隊在發(fā)現(xiàn)資源冗余問題后,對相關(guān)AssetBundle的制作流程一定要進行檢查。
同時,我們在UWA測評中為每個資源引入了一個衡量指標(biāo)——“數(shù)量峰值”。它指的是同一資源在同一幀中出現(xiàn)的最大數(shù)量。如果大于1,則說明該資源很可能存在 “冗余資源”。大家可以通過這一列進行排序,即可立即查看項目中的資源冗余情況。
?
二、資源的實例化所致
在Unity引擎中,當(dāng)我們修改了一些特定GameObject的資源屬性時,引擎會為該GameObject自動實例化一份資源供其使用,比如Material、Mesh等。以Material為例,我們在研發(fā)時經(jīng)常會有這樣的做法:在角色被攻擊時,改變其Material中的屬性來得到特定的受擊效果。這種做法則會導(dǎo)致引擎為特定的GameObject重新實例化一個Material,后綴會加上(instance)字樣。其本身沒有特別大的問題,但是當(dāng)有改變Material屬性需求的GameObject越來越多時(比如ARPG、MMORPG、MOBA等游戲類型),其內(nèi)存中的冗余數(shù)量則會大量增長。如下圖所示,隨著游戲的進行,實例化的Material資源會增加到333個。雖然Material的內(nèi)存占用不大,但是過多的冗余資源卻為Resources.UnloadUnusedAssets API的調(diào)用效率增加了相當(dāng)大的壓力。
?
一般情況下,資源屬性的改變情況都是固定的,并非隨機出現(xiàn)。比如,假設(shè)GameObject受到攻擊時,其Material屬性改變隨攻擊類型的不同而有三種不同的參數(shù)設(shè)置。那么,對于這種需求,我們建議你直接制作三種不同的Material,在Runtime情況下通過代碼直接替換對應(yīng)GameObject的Material,而非改變其Material的屬性。這樣,你會發(fā)現(xiàn),成百上千的instance Material在內(nèi)存中消失了,取而代之的,則是這三個不同的Material資源。其中的益處,對于能夠閱讀到這里的你來說,應(yīng)該已經(jīng)不需要我多說了。
總結(jié)
以上是生活随笔為你收集整理的游戏性能优化技术干货分享——内存管理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Unity VR游戏开发干货教程:优化V
- 下一篇: 分享:游戏新手引导程序框架设计3要点