FMOD热更新在安卓下的堆内存占用

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(原群已满员)