FMOD熱更新在安卓下的堆內存佔用

1)FMOD熱更新在安卓下的堆內存佔用
2)優化MeshSkinning.Render的Draw Call
3)通過UnityWebRequest的API下載AssetBundle並進行本地緩存
4)如何選擇DOTS項目的熱更新方案
5)Addressable的熱更新和打包問題


這是第202篇UWA技術知識分享的推送。今天我們繼續爲大家精選了若干和開發、優化相關的問題,建議閱讀時間10分鐘,認真讀完必有收穫。

UWA 問答社區:answer.uwa4d.com
UWA QQ羣2:793972859(原羣已滿員)

Mono

Q:我們在Unity 2017.4.22f1版本中集成了FMOD,在最近的性能測試中發現它的內存佔用比較大,然後發現是FMOD在LoadBank時分配了大量內存。在源碼中發現,如果FMOD的LoadBank函數在安卓平臺上的路徑爲非file:///android_asset開頭的bank文件,會採取WWW阻塞式加載,也就意味着,如果bank文件不是放在StreamingAssets目錄下,bank文件就不可能採取FMOD的流加載方式。這看上去是一個非常低端的做法,不知道大家有沒有使用過FMOD?能否提供什麼解決方案呢?FMOD的版本是2.00.03。

 

A1:如果bank文件不是放在StreamingAssets目錄下,也是可以用流式加載的,需要魔改一下RuntimeManager的LoadBank。

我們項目的用法是:

    LoadedBank loadedBank = new LoadedBank(); 
    loadResult = Instance.studioSystem.loadBankFile(bankPath, 
    FMOD.Studio.LOAD_BANK_FLAGS.NORMAL, out loadedBank.Bank); 
    Instance.loadedBankRegister(loadedBank, bankPath, bankName, loadSamples, loadResult);

bankPath是bank在沙盒目錄中的絕對路徑,比如($"{Application.persistentDataPath}/banks/MasterBank.bank")。
感謝譚銘@UWA問答社區提供了回答

A2:感謝譚銘的幫助,現在公佈最後結果。這個問題其實歸結爲兩個部分:

  1. 使用FMOD的LoadBankFile來裝載bank,就可以提供流方式加載。
  2. 在安卓手機下,如果bank資源,是放在StreamingAssets文件夾裏面打包進去的,那麼文件路徑就應該寫成"file:///android_asset/" + bank路徑,如:“file:///android_asset/banks/Maseter.string.bank”,如果是放在persisternData目錄裏,那麼就使用沙盒目錄絕對路徑即可,即:Application.persistentDataPath + “/banks/MasterBank.bank”。

關於FMOD的熱更新方案,因爲網上沒有找到確切的內容,但是根據上面的結論,可以得出我們可以在StreamingAssets或者persistentData目錄下,裝載bank文件,這也爲熱更新提供了可能性。我們只要確定什麼時候使用那個路徑即可。理論上,對於一些插件,我是不贊成修改原文件的,這樣不利於以後的升級,但是看完之後還是決定對RuntimeManager進行魔改。

建議提供兩個函數:

  1. 提供一個clearbank函數,因爲原來RuntimeManager是採取引用計數的方式,unload不一定能卸載掉所有bank文件。在熱更新之前可能要播放音樂,然後熱更新,clearbank,然後再裝載新的bank。
  2. 魔改或者新提供一個LoadBank,內容如下:
        public static void LoadBank(string bankName, string bankPath, bool loadSamples = false)
    {
        if (Instance.loadedBanks.ContainsKey(bankName))
        {
            LoadedBank loadedBank = Instance.loadedBanks[bankName];
            loadedBank.RefCount++;

            if (loadSamples)
            {
                loadedBank.Bank.loadSampleData();
            }
            Instance.loadedBanks[bankName] = loadedBank;
        }
        else
        {   
            FMOD.RESULT loadResult;

            {
                LoadedBank loadedBank = new LoadedBank();
                loadResult = Instance.studioSystem.loadBankFile(bankPath, FMOD.Studio.LOAD_BANK_FLAGS.NORMAL, out loadedBank.Bank);
                Instance.loadedBankRegister(loadedBank, bankPath, bankName, loadSamples, loadResult);
            }
        }
    }

建議還是新增,然後自己的Audiomanager管理bank文件的時候使用這個新的函數,不然在其它地方會有一些報錯。

這裏面RuntimeManager中的LoadBanks在非Editor環境下可以不調用。項目啓動的時候,想辦法把全部bank文件都裝載就可以,注意要寫裝載Master.strings.bank和Master.bank。因爲有流加載,所以全部加載完,整個項目音頻文件分配大概就是1~2MB的堆內存。

感謝題主衛鵬鴻@UWA問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5eafa313979400061e5451a7


Rendering

Q:MeshSkinning.Render部分開銷過高,怎麼優化其Draw Call呢?

A:MeshSkinning.Render耗時較高,一般可能出現在兩種情況下:

1. 角色有實時換裝需求,同時場景中有大量不同種類角色。
這種情況下目前最好的Best Practise,就是根據機型控制同屏顯示人數。當然,也可以通過Mesh Baker等插件來將這些Skinned Mesh進行合批,但它很可能會帶來大量的堆內存分配,從而引發GC的到來。這裏有兩點可能在今後的Unity版本中得以控制,一是利用Mesh指針來進行操作;二是手動控制GC的開啓和關閉。這兩點都能有效降低堆內存分配;

2. 場景中含有大量同種怪物。
這種情況在MMO遊戲中非常常見,一般在現在國內的移動設備上,建議直接使用GPU Skinning + GPU Instancing的方法來降低Draw Call;建議題主查看這篇文章《GPU Skinning 加速骨骼動畫》

以上是目前較爲常見的MeshSkining.Render CPU較高的問題。當然,也會出現一些其它的可能,比如把樹和草做成Skinned Mesh,把大風車、旋轉木馬做成Skinned Mesh,甚至也有把地球等天體做成Skinned Mesh的,這些就需要研發團隊具體案例具體分析了。

該回答由UWA提供,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5ea92f6e4d93790618e0ebfd


AssetBundle

Q:如何通過UnityWebRequest API 下載AssetBundle並進行本地緩存?最近我想實現此功能,我的思路是下載AssetBundle之後,再拿到byte[],之後再寫入本地。我使用了兩種方法:
使用方法1(如下圖),可以下載到AssetBundle,卻無法取得byte[]。
使用方法2,雖實現了此功能,但實現方式卻並不理想,具體的情況可以看一下注釋。

請問有人可以提供解決方案嗎?(PS:寫入本地也未必受限於獲取byte[]再寫入本地的方式,有其它的做法也可以。)

A:提供另外一個思路,UnityWebRequestAssetBundle.GetAssetBundle這個接口如果提供了版本號或者hash值是支持緩存功能的,使用Caching可以設置緩存路徑。具體情況可以參考這個文件

    IEnumerator GetAssetBundle()
    {
        Caching.currentCacheForWriting = Caching.AddCache(“D:/Shalou/UnityCaching/”);
        string url = “http://localhost:8083/StandaloneWindows/zx1234.bundle”;
        UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(url,2,0);//這裏隨便給了一個版本號:2
    
        yield return request.SendWebRequest();
        AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
    }

下載一次之後,就會生成如下圖所示的文件夾路徑,第二次加載的時候就能自動從緩存里加載了。

在編輯器裏面試了,但沒有在真機上測試。

感謝Xuan@UWA問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5eb37c91979400061e5451bd


Script

Q:最近了解了Unity DOTS後感覺很不錯,進入Unity 2019.3後也開始穩定了,但是瞭解完後對DOTS和熱更新的契合程度有些疑惑。

具體問題如下:
1. 如果需要熱更新,是否建議用DOTS進行項目的開發(大概做一到兩年的項目(也就是說到那時Unity 2020LTS也已開發出來));
2. 如果使用DOTS,哪種熱更新方案支持比較好呢?看了DOTS的ForEach都是各種不一樣的類(泛型);
3. 繼承ComponentSystem、JobSystem之類的可以在熱更新層實現嗎?

A:DOTS說直接點就是“緩存友好 + SIMD+ 多線程 + struct去掉GC + LLVM burst 編輯器優化”,圍繞着這些Unity提供了完整的解決方案DOTS。

就我的理解回答一下樓主的3個問題:

  1. 我覺得首先要找出項目中可能的性能瓶頸。如果只是普通的幾個遊戲對象,使用或者不使用DOTS其實沒有什麼區別,如果是成千上萬個,比如去年哥本哈根的一個殭屍遊戲的分享,他們的遊戲通過DOTS性能提升了2000倍,有興趣可以看看他們的分享。
  2. 關於熱更新,現在大家都有Lua,其實即使以前沒有DOTS,我們也不需要對所有東西進行熱更新。傳統的做法比如Lua將數據傳入主工程,主工程裏在DOTS中進行多線程計算最終返回結果。前提是計算步驟是不能熱更新的,傳入的參數可以熱更新修改(DOTS有一部分代碼也是寫在主線程的,主線程完全可以和Lua進行交互,然後在JOB多線程進行加速,最後是返回,只是沒必要每幀都穿透)。
  3. 繼承ComponentSystem、JobSystem之類不能直接熱更新。

最後說說我的一點見解, DOTS和傳統面向對象的開發還是有些不同的。有時候沒必要爲了DOTS而使用DOTS。我們做項目一般有兩個目標:一是容易做,二是效率高(事實證明容易做效率就會低,效率高必然不容易做)。我們反反覆覆在這兩個目標之間尋找平衡點,所以我說一定要一開始確定項目中哪些可能是性能瓶頸。比如原本要在主線程中完成的,我們看看能否移動到多線程中。

關於DOTS的更多信息,可以參考UWA學堂的兩篇文章:《DOTS深度研究之原理分析篇》《DOTS深度研究之應用實踐篇》

感謝雨鬆MOMO@UWA問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5eb12b384d93790618e0ec87


Addressable

Q:看了UWA關於Addressable的相關回答後,受益匪淺,但是有兩個問題一直沒有研究明白:

1. Addressable的熱更新更像是邊玩邊下載的方案,並且還需要按照特定的部署方式。對一些資源很大的遊戲來說,一般都是啓動時集中下載,把所有的增量資源打包成Zip,下載解壓到persistentDataPath目錄中。是不是如果把RemoteLoadPath設置爲file://的地址,就可以先嚐試加載增量資源,再加載包內地址呢?

2. 仍舊是打包顆粒度的問題,如果把所有美術資源打成AseetBundle,雖然沒有冗餘,但是顆粒度很大。一般的資源可以分爲動態加載的資源,以及引用加載的資源,例如一些紋理和模型,動態加載的資源都需要打包,而引用的資源,如果多個動態資源引用,則單獨打成AseetBundle,如果只有一個或者幾個引用,則由引用的資源一起打成AssetBundle。這是關於平衡顆粒度和冗餘的問題,這個問題Addressable可以解決嗎?可以自己根據引用計數來做顆粒度控制嗎?

A1:說一下第一個問題:Addressable是支持增量加載的。Addressable進行遠程加載時,使用UnityWebRequestAssetBundle.GetAssetBundle(url)來下載AssetBundle,UnityWebRequestAssetBundle是內部支持Caching的。所以當你的AssetBundle已經下載到本地進行緩存過以後,UnityWebRequestAssetBundle再加載同一個AssetBundle的時候就能自動從本地進行加載了,具體情況可以查看官方文檔

預先下載的思路大致如下:
在Addressable Settings裏面將Disable Catalog Update on Startup勾選上,這樣就可以在第一次更新的時候手動判斷遠程的Catalog是否有更新。如果獲取到遠程的Catalong有更新之後,再根據剛剛更新的Catalog來下載AssetBundle。推薦一下黃程寫的文章《Addressable系統解析及實踐經驗》

上面的代碼在Unity編輯器裏面跑的時候,percentage會不正常顯示,有時候會在某一個百分比停留很多時間,不知道是不是Addressable的Bug。沒有試過在真機上跑,所以不確定是不是編輯器獨有的問題。
感謝Xuan@UWA問答社區提供了回答

A2:關於問題2,Addressable內部自己做引用計數。至於粒度,Addressable支持按Group打包、按Lable打包,或者按目錄或文件單位打包,可以說很靈活,應該可以滿足題主的需求。
感謝黃程@UWA問答社區提供了回答

A3:1. 我測試了一下熱更新,通過Addressable.InternalIdTransformFunc實現地址的重定向,這樣就可以實現多種熱更新方案了。

  1. 顆粒度控制的問題不是靈活度的問題,在多人協作開發的時候,肯定需要減少個人操作。目前我們區分了需要通過程序加載的動態資源和動態資源的引用資源,引用的資源不會都放在Group中,否則資源量很大的時候很難操作。目前我的解決方法是,創建一個打包的方法,先對每個Group中的資源創建依賴關係進行分析,找出需要單獨打包的資源,創建一個Group,再打包。以前我也實現過,但是有個問題,例如Animator經常依賴FBX中的動作,或者依賴另一個controller,有時候會出現循環依賴。現在通過一種羣體算法分析,分析動態資源的依賴關係羣,找出最小依賴羣體,這個羣體的依賴和引用形成一個閉環,可以實現完全無冗餘,並且顆粒度最小。

感謝題主greedylin@UWA問答社區提供了回答,歡迎大家轉至社區交流:
https://answer.uwa4d.com/question/5ea2e4ba4d93790618e0ebb3


今天的分享就到這裏。當然,生有涯而知無涯。在漫漫的開發週期中,您看到的這些問題也許都只是冰山一角,我們早已在UWA問答網站上準備了更多的技術話題等你一起來探索和分享。歡迎熱愛進步的你加入,也許你的方法恰能解別人的燃眉之急;而他山之“石”,也能攻你之“玉”。

官網:www.uwa4d.com
官方技術博客:blog.uwa4d.com
官方問答社區:answer.uwa4d.com
UWA學堂:edu.uwa4d.com
官方技術QQ羣:793972859(原羣已滿員)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章