山东省住房和城乡建设部网站首页四川润邦建设工程设计有限公司网站
news/
2025/9/26 13:53:18/
文章来源:
山东省住房和城乡建设部网站首页,四川润邦建设工程设计有限公司网站,上海有多少家公司,数控机床网站建设大家好#xff0c;我是阿赵。 这篇文章我想写了很久#xff0c;是关于Unity项目使用AssetBundle加载资源时的内存管理的。这篇文章不会分享代码#xff0c;只是分享思路#xff0c;思路不一定正确#xff0c;欢迎讨论。 对于Unity引擎的资源内存管理#xff0c;我… 大家好我是阿赵。 这篇文章我想写了很久是关于Unity项目使用AssetBundle加载资源时的内存管理的。这篇文章不会分享代码只是分享思路思路不一定正确欢迎讨论。 对于Unity引擎的资源内存管理我猜很多朋友都存在一定的疑惑。疑惑的点有非常多包括资源怎样才能避免冗余怎样才能不会在内存里面创建重复的资源内存怎样才能在合适的时机把不需要的资源内存清理干净什么时候能把AssetBundle本身清理掉。 在这方面我觉得Unity官方一直没有给用户一个很好的指导。或者说使用Unity进行开发的游戏研发厂商们对引擎的使用有点超出了Unity自己的预想导致对于复杂的游戏类型Unity本身已经不能用单一的策略去处理只能提供自己的API让厂商自己想办法做管理策略了。 关于AssetBundle的使用基础网上有很多文章介绍我这里重点不是介绍基础用法所以我会直接跳过介绍。然后我这里得出了很多测试结论测试的过程我也不想逐个说明因为基本上网上都有很多同类的测试。所以接下来我会直接说出测试结果然后提出我自己的解决方案。
一、资源对内存的占用分析 通过AssetBundle加载的资源在游戏运行时会产生多种内存比较明显的有以下几种
1、AssetBundle本身的内存占用 如果是通过AssetBundle.LoadFromFile类方法加载在没有LoadAsset之前AssetBundle本身占用的内存很少只是一个头文件的数据引用。 如果是通过AssetBundle.LoadFromMemory类方法加载那么文件的二进制是完全读取进入内存的所以AssetBundle文件越大占用的内存就越大。 由于AssetBundle是互相依赖的比如一个模型的预设AssetBundle可能会依赖其他材质球、贴图、动画之类的AssetBundle所以在加载主AssetBundle时需要加载一堆依赖的AssetBundle。 值得注意的是对于同一个AssetBundle运行时是不可能重复加载的如果尝试重复加载同一个AssetBundle会有报错。所以同一个AssetBundle是不存在同时占用多份内存的情况。
2、Asset资源的内存占用 这里的Asset资源指的是通过AssetBundle.LoadAsset类方法把AssetBundle里面的具体资源读取出来之后资源比如预设Object、网格、材质球、贴图、动画、shader之类。 Asset的资源占用才是占用内存的主要部分。当资源被LoadAsset出来之后它会从二进制文件反序列化变成网格数据、贴图数据等。资源本身的复杂程度越高占用的内存就越大。 值得注意的是Asset的资源内存是可能重复的而且出现重复的条件很多。比如有以下这些情况 1.一张贴图同时被2个模型使用了不同模型单独生成了AssetBundle但这张作为共用依赖的贴图没有单独作为一个AssetBundle而是同时包含在A、B两个模型的AssetBundle里面。那么运行时同时加载A、B两个模型虽然看起来它们使用了同一张贴图实际上内存里面是有2张重复的贴图的内存的。 2.AssetBundle.Unload(false)卸载之后该AssetBundle内的资源还留在内存里再重新加载同一个AssetBundle并LoadAsset资源内存里面的资源就会有重复多份。
3、实例化对象的内存占用 这里指的实例化是指通过Object.Instantiate把预设的Object在场景里面实例化成GameObject。GameObject在场景里面肯定是有内存占用的但它对自身的资源比如网格、贴图等只是一个引用关系并不会复制多一份资源本身。所以实例化GameObject占用的内存不会很大。
二、资源管理在理想中想达到的目的 通过上面的分析Asset内存占用是最大的我们必须首先要避免重复然后不用的时候要清除内存。 实例化GameObject的内存不大但也应该在删除GameObject的时候及时清除。 AssetBundle的占用内存虽然同样不大不过如果项目很庞大加载的AssetBundle的数量很多那么可能也会占用一定量的内存。我们肯定希望在一个AssetBundle不再使用的时候把它Unload掉。 所以最理想的状态就是当我们加载一个资源会顺便加载该资源依赖的所有AssetBundle然后当我们确定某个资源不再被使用了之后就把它们在场景里面删除卸载Asset本身的内存然后卸载资源的AssetBundle和依赖的其他AssetBundle。这样内存就干净了。 这个看起来很清晰合理的目的在Unity里面实现起来却并不那么容易存在一些问题下面会逐步分析。
三、清理资源内存的API 为了方便理解管理策略先来明确一下几个不同类型的资源内存的释放API
1、AssetBundle资源卸载
AssetBundle资源的卸载是通过AssetBundle.Unload方法实现可以传入true或者false。 如果传入true会把该AssetBundle里面所有已经加载的资源都卸载掉是最干净的清理方式这样不管之前LoadAsset过什么资源都会直接被清理掉了。如果该AssetBundle的资源还在使用中那么这些资源都将会丢失。 如果传入false那么单纯AssetBundle本身的引用内存会被清理掉但LoadAsset的资源会保留。所以使用这种方式卸载就算场景里面还有使用资源也不会丢失。相对的清理不那么干净需要自己判断正在使用的资源什么时候释放。
2、Asset资源内存释放 要卸载Asset的资源有2个方法
1.Resources.UnloadAsset(Object assetToUnload) 这个方法可以有针对性的释放某个指定的Asset对象资源。
2.Resources.UnloadUnusedAssets() 这个方法是Unity自己管理的Unity会遍历所有的资源把所有没有被引用的资源卸载掉。这个方法消耗比较大可能会导致游戏卡顿。
3、实例化资源内存卸载 而GameObject本身的内存删除之后会根据GC回收。通过Resources.UnloadUnusedAssets()也能把不再使用的内存回收。
四、需要解决的问题 AssetBundle打包资源冗余的问题之前我也写过好几篇文章去分析怎样做才是粒度最合理又没有冗余的。这里就不再重复主要是说运行中加载卸载的问题。 从最理想的情况下来看如果我们能判断得很准确知道哪些资源已经完全没有用到了那么直接调用AssetBundle.Unload(true)资源肯定就可以清理得很干净也不需要管Asset的内存也不需要Resources.UnloadUnusedAssets。 不过如果对判断资源使用情况的准确度不是那么的自信一般是不敢直接AssetBundle.Unload(true)的因为一个不好判断错了那么还在使用的资源就会突然间全部丢失了。所以一般的处理情况是AssetBundle.Unload(false)来清理AssetBundle然后通过Resources.UnloadUnusedAssets来清理已经不再使用的Asset资源内存。 我在网上看了一些文章的经验分享有不少的建议是对于直接加载读取的资源可以读取出来Asset的Object之后把Object缓存然后立刻对该AssetBundle资源AssetBundle.Unload(false)而对作为依赖加载的AssetBundle做计数器处理。 我个人认为这个做法对于主AssetBundle是prefab类的对象是可以的。因为这些预制体都是需要实例化到场景里面只需要在游戏里面做对象池就可以很容易的知道资源是否还有在使用也很容易做计数器。 但如果是对于纯资源类的AssetBundle比如图片那就不太行了。比如我从AssetBundle里面加载一张图片然后立刻Unload(false)那么这张图片的内存在什么时候清理呢 如果通过代码将图片存起来反复使用那么很难判断图片现在还是否存活举个例子我把一张图片加载完之后赋予给了一个模型然后模型本身被主动删除了或者切换场景被删除了我们是不知道图片还在模型身上被删除的除非每次模型被Destroy的时候都遍历身上所有的Component然后逐个资源类型去判断。那么被代码引用着的这张图片就永远不会回收。如果不用代码存起来反复需要读取这张图片时由于之前的AssetBundle被unload(false)了如果重新加载AssetBundle那么这张相同的图片就会在内存里面生成多份不同的内存。 通过上面的分析可以看出实际上我们需要解决的问题只有一个就是怎样很准确的知道我们的资源是否还在存活是否还被引用是否可以释放。我们需要一个很准确的依据。 Unity本身肯定是知道的因为在使用Resources.UnloadUnusedAssets的时候Unity会把没用引用的资源都回收掉。但可惜的是Unity没有提供直接的接口给我们查询。
五、弱引用计数 C#有一个弱引用(WeakReference)的机制可以判断一个对象是否存活。 具体的用法是
WeakReference weakRefObj new WeakReference(obj);然后通过weakRefObj.IsAlive可以判断该对象是否存活然后通过weakRefObj.Target可以取到之前存进去的那个资源对象。 当一个对象在内存里面已经不存在的时候这个弱引用对象的IsAlive会变成false然后Target会变成null。 不过一般情况下Asset本身是不会自然变成不存在于内存的因为如果不是执行Resources.UnloadAsset或者Resources.UnloadUnusedAssets资源并不会随着GameObject的删除而回收的。所以正常的情况下想要弱引用能正确判断资源已经不存活必须在删除GameObject时候调用Resources.UnloadUnusedAssets。 所以如果正常的使用方法应该是用从AssetBundle里面LoadAsset得到的asset的对象创建一个弱引用对象然后如果需要使用该对象作为资源或者实例化对象的时候先判断弱引用资源的IsAlive和Target在保证IsAlive为true并且Target不为null的情况下可以取得Target去使用。如果IsAlive为false或者Target为null时证明这个弱引用关联的资源已经没有任何人在使用了就可以删除当前的弱引用对象。 不过这个弱引用对象在Unity里面使用是有问题的在Unity的自带文档的Overview of .NET in Unity页面里面有这么一句说明 Unity does not currently support the use of the C# WeakReference class with UnityEngine.Objects. For this reason, you should not use a WeakReference to reference a loaded asset. See Microsoft’s WeakReference documentation for more information on the WeakReference class. 看了Unity的这个说明对刚刚找到希望的我们来说简直是晴天霹雳。不过究竟Unity对弱引用不支持到什么程度呢 我自己测试过实际上在大部分情况下弱引用对于Unity的Asset对象都是能正常判断的。但在有一种情况下是会出问题的 如果我们从弱引用对象的Target实例化一个GameObject然后删除这个GameObject并执行一次Resources.UnloadUnusedAssets并且在很短的时间内再次访问弱引用对象的Target(只要访问包括读取属性、打log、实例化)这时候弱引用就大概率的会出错具体出错的信息是这样的 A scripted object (script unknown or not yet loaded) has a different serialization layout when loading. (Read 32 bytes but expected 120 bytes) Did you #ifdef UNITY_EDITOR a section of your serialized properties in any of your scripts? 这个状态下弱引用里面的Target并不为空IsAlive也是true的但Target被破坏了再次取得这个Target就都是错的了所有里面的资源都会丢失掉。 上面提到了大概率会出问题是在于调用Resources.UnloadUnusedAssets之后多短时间内再次访问这个弱引用对象的Target时间并不固定。有时候同一帧访问会出错有时候又不会。 这个现象我做出了一些猜想很有可能是因为Resources.UnloadUnusedAssets本身是异步处理的会返回AsyncOperation并不是同一帧就立刻清理完所有。在这个过程中如果访问弱引用对象它还没有完全判断到资源的清空情况又一次去尝试访问这个资源本身导致Resources.UnloadUnusedAssets清理到一半失败了。
六、加载和卸载的策略 到了最后来总结一下。 关于怎样去加载和卸载AssetBundle的资源和Asset资源本身。策略可以有很多种但核心的问题基本上就只有一个就是什么时候才是真正可以释放资源内存。 通过上面的分析知道Unity并没有很明确的API接口让我们知道一个资源是否还在被引用着。弱引用虽然可以判断但存在一定的问题Unity本身是不建议我们这样做的。 那么接下来就可以分为2种可能性一种是使用弱引用来判断另外一种是不使用弱引用判断。
1、使用弱引用的情况 使用弱引用最大的问题是在Resources.UnloadUnusedAssets的过程中访问Target会导致资源释放失败并让Target永久的被破坏。其实这个时候我们只要重新去AssetBundle里面LoadAsset那么就能解决Target被破坏的问题。但由于这种情况下弱引用对象的IsAlive是true而Target也不为null导致我们不知道Target被破坏了而导致这个资源一直错下去。 为了解决这个问题我们只能严格的控制Resources.UnloadUnusedAssets的使用时机。切换场景也是会默认有UnloadUnusedAssets的调用的。 于是整体的加载和卸载过程就会变成这样 加载 1、建立一个AssetsMgr管理器里面通过AssetBundleName和AssetName作为key可以获取对应的资源。 2、逻辑层通过对象池来申请对象使用假如对象池里面不存在可用对象则去AssetsMgr里面获取Asset的Object。 3、AssetMgr里面保存的是WeakReference对象如果该key没有被保存过或者WeakReference对象的IsAlive不为true或者WeakReference对象的Target的对象为null则需要重新去AssetBundleMgr管理器加载资源并保存成为新的WeakReference。当AssetMgr里面已经有对应的WeakReference那么将会返回作为Target的Object给对象池那边实例化使用。 4、AssetBundleMgr管理器里面保存着加载过的AssetBundle和它们所有的依赖关系。如果没有加载过对应的AssetBundle将会在这里加载并返回。 卸载 1、定时去遍历AssetMgr里面的所有弱引用对象判断它们是否存活。如果已经不存活了那么推进一个等待删除的列表并等待一个短的时间再把它们真正清理。不立刻清理的原因是怕当前帧里面判断到不使用然后立刻又有地方请求使用。 2、关于AssetBundle的卸载按道理使用的Object已经存到弱引用管理里面所以AssetBundle在加载完Asset的Object之后就可以直接Unload(false)了。不过引用的资源的AssetBundle应该存起来并通过计数器记录现在的使用情况。如果弱引用里面的主Asset被清理了那么应该通知依赖AssetBundle计数器减一。当依赖AssetBundle的计数器为0时则应该去卸载对应的AssetBundle。 特殊处理 由于弱引用存在一定的问题所以要避免Resources.UnloadUnusedAssets或者切换场景之后立刻去判断弱引用的存活。 如果是可以频繁切换场景的游戏比如回合制游戏每次战斗都需要切换场景。那么可以用切换场景作为时机每次切换场景时候等于是Resources.UnloadUnusedAssets了一次然后等待Resources.UnloadUnusedAssets完全结束之后才开始时机加载模型并判断弱引用的使用情况。 如果是不会切换地图场景的游戏比如大地图SLG那种建议是所有资源都做成异步加载定时的执行Resources.UnloadUnusedAssets然后在等待Resources.UnloadUnusedAssets执行完成之前先把所有异步加载的请求和弱引用判断停掉等待Resources.UnloadUnusedAssets结束再继续执行弱引用加载请求和清理。
2、不使用弱引用的情况 如果不敢使用弱引用计数的话那么纯资源类型的引用就不好精确判断。
GameObject类资源
我们还是从对象池入手。 1.对于GameObject类的对象我们做场景对象池在每个对象需要实例化的时候去AssetsMgr获取Object。而AssetsMgr里面同样是通过AssetBundleName和AssetName作为key 去保存资源不过这时候就不是保存一个弱引用对象而是保存一个计数器类对象里面会保存Object本身还有这个Object被实例化的次数。然后Object的GetInstanceID获得唯一Id通过这个id也同样存一份对应计数器对象的字典。 2.当Object不存在的时候去加载AssetBundle并且加载依赖AssetBundle。然后LoadAsset出资源Object主AssetBundle执行Unload(false)依赖的AssetBundle计数器加1。 3.当对象实例化的时候在实例化对象身上挂一个脚本里面记录着实例化这个GameObject的Object的InstanceID脚本Awake的时候抛出创建事件InstanceID作为参数。AssesMgr里面监听这个事件对InstanceID的引用加一。 4.当对象被删除的时候身上的脚本的OnDestroy方法会抛出删除事件InstanceID作为参数。AssetsMgr监听这个事件并对InstanceID的引用减一。 5.定时去检查AssetsMgr里面的InstanceID计数器如果有计数器数量为0的情况就推入待删除队列下一帧就把InstanceID对应的计数器删除并且把该AssetBundle的依赖AssetBundle计数器减一再检查依赖AssetBundle的计数器有等于0 的执行Unload(false)。
纯资源 对于纯资源类的Asset比如图片之类的如果不用弱引用就只有有所取舍了。可以考虑一下这两种方案 1、读取了AssetBundle里面的图片之后AssetBundle本身立刻Unload(false)图片保存在代码里面复用。然后通过最后一次申请调用的时间去判断大于一定时间之后就清理掉。 这样做的好处是AssetBundle本身的内存不会再占用了然后如果很久没有人申请的资源也只是在代码层面去掉引用如果场景里面还有也不会受到影响。 坏处是如果一张图片在场景里面长久都没删除代码引用被清理掉了。下次又申请了同一张图片使用就要重新在AssetBundle加载一次内存里面就会出现重复资源。 2、纯资源的asset对象不保存在代码引用它们的AssetBundle也完全不卸载这样每次需要使用该图片的时候都从AssetBundle里面LoadAsset。然后定时执行Resources.UnloadUnusedAssets让没有在使用的图片Asset内存得到释放。 这样做的好处是由于每次请求的资源都是从同一次加载的AssetBundle里面读取的所以同一张图片的内存肯定不会有多份重复的。 这样做的坏处是引用AssetBundle占用内存由于没有释放的时机会一直占用内存。不过实际上AssetBundle本身如果是通过LoadFromFile方法加载的话AssetBundle不会占用太多的内存。 至于LoadFromMemory类的方法加载原则上是不建议这么做的不过有些时候却迫不得已比如要加密AssetBundle文件或者混淆AssetBundle文件就需要在文件的二进制里面做修改然后使用时通过读取完整的二进制再进行解密。这种情况下就肯定不能不释放AssetBundle文件了不然内存的占用就非常大。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/917012.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!