Asset Bundles与Resources的内存大对决

发表于2017-04-24
评论0 1.4k浏览

近期,有不少Unity开发者咨询有关Asset Bundle和旧有资源系统Resources

的问题:为何Asset Bundle加载Asset时消耗的内存要比Resources多。


首先说明一下,事实并非如此。或者说从长远来看,如果用好Asset Bundle中旧有资源系统Resources不具备的新特性,Asset Bundle的内存消耗会小的多。如果您不熟悉Asset Bundle,可以点击【阅读原文】参考Unity手册和Asset BundleResources指南。


详细问题


根据我们收到的一些Bug报告,基本上都在说同一件事情:当从一个Asset Bundle加载某个Asset时,内存使用量会突增几兆,但在使用Resources时却没发生这样的情况。重现这些Bug时,我们看到的结果也非常相似:启动时内存正常,载入Asset后内存激增,并且不会回落到原来的水平。


Asset Bundle的内存使用情况


Resources的内存使用情况


下面我们就通过内存系统及其关联关系、数据保存方式、内存使用量数值的含义和内存使用效率几个方面,为大家剖析一下这个问题的原理,帮助您更好地理解。注意:本文使用的Unity版本为Unity 5.5.0f3。


问题剖析


内存系统及关联


首先来了解提供这些数字的系统,以及它们之间的关联。


Unity的原生内存系统会使用多种大小在1MB到32MB(平均1MB到4MB)之间固定大小的块内存分配器。具体大小根据分配的工作类型而定,例如主线程还是后台线程;或者根据当前运行的平台。


保留总量(Reserved Total)是操作系统分配的所有块的总量,已用总量(Used Total)是其中正由Unity使用的内存量。每个区域标签、FMOD、Porfiler等等,表示系统列出的相应分配器或大概的外部内存。这些区域标签信息在内存分析器手册页面中可以查询。


总系统内存使用量(Total System Memory Usage)是由平台系统提供的虚拟内存大小,不支持此功能的平台会显示为0。最后,已用总并不包括对象的头部或字节对齐,而保留总量包括这些部分。因此,要对比Asset Bundle和Resources间的内存使用量,我们会主要关注已用总量和保留总量中的Unity区域标签。


数据保存方式


另外,了解Asset Bundle以及Resources数据在磁盘上的保存方式对于理解分析器的原理也十分必要。


Resources和Asset Bundle在底层数据结构上非常相似,它们都有一个用于存放每个对象序列化数据的文件,一些用于高效异步加载的额外资源文件(纹理、音频等),以及一个包含序列化对象Asset文件路径映射表。Asset Bundle将这些文件都打包在一个压缩包中,映射表则储存在Asset Bundle对象的序列化数据中。Resources将其映射表保存在一个名为ResourceManager的全局单例中,其他文件则散落在磁盘上。此外,与Resources系统不同的是,Asset可以分散在不同的Asset Bundle中,因此可以通过仅加载数据子集来最大限度地减少内存使用量


Asset Bundles


Resources


内存使用量数值含义


了解Unity的内存与文件系统之后,我们再详细解释下这些内存使用量数值的意义。


第一个突出的问题是Unity中用于Asset Bundle的保留区域增长了10MB,而用于Resources系统的则没有增长。什么原因呢?这主要是因为前面提及的块内存分配器。就这个特定的内存使用率测试而言,我们使用的是异步加载API协程的AsyncBundleLoader.cs行为。值得注意的是:这种组合实际上使用了不同的块内存分配器,而且这些块内存分配器在此时其实尚未被使用。


所以,10MB的增量来源于两个块内存分配器初始化内存块的动作,其中一个块内存分配器需要为新对象分配更多内存,所以它分配了一个4MB的内存块。两个新分配器中,一个为Asset Bundle异步加载分配了一个2MB的内存块,另一个则为类型树分配了一个4MB的内存块。这些内存块的大小是专为同时加载多个Asset与Asset Bundle而优化的。例如,您可以同时从4 - 5个Asset Bundle中加载对象,而无须为Asset Bundle异步加载或需要新块的类型树创建新的分配器。当然,这具体还是要根据Asset Bundle的大小,采取的压缩方式,以及这些包中所使用的唯一脚本类型的数量而定。


在这些分配的块中,用于类型树的4MB内存块,仅在从Asset Bundle加载对象时使用。操作完成后这个块应当会恢复。但是,由于示例中构建协程的方式,导致AssetBundleRequest对象一直处于作用域中,没有被垃圾回收器清除;而用于Asset Bundle异步加载的2MB内存块,它是读取Asset Bundle档案时的缓冲区,在没有包的内部引用后会被释放。最后的4MB内存块的使用者是负责我们所有对象存储的主分配器,因此不会被释放。


通常在一个项目中,对象的创建/删除非常频繁,我们会使用内存池来重用内存而非将其释放回分配器。观察最终卸载后保留区域的Unity数值时,您会发现使用Asset Bundle(64.1MB)与使用Resources系统(63.3MB)的差异很小,仅与块内存分配器获得新块的顺序有关。


内存使用效率


我们一直都在讨论保留内存,那Asset Bundle与Resources之间的保留内存实际使用效率差别又有多大呢?


这个问题非常简单,因为已使用中的Unity区域已经告诉了我们答案。使用Asset Bundle时,占用了保留内存中的21.7MB,而使用Resources时稍多,大概在22.2MB。此外,在卸载时,这个内存数值分别下降为20.7MB和21.2MB。所以,很显然Asset Bundles是内存利用效率方面的赢家


您可能已经注意到,Asset Bundle的使用量在卸载后要比其启动时更大(4.4MB)。这是因为前面提到的内存池的关系,因此如果您重新载入Asset Bundle与Asset,它将会回到21.7MB。而对于Resources来说,启动与卸载时的内存差异来源于舍入误差。对于Asset Bundle的块内存分配可以减少,但是要牺牲性能以及向下兼容性。正如上面提到的,为了满足加载对象所需的内存量,所以必须要分配4MB大小的内存块。剩下的6MB中,2MB用于了异步加载API。


因此,为了防止块内存分配,只要牺牲帧率使用同步API即可。最后的4MB是源于类型树的分配。这个系统会在Asset Bundle里储存在Resources系统中没有的额外数据,这些数据使Asset Bundle可以兼容更多版本的Unity,并使诸如FormerlySerializedAs这样的序列化特性正常工作。这使您可以在升级到更新的Unity版本后依然能使用相同的Asset Bundle,或仅需修改少量代码,而非重新构建它们,导致用户因为您所做的更改必须重新下载整个Asset Bundle。向BuildPipeline.BuildAssetBundlesAPI传入BuildAssetBundleOptions.DisableWriteTypeTree选项,可以禁止写入这个额外数据。


无类型树Asset Bundle同步加载


您可以用Asset Bundles 1, Resources 0试试。如果您想生成自己的数据,相关脚本已经上传到了 Github Gist,您可点击【阅读原文】下载。目前它设置为每类创建100个:纹理、Monobehavior、预制件、一个固定的随机种子,所以您在自己机器上每次运行都会生成同样的输出(但可能会和别人的不一样)。请确保您的Asset Bundle项目中没有意外包含了一个有内容的Resources文件夹,否则您的内存数值将会比预想高出两倍。试试将每类资源的数量提高至300或甚至500个进行测试。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引