英文原文:https://unity3d.com/cn/learn/tutorials/topics/best-practices/assetbundle-fundamentals
本文介绍了AssetBundle的构建系统原理和用于访问AssetBundle的核心API,并专门对加载和卸载AssetBundle、从AssetBundle中加载和卸载特定的Asset和Object进行了讨论。
更多关于AssetBundle的使用模式了最佳实践内容,请查看本系列文章的下一篇。
AssetBundle系统提供了将一个或多个文件存储到Unity能够进行索引和序列化的档案格式中的方法,它是Unity用来在应用程序安装之后进行分发和更新非代码内容的首选工具。通过AssetBundle,开发者可以提交更小的程序安装包、最小化运行时内存压力以及根据终端用户设备选择性地加载优化内容。
关于AssetBundle地完整描述,请查看AssetBundle文档。
简单来说,一个AssetBundle中包含两部分:数据头和数据段。
数据头中含有AssetBundle的相关信息,例如标识符(Identifier)、压缩类型(Compression Type)和配置文件(Manifest)。配置文件是一个以Object名称为键的查找表,表中的每个条目头提供了一个用于标识Object在数据段中的位置的byte索引。在大多数平台上,这个查找表是用平衡查找树实现的,在Windows和OSX衍生平台(包括iOS)上的查找表是使用红黑树实现的。因此,构建配置文件所需的时间随着AssetBunlde中Asset数量而增长的速度大于线性增长。
数据段中含有由序列化AssetBundle中的Asset而生成的原始数据。如果指定了压缩方案为LZMA,所有序列化的Asset会被压缩到一个字节数组中;如果指定了压缩方案为LZ4,不同Asset的字节数据会被单独压缩;如果没有使用任何压缩,数据段会保留原始的字节流数据。[?1]
[?1]:原文:If LZMA is specified as the compression scheme, the complete byte array for all serialized assets is compressed. If LZ4 is instead specified, bytes for separate Assets are individually compressed.
在Unity 5.3之前的版本中,AssetBundle中的Object不能被单独压缩。因此,在5.3之前版本的Unity中,如果要从已压缩的AssetBundle中读取Object,引擎必须解压整个AssetBundle。通常情况下,Unity会缓存一份解压后的AssetBundle副本,以此来提高加载性能。
AssetBundle可以通过5个不同的API来加载,这5个API会受下面的两种因素的影响而产生不同的行为:
这些API是:
通过这些API获取的AssetBundle引用可以随意混用,也就是说,通过UnityWebRequest加载的AssetBundle可以兼容通过 AssetBundle.LoadFromFile 或者 AssetBundle.LoadFromMemoryAsync 加载的AssetBundle。
Unity不推荐使用这个API。
AssetBundle.LoadFromMemoryAsync从托管代码的字节数组(C#中的byte[])中加载AssetBundle。该方法总是将托管代码中的源数据复制到新分配的连续的内存区块中。如果AssetBundle使用了LZMA压缩,在复制过程中AssetBundle会被解压;如果使用了LZ4压缩或者没有压缩,AssetBundle会被逐字复制(be copied verbatim)。
这个API占用内存的峰值至少是他所处理的AssetBundle大小的两倍:一份由此API创建在本机内存中的副本和一份传递给此API的位于托管字节数组中的副本。通过此API从AssetBundle加载的Asset会在内存中被复制3次:在托管代码中的字节数组、本机内存中的副本以及在GPU或系统内存中的Asset本体。[?2]
[?2]:原文:once in the managed-code byte array, once in the native-memory copy of the AssetBundle and a third time in GPU or system memory for the asset itself.
在Unity 5.3.3之前,这个API名为 AssetBundle.CreateFromMemery,它的功能没有变化。
AssetBundle.LoadFromFile是用于从本地存储加载未压缩或使用LZ4压缩的AssetBundle的高效API。
在桌面系统(PC、Mac、Linux)、主机和移动平台上,这个API只会加载数据头,把其余的数据留在磁盘上。AssetBundle中的Object会在调用了加载方法(例如AssetBundle.Load)或者在它们的InstanceID被引用时按需加载。在这种情况下,不需要消耗额外的内存。在Unity编辑器中,这个API会将整个AssetBundle加载到内存中,就像这些字节被从磁盘中读出而且使用了 AssetBundle.LoadFromMemoryAsync 方法。如果在Unity编辑器中对项目进行分析,会发现在使用此方法加载AssetBundle时会出现内存使用高峰,在发布版本中这应该不会对设备性能造成影响。
提示:在Unity 5.3以及更早版本地Android工程中,使用此API从StreamAssets中加载AssetBundle会失败,这个问题已经在Unity 5.4中修复。更多详细内容,请查看本系列文章地第四篇中的分发-附加在项目中一节。
在Unity 5.3.3之前,这个API名为 AssetBundle.CreateFromFile,它的功能没有变化。
开发者可以使用UnityWebRequest API明确指定Unity如何处理下载的数据,并且可以免除不必要的内存占用。使用UnityWebRequest下载AssetBundle的最简单的方法是调用UnityWebRequest.GetAssetBundle方法。
DownloadHandlerAssetBundle可以使用一个工作线程(Worker Thread)将数据流存储到固定大小的缓冲中,然后再将缓冲数据存储到临时存储或者AssetBundle缓存中,具体的存储位置取决于DownloadHandler的配置。这些操作全部发生在本地代码(Native-Code)中,免除了对托管堆内存的扩张。另外,这一DownloadHandler不会保留下载的字节数据的本地代码副本,进一步降低了下载AssetBundle的内存开销。
使用LZMA压缩的AssetBundle在下载时会被解压并使用LZ4压缩进行缓存。这一行为可以通过设置Caching.CompressionEnabled来修改。
下载完成后,可以通过DownloadHandler的assetBundle属性访问下载的AssetBundle,如同对下载的AssetBundle调用 AssetBundle.LoadFromFile 方法。
如果为UnityWebRequest对象提供了缓存信息,并且Unity的缓存中已经有了所请求的AssetBundle,那么AssetBundle会立即变为可用,并且此API的运行模式和 AssetBundle.LoadFromFile 相同。
在Unity 5.6之前,UnityWebRequest系统使用一个固定的工作线程池和内部任务系统来防止重复、并发下载,线程池的大小不可配置。在Unity 5.6中,该保护措施被移除了,目的是适应现代硬件以及更快的访问HTTP响应代码和数据头。
提示:从Unity 2017.1开始,WWW.LoadFromCacheOrDownload被封装到了UnityWebRequest中。因此,在使用Unity 2017.1或者更高版本进行开发时,应该使用UnityWebRequest。WWW.LoadFromCacheOrDownload会在将来的发行版中被废弃。
下列内容适用于Unity 5.6以及更早的版本。
WWW.LoadFromCacheOrDownload可以既可以从远程服务器加载Object,也可以从本地存储加载Object。可以通过 file://
URL从本地存储加载文件。如果AssetBundle已经在Unity的缓存中,那么此API的行为将和 AssetBundle.LoadFromFile 一致。
如果AssetBundle还没有被缓存,WWW.LoadFromCacheOrDownload 会根据传入的路径读取AssetBundle。如果AssetBundle有压缩,它将会在工作线程中解压并写入到缓存中;否则,AssetBundle会在工作线程中直接写入到缓存中。一旦AssetBundle被缓存,WWW.LoadFromCacheOrDownload 会从缓存中加载数据头信息。这份缓存在 WWW.LoadFromCacheOrDownload 和 UnityWebRequest 间共享。任何使用此API下载的AssetBundle都可以通过其他API访问。
在使用固定大小的缓冲对数据进行解压并写入缓存的过程中,WWW对象会在本地内存中保留一份AssetBundle字节的完整副本,这份额外的副本用于维护WWW.bytes属性。
因为WWW对象缓存AssetBundle字节数据会造成内存开销,所以AssetBundle文件体积应该尽量小,最多几MB。关于AssetBundle体积的更多讨论,请查看本系列文章地第四篇中的Asset分配策略一节。
与UnityWebRequest不同的是,每次调用此API都会生成一个新的工作线程。因此,在内存有限的平台上,例如移动设备上,同一时刻应该只是用此API下载单个AssetBundle,以避免内存使用高峰。当多次调用此API时,小心过多地创建线程。如果需要下载的AssetBundle超过5个,在脚本中维护一个下载队列,确保同一时刻只有少量的AssetBundle在进行下载。
通常情况下,应该优先使用 AssetBundle.LoadFromFile,这一API在速度、磁盘使用和运行时内存占用方面都很高效。
如果项目必须下载AssetBundle,强烈推荐在Unity 5.3以及更新的版本中使用 UnityWebRequest、在Unity 5.2以及更早的版本中使用 WWW.LoadFromCacheOrDownload。也可以像下一篇文章中的分发一节中描述的那样将AssetBundle缓存包含在项目的安装程序中。[?3]
[?3]:原文:As detailed in the Distribution section of the next chapter, it is possible to prime the AssetBundle Cache with Bundles included within a project’s installer.
无论是在使用 UnityWebRequest 还是 WWW.LoadFromCacheOrDownload,都要确保下载AssetBundle后在合适的位置调用 Dispose 方法。C#的using语句是一个很简单并且可以确保WWW和UnityWebRequest被安全释放的方法。
对于由大型工程团队开发,并且有唯一、特定的缓存或下载需求的项目,可以考虑使用自定义的下载器。开发自定义下载器是一项非凡的工程任务,任何自定义下载器都应该兼容 AssetBundle.LoadFromFile。更多细节请查看下一篇文章中的分发章节。
可以使用AssetBundle对象的几个不同的方法来加载Object,这些方法都有同步和异步两个版本:
这3个API的同步版本都比它们的异步版本速度快,至少会快1帧。
异步方法会在每帧加载多个Object,数量上限受到们的时间片(Time-Slice)限制。详细信息请查看 3.4.1 底层加载细节。
当加载多个独立的Object时应该使用 LoadAllAssets 方法,只有在AssetBundle中的大多数或全部Object都要用到时才应该使用这个方法。与其他两个API相比,LoadAllAssets 要比多次调用 LoadAsset 略快。因此,如果要加载的AssetBundle数量非常多,但AssetBundle中需要同时使用的Object的数量不超过66%,可以考虑将AssetBundle分割成多个更小的AssetBundle然后使用 LoadAllAssets 方法。
当加载含有多个嵌套Object的复合Asset时应该使用 LoadAssetWithSubAssets,例如加载嵌入了动画的FBX文件,或者加载嵌入了多个精灵的图集。如果要加载的Object都来自于同一Asset,而他们又和很多不相关的其他Object存放在同一个AssetBundle中,那就应该使用此方法。
除了上述几种情况,都应该使用 LoadAsset 或者 LoadAssetAsync。
Object的加载不在主线程中执行:Object的数据会在工作线程中进行读取。Unity系统(脚本、图形)中任何非线程敏感的内容都会被转移到工作线程中。例如,由网格创建VBO、解压纹理等。
从Unity 5.3起,Object开始并行加载,在多个工作线程中对多个Object进行反序列化、处理和继承。当一个Object完成加载后,会调用它的 Awake 回调,并且在下一帧中对Unity引擎可用。
同步的 AssetBundle.Load 方法会暂停主线程,直到Object加载完成。它也会对Object加载进行时间切片,这样Object集成占用的每帧时间就不会超过指定的毫秒数。这一毫秒数可以通过 Application.backgroundLoadingPriority 属性设置:
从Unity 5.2开始,多个Object会同时加载,直到打到帧时间限制(Frame-time limit)。假设其他的影响因素都相同,这些资源加载API的异步变体完成任务的耗时都会比它们的同步版本长,因为发起异步调用和对象变得对引擎可用之间至少会有一帧的延迟。
AssetBundle之间的依赖关系通过两个不同的API自动进行追踪,具体取决于运行时环境。在Unity编辑器中,AssetBundle依赖可以通过AssetDatabase API查询。AssetBundle分配和依赖可以通过AssetImporter访问和修改。在运行时,Unity提供了一个可选的API用于加载在AssetBundle构建过程中通过AssetBundleManifest API生成的依赖信息。
当一个AssetBundle的父AssetBundle中的Object引用了其他AssetBundle中的Object时,前者会依赖后者。更多关于Object内部引用的信息,请查看本系列文章的第一篇资源(Assets)、对象(Objects)和序列化中的Object间的引用章节。
就像第一篇文章的序列化与实例章节所描述的那样,AssetBundle是它所包含的通过每个Object的FileGUID和LocalID标识的源数据的数据源。
因为Object会在它的Instance ID首次被引用时进行加载,而且当加载AssetBundle时Object会被分配一个合法的Instance ID,所以加载AssetBundle的顺序并不重要。但是,在加载Object自身之前加载他所依赖的所有AssetBundle很重要。当加载了父AssetBundle后,Unity不会尝试去自动加载任何子AssetBundle。
示例:
假设材质A引用了纹理B;材质A被打包到AssetBundle 1中,纹理B被打包到AssetBundle 2中。
在这个用例中,在从AssetBundle 1加载材质A之前,必须先加载AssetBundle 2。
但这不意味着在加载AssetBundle 1之前必须先加载AssetBundle 2或者从AssetBundle 2中加载纹理B。只要在从AssetBundle 1中加载材质A之前加载了AssetBundle 2就足够了。
然而,Unity不会在加载了AssetBundle 1之后自动加载AssetBundle 2。这必须在脚本代码中手动实现。
更多关于AssetBundle依赖的信息,请查看AssetBundle依赖手册。
在使用 BuildPipeline.BuildAssetBundles API执行AssetBundle构建管线时,Unity会序列化一个包含每个AssetBundle的依赖信息的Object,它被单独存放到一个AssetBundle中,这个AssetBundle中只有一个AssetBundleManifest类型的Object。
这一Asset会存储在与构建AssetBundle时的目录同名的AssetBundle中。如果项目在 (projectroot)/build/Client/ 文件夹中构建了AssetBundle,那么包含了配置表的AssetBundle会被保存为 (projectroot)/build/Client/Client.manifest。
包含配置表的AssetBundle可以像其他AssetBundle一样被加载、缓存和卸载。
AssetBundleManifest对象本身提供了GetAllAssetBundles API来列出所有和配置表同时构建的AssetBundle,还提供了两个用于查询指定AssetBundle的依赖的方法:
注意,这两个方法都会分配字符串数组,因此,应该少用它们,并且不要在应用程序的性能敏感时期使用它们。
在很多情况下,最好在玩家进入应用程序的性能临界区之前尽量多的加载所需的Object,例如主要的游戏关卡或世界内容。这在移动设备上尤其关键,因为访问本地存储较慢,而且在游戏时加载和卸载Object可能触发垃圾回收器工作。
如果应用程序必须在进行人机交互时加载和卸载Object,请查看下一篇文章的管理已加载资源章节来了解更多关于下载Object和AssetBunble的信息。