AssetBundle基本原理
版本检查:2017.3 -难度:高级
这是涵盖Unity5资产,资源和资源管理系列文章的第四章。
本章讨论AssetBundles。它介绍了构建AssetBundles的基本系统,以及用于与AssetBundles进行交互的核心API。具体来说,它讨论了AssetBundles本身的加载和卸载以及AssetBundles中特定资产和对象的加载和卸载。
有关AssetBundles使用的更多模式和最佳实践,请参阅本系列的下一章。
3.1.概观
AssetBundle系统提供了一种方法,用于存储Unity可以索引和序列化的档案格式的一个或多个文件。AssetBundles是Unity的主要工具,用于在安装后交付和更新非代码内容。这允许开发人员提交更小的应用程序包,最大限度地减少运行时内存压力,并有选择地加载为最终用户设备优化的内容。
了解AssetBundles的工作方式对于为移动设备构建成功的Unity项目至关重要。有关AssetBundle内容的总体说明,请查看AssetBundle文档。
3.2.AssetBundle设计
总而言之,一个AssetBundle由两部分组成:头部和数据段。
头部包含有关AssetBundle的信息,例如标识符,压缩类型和清单。清单是一个由对象名称键入的查找表。每个条目都提供一个字节索引,用于指示在AssetBundle的数据段中可以找到给定对象的位置。在大多数平台上,这个查找表被实现为一个平衡搜索树。具体来说,Windows和OSX派生的平台(包括iOS)采用红黑树。因此,构建清单所需的时间将随着AssetBundle内资产数量的增长而线性增加。
数据段包含通过序列化AssetBundle中的资产生成的原始数据。如果将LZMA指定为压缩方案,则将压缩所有序列化资产的完整字节数组。如果指定LZ4,单独资源的字节将被单独压缩。如果不使用压缩,数据段将保持为原始字节流。
在Unity 5.3之前,对象无法在AssetBundle中单独压缩。因此,如果指示5.3版本的Unity版本从压缩的AssetBundle中读取一个或多个对象,则Unity必须解压缩整个AssetBundle。通常,Unity会缓存AssetBundle的解压缩副本,以提高同一AssetBundle上后续加载请求的加载性能。
3.3.加载AssetBundles
AssetBundles可以通过五个不同的API加载。这五个API的行为取决于两个标准是不同的:
AssetBundle是否是LZMA压缩,LZ4压缩或未压缩
AssetBundle正在加载的平台
这些API是:
- AssetBundle.LoadFromMemory(异步可选)
- AssetBundle.LoadFromFile(异步可选)
- AssetBundle.LoadFromStream(异步可选)
- UnityWebRequest的DownloadHandlerAssetBundle
- WWW.LoadFromCacheOrDownload(在Unity 5.6或更早版本上)
这些API中的AssetBundle引用可以自由混合。也就是说,使用UnityWebRequest加载的AssetBundles与通过AssetBundle.LoadFromFile或AssetBundle.LoadFromMemoryAsync加载的AssetBundles兼容。
3.3.1 AssetBundle.LoadFromMemory(异步)
Unity的建议是不要使用这个API。
AssetBundle.LoadFromMemoryAsync从托管代码字节数组在C#中的字节[])中加载一个AssetBundle它将始终将来自托管代码字节数组的源数据复制到新分配的连续本机内存块中。如果AssetBundle是LZMA压缩的,它将在复制时解压缩AssetBundle。未压缩的和LZ4压缩的AssetBundles将被逐字复制。
此API消耗的最大内存量至少为AssetBundle的两倍:由API创建的本地内存中的一个副本,以及传递给API的托管字节数组中的一个副本。因此,从通过此API创建的AssetBundle加载的资产将在内存中重复三次:一次位于托管代码字节数组中,一次位于AssetBundle的本机内存副本中,第三次位于GPU或系统内存中用于资产本身。
在Unity 5.3.3之前,这个API被称为AssetBundle.CreateFromMemory。它的功能没有改变。
3.3.2.AssetBundle.LoadFromFile(异步)
AssetBundle.LoadFromFile是一个高效的API,用于从本地存储器(如硬盘或SD卡)加载未压缩或LZ4压缩的AssetBundle。
在桌面独立,控制台和移动平台上,API只会加载AssetBundle的标题,并将剩余的数据保留在磁盘上。AssetBundle的对象将在加载方法(例如AssetBundle.Load)被调用或InstanceID被解除引用时按需加载在这种情况下不会消耗过量的内存。在Unity编辑器中,API会将整个AssetBundle加载到内存中,就好像从磁盘读取字节并使用AssetBundle.LoadFromMemoryAsync一样。如果在Unity编辑器中对项目进行概要分析,则此API可能导致在AssetBundle加载期间出现内存尖峰。这不应该影响设备性能,并且应该在采取补救措施之前在设备上重新测试这些尖峰。
注意:在Unity 5.3或更早版本的Android设备上,尝试从Streaming Assets路径加载AssetBundles时,此API将失败。Unity 5.4中已解决该问题。有关更多详细信息,请参阅AssetBundle使用模式>章节中的分发 - 随项目一起提供的部分。
在Unity 5.3之前,这个API被称为AssetBundle.CreateFromFile。其功能尚未更改。
3.3.3.AssetBundleDownloadHandler
该UnityWebRequestAPI允许开发人员指定统一究竟应该如何处理下载的数据,并允许开发者以消除不必要的内存使用情况。使用UnityWebRequest下载AssetBundle的最简单方法是调用。
就本指南而言,感兴趣的类是DownloadHandlerAssetBundle使用工作线程,它会将下载的数据流式传输到固定大小的缓冲区,然后根据下载处理程序的配置方式将缓冲的数据缓冲到临时存储或AssetBundle缓存所有这些操作都以本机代码形式进行,消除了扩展托管堆的风险。此外,该下载处理程序并没有把所有下载的字节的本机代码副本,进一步降低了下载的AssetBundle的内存开销。
LZMA压缩的AssetBundles将在下载过程中进行解压缩并使用LZ4压缩进行缓存。通过设置Caching.CompressionEnabled可以更改此行为。
当下载完成后,assetBundle下载处理程序的属性提供下载的AssetBundle,仿佛已经呼吁下载AssetBundle。
如果将缓存信息提供给UnityWebRequest对象,并且所请求的AssetBundle已经存在于Unity的缓存中,则AssetBundle将立即变为可用,并且此API将以与AssetBundle.LoadFromFile相同的方式运行。
在Unity 5.6之前,UnityWebRequest系统使用固定的工作线程池和内部作业系统来防止过度的并发下载。线程池的大小不可配置。在Unity 5.6中,这些保护措施已被删除,以适应更多现代硬件,并允许更快地访问HTTP响应代码和标头。
3.3.4.WWW.LoadFromCacheOrDownload
注意: 从Unity 2017.1开始,WWW.LoadFromCacheOrDownload 只是包装UnityWebRequest,因此,使用Unity 2017.1或更高版本的开发人员应迁移到UnityWebRequest。
WWW.LoadFromCacheOrDownload将在未来版本中弃用。
以下信息适用于Unity 5.6或更早版本。
WWW.LoadFromCacheOrDownload是一个API,允许从远程服务器和本地存储装载对象。文件可以通过file:// URL从本地存储中加载。如果AssetBundle存在于Unity缓存中,则此API的行为与AssetBundle.LoadFromFile完全相同。
如果AssetBundle尚未缓存,然后WWW.LoadFromCacheOrDownload将读取其源AssetBundle。如果AssetBundle是压缩的,它将使用工作线程解压缩并写入缓存。否则,它将通过工作线程直接写入缓存。一旦AssetBundle被缓存,WWW.LoadFromCacheOrDownload将从缓存的解压缩的AssetBundle中加载标题信息。然后,API将与使用AssetBundle.LoadFromFile加载的AssetBundle的行为相同该缓存在WWW.LoadFromCacheOrDownload和UnityWebRequest之间共享任何通过一个API下载的AssetBundle也将通过其他API提供。
虽然数据将通过固定大小的缓冲区解压缩并写入缓存,但WWW对象将在本机内存中保留AssetBundle字节的完整副本。AssetBundle的额外副本保留为支持WWW.bytes属性。
由于在WWW对象中缓存AssetBundle字节的内存开销,AssetBundles应该保持很小 - 最多几兆字节。有关AssetBundle大小的更多讨论,请参阅AssetBundle使用模式章节中的资产分配策略部分。
与UnityWebRequest不同,每次调用此API都会产生一个新的工作线程。因此,在移动设备等内存有限的平台上,每次只能使用此API下载一个AssetBundle,以避免内存高峰。多次调用此API时,请小心创建过多的线程。如果需要下载超过5个AssetBundle,请在脚本代码中创建并管理下载队列,以确保只有少量AssetBundle下载正在同时运行。
3.3.5.建议
一般情况下应尽可能使用AssetBundle.LoadFromFile。就速度,磁盘使用情况和运行时内存使用情况而言,此API是最高效的。
对于必须下载或修补AssetBundles的项目,强烈建议对于使用Unity 5.3或更新版本UnityWebRequest,对于使用Unity 5.2或更早,则强烈建议使用WWW.LoadFromCacheOrDownload。正如下一章分发详细描述的那样,可以使用包含在项目安装程序中的Bundle来安装AssetBundle缓存。
使用UnityWebRequest 或 WWW.LoadFromCacheOrDownload时,请确保下载器代码在加载AssetBundle后正确调用Dispose。或者,C#的使用语句是确保WWW或UnityWebRequest安全处置的最便捷方式。
对于需要独特的特定缓存或下载要求的大量工程团队的项目,可以考虑使用自定义下载器。编写自定义下载程序是一项不重要的工程任务,任何自定义下载程序都应与AssetBundle.LoadFromFile兼容 。有关更多详细信息,请参阅下一章分发部分。
3.4.加载资产
UnityEngine.Objects可以使用三个不同的API从AssetBundles加载,这些API都附加到AssetBundle对象,它们同时具有同步和异步变体:
LoadAssetWithSubAssets(LoadAssetWithSubAssetsAsync)
这些API的同步版本总是比其异步版本快至少一帧。
异步加载将每帧加载多个对象,直到它们的时间片限制。有关此行为的基本技术原因,请参阅低级加载详细信息部分。
LoadAllAssets应该加载多个独立UnityEngine.Objects时使用。只有在需要加载AssetBundle中的大部分或全部对象时才能使用它。与其他两个API相比,LoadAllAssets比多个单独调用LoadAssets稍快。因此,如果要加载的资产数量很大,但需要一次加载不到66%的AssetBundle,请考虑将AssetBundle拆分为多个较小的捆绑包并使用LoadAllAssets。
加载包含多个嵌入对象的复合资产时,应使用LoadAssetWithSubAssets,例如嵌入动画的FBX模型或嵌入多个精灵的精灵图集。如果需要加载的对象全部来自同一个资产,但与许多其他不相关的对象一起存储在AssetBundle中,则使用此API。
对于任何其他情况,请使用LoadAsset或LoadAssetAsync。
3.4.1.低级加载细节
UnityEngine.Object加载是在主线程之外执行的:从工作线程的存储中读取对象的数据。任何不接触Unity系统的线程敏感部分(脚本,图形)的内容都将在工作线程上进行转换。例如,VBOs将从网格创建,纹理将被解压缩等。
从Unity 5.3开始,对象加载已经并行化。多个对象在工作线程上被反序列化,处理和集成。当一个对象完成加载时,它的Awake回调将被调用,并且该对象将在下一帧期间可用于Unity Engine的其余部分。
同步的方法将暂停主线程,直到对象加载完成。他们还会对对象加载进行时间片分割,以便对象集成不会占用超过特定数量的毫秒帧时间。毫秒数由属性Application.backgroundLoadingPriority设置:
- ThreadPriority.High:每帧最多50毫秒
- ThreadPriority.Normal:每帧最多10毫秒
- ThreadPriority.BelowNormal:每帧最多4毫秒
- ThreadPriority.Low:每帧最多2毫秒。
从Unity 5.2开始,多个对象被加载,直到达到对象加载的帧时间限制。假设所有其他因素相同,由于发出异步调用和引擎可用对象之间的最小一帧延迟,资产加载API的异步变体的完成时间总是比可比较的同步版本更长。
3.4.2AssetBundles依赖关系
AssetBundles之间的依赖关系使用两个不同的API自动跟踪,具体取决于运行时环境。在Unity编辑器中,可以通过AssetDatabaseAPI查询AssetBundle依赖关系可以通过AssetImporterAPI访问和更改AssetBundle分配和依赖关系。行时,Unity提供了一个可选的API来加载AssetBundle构建期间通过基于脚本对象的setBundleManifestAPI生成的依赖信息。
当一个或多个父AssetBundle的UnityEngine.Objects引用一个或多个其他AssetBundle的UnityEngine.Objects时,AssetBundle依赖于另一个AssetBundle。有关对象间引用的更多信息,请参阅资产,对象和序列化文章对象间引用部分。
如该文章的列化和实例部分所述,AssetBundles充当由AssetBundle中包含的每个对象的FileGUID&LocalID标识的源数据的源。
因为一个对象在其实例ID被首先取消引用时被加载,并且因为一个对象在加载其AssetBundle时被分配了有效的实例ID,所以AssetBundles加载的顺序并不重要。相反,加载对象本身之前加载包含Object的依赖关系的所有AssetBundles是非常重要的。装载父级AssetBundle时,Unity不会尝试自动加载任何子AssetBundles。
例:
假设材料A是指组织B。物料A打包到AssetBundle 1中,而质地B打包到AssetBundle 2中。
在这种使用情况下,AssetBundle 2必须在从AssetBundle 1中加载材料A之前加载。
这并不意味着必须在AssetBundle 1之前加载AssetBundle 2,或者必须从AssetBundle 2明确加载Texture B。在将Material A从MaterialBundle 1中加载之前加载AssetBundle 2就足够了。
但是,在加载AssetBundle 1时,Unity不会自动加载AssetBundle 2。这必须在脚本代码中手动完成。
有关AssetBundle依赖关系的更多信息,请参阅手册页。
3.4.3.AssetBundle清单
当使用BuildPipeline.BuildAssetBundles执行AssetBundle构建管道时Unity会序列化包含每个AssetBundle的依赖性信息的Object。此数据存储在单独的AssetBundle中,其中包含AssetBundleManifest类型的单个对象。
此资产将存储在AssetBundle中,其名称与构建AssetBundles的父目录相同。如果项目将其AssetBundles构建到(projectroot)/ build/Client/文件夹,则包含该清单的AssetBundle将被保存为(projectroot)/build/Client/Client.manifest。
包含清单的AssetBundle可以像其他任何AssetBundle一样加载,缓存和卸载。
AssetBundleManifest对象本身提供GetAllAssetBundlesAPI来列出与清单并发构建的所有AssetBundles,以及两种方法来查询特定AssetBundle的依赖关系:
AssetBundleManifest.GetAllDependencies返回AssetBundle的所有分层依赖项,其中包括AssetBundle的直接子项,子项的子项等的依赖项。
AssetBundleManifest.GetDirectDependencies只返回一个AssetBundle的直接子项
请注意,这两个API都分配字符串数组。相应地,它们只应该谨慎使用,而不应该在应用程序生命周期的性能敏感部分中使用。
3.4.4.建议
在许多情况下,在玩家进入应用程序的性能关键区域(例如主游戏级别或世界)之前,最好加载尽可能多的所需对象。这在移动平台上尤其重要,因为访问本地存储的速度很慢,而且在播放时加载和卸载对象的内存流失可能会触发垃圾收集器。
对于必须在应用程序交互时加载和卸载对象的项目,请参阅AssetBundle使用模式管理加载资产部分以获取有关卸载对象和AssetBundles的更多信息。