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

侑虎科技 · 2020年06月01日 · 1765 次阅读

1)FMOD 热更新在安卓下的堆内存占用
2)优化 MeshSkinning.Render 的 Draw Call
3)通过 UnityWebRequest 的 API 下载 AssetBundle 并进行本地缓存
4)如何选择 DOTS 项目的热更新方案
5)Addressable 的热更新和打包问题


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问答社区提供了回答


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 提供


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 问答社区提供了回答


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 问答社区提供了回答


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 问答社区提供了回答


今天的分享就到这里。当然,生有涯而知无涯。在漫漫的开发周期中,您看到的这些问题也许都只是冰山一角,我们早已在 UWA 问答网站上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入,也许你的方法恰能解别人的燃眉之急;而他山之 “石”,也能攻你之 “玉”。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册