本系列文章翻译自:
https://learn.unity.com/tutorial/assets-resources-and-assetbundles

这是一系列文章,深入讨论了Unity引擎中的资产(Asset)和资源管理(Resource Management)。旨在为高级开发人员提供Unity资产和序列化系统的深入原理的知识。研究了Unity的AssetBundle系统的技术基础和当前使用它们的最佳使用方法。

该指南分为四章:

  1. 资产,对象和序列化(Assets,Objects and serialization)讨论了Unity如何序列化资产和处理资产之间引用的底层细节。强烈建议读者从本章开始,因为它介绍了整个指南中使用的术语。
  2. Resources 文件夹 讨论了内置的Resources API。
  3. AssetBundle 基础知识 是建立在第1章的知识基础上,用于描述 AssetBundles 的运作方式,并讨论 AssetBundles 的加载和 AssetBundles 的资产加载。
  4. AssetBundle 使用指南 是一篇长篇文章,讨论 AssetBundles 实际使用的许多方式。包括将有关将资产分配给 AssetBundles 和管理已加载资产的部分,并描述了使用 AssetBundles 的开发人员遇到的许多常见问题。

注意:本指南的 ObjectsAssets 术语与Unity的API中命名有所不同。

本指南调用 Objects 的数据在许多Unity API 中称为Assets,例如AssetBundle.LoadAssetResources.UnloadUnusedAssets。本指南称之为 Assets 的文件很少暴露给任何公共API。当它们被公开时,通常只在与构建相关的代码中,例如AssetDatabaseBuildPipeline。在这些情况下,它们在公共API 中称为文件(files)

0×00 前言

本章讨论 AssetBundles。它介绍了构建 AssetBundle 的基本系统,以及用于与 AssetBundles 交互的核心API。特别是,它讨论了 AssetBundles 本身的加载和卸载,以及 AssetBundles 中特定 Asset 和 Objects 的加载和卸载。

有关 AssetBundles 使用的更多模式和最佳实践,请参阅本系列的下一章

0×01 概观

AssetBundle 系统提供了一种以 Unity 可以索引和序列化的存档格式存储一个或多个文件的方法。AssetBundles 是 Unity 在安装后交付和更新非代码内容的主要工具。这允许开发人员提交较小的应用程序包,减少运行时内存压力,并有选择地加载针对最终用户设备优化的内容。

了解 AssetBundles 的工作方式对于为构建成功的移动设备 Unity 项目至关重要。有关 AssetBundle 内容的总体描述,请查看 AssetBundle文档

0×02 AssetBundle布局

总而言之,AssetBundle 由两部分组成:标题和数据段。

标头包含有关 AssetBundle 的信息,例如其标识符,压缩类型和清单。清单是一个由 Object 名称键入的查找表。每个条目都提供一个字节索引,指示在 AssetBundle 的数据段中可以找到给定 Object 的位置。在大多数平台上,此查找表实现为平衡搜索树。具体来说,Windows 和 OSX 派生的平台(包括iOS)使用红黑树。因此,随着AssetBundle 内资产数量的增加,构建清单所需的时间将超过线性的增加。

数据段包含通过序列化 AssetBundle 中的资产生成的原始数据。如果将 LZMA 指定为压缩方案,则会压缩所有序列化资产的完整字节数组。如果指定了 LZ4,则单独压缩单独 Assets 的字节。如果未使用压缩,则数据段将保留为原始字节流。

在 Unity 5.3 之前,无法在 AssetBundle 中单独压缩对象。因此,如果指示5.3之前的Unity版本从压缩的AssetBundle 读取一个或多个对象,则Unity必须解压缩整个 AssetBundle。通常,Unity 会缓存 AssetBundle 的解压缩副本,以提高同一个 AssetBundle 上后续加载请求的加载性能。

0×03 加载AssetBundles

AssetBundles可以通过四个不同的API加载。这四种API的行为因两个标准而异:

  1. AssetBundle 是否为 LZMA 压缩,LZ4 是压缩还是未压缩;
  2. 正在加载AssetBundle的平台;

这些API是:

  • AssetBundle.LoadFromMemoryAsync
  • AssetBundle.LoadFromFileAsync
  • UnityWebRequest的DownloadHandlerAssetBundle
  • WWW.LoadFromCacheOrDownload(在Unity 5.6或更早版本上)

1. AssetBundle.LoadFromMemory(Async)

Unity建议不要使用此API。

AssetBundle.LoadFromMemoryAsync 从托管代码字节数组(C#中的byte [])加载 AssetBundle。它总是将源数据从托管代码字节数组复制到新分配的,连续的本机内存块中。如果 AssetBundle 是 LZMA 压缩的,它将在复制时解压缩 AssetBundle。未压缩和LZ4压缩的 AssetBundles 将逐字节复制。

此 API 消耗的最大内存量至少是 AssetBundle 大小的两倍:API创建的本机内存中的一个副本,以及传递给 API 的托管字节数组中的一个副本。因此,通过此 API 创建的 AssetBundle 加载的资产将在内存中重复三次:一次在托管代码字节数组中,一次在 AssetBundle 的本机内存副本中,第三次在 GPU 或系统内存中用于资产本身。

在Unity 5.3.3之前,此API称为 AssetBundle.CreateFromMemory 。它的功能没有改变。

2. AssetBundle.LoadFromFile(Async)

AssetBundle.LoadFromFile 是一种高效API,用于从本地存储(如硬盘或SD卡)加载未压缩或 LZ4 压缩的 AssetBundle。

在桌面独立,控制台和移动平台上,API 将仅加载 AssetBundle 的标头,并将剩余数据保留在磁盘上。当调用加载方法(例如 AssetBundle.Load )或取消引用其 InstanceID 时,AssetBundle 的对象将按需加载。在这种情况下,不会消耗多余的内存。在 Unity Editor 中,API 会将整个 AssetBundle 加载到内存中,就像从磁盘读取字节并使用 AssetBundle.LoadFromMemoryAsync 一样。如果在 Unity Editor 中对项目进行了分析,则此 API 可能会导致在 AssetBundle 加载期间出现内存峰值。这不应该影响设备上的性能,并且应该在采取补救措施之前在设备上重新测试这些峰值信息。

注意:在使用 Unity 5.3 或更早版本的 Android 设备上,尝试从 Streaming Assets 路径加载 AssetBundle 时,此API 将失败。Unity 5.4 中已解决此问题。有关更多详细信息,请参阅 AssetBundle使用指南 步骤的 “ 分发 - 与项目同时安装” 部分。

在Unity 5.3之前,这个API被称为 AssetBundle.CreateFromFile。它的功能没有改变。

3. AssetBundleDownloadHandler

UnityWebRequest API允许开发人员指定统一究竟应该如何处理下载的数据,并允许开发者以消除不必要的内存使用情况。使用 UnityWebRequest 下载 AssetBundle 的最简单方法是调用UnityWebRequest.GetAssetBundle

出于本指南的目的,相关的类是 DownloadHandlerAssetBundle。使用工作线程,它将下载的数据流式传输到固定大小的缓冲区,然后将缓冲的数据假脱机到临时存储或 AssetBundle 缓存,具体取决于下载处理程序的配置方式。所有这些操作都在本机代码中进行,从而消除了扩展托管堆的风险。此外,该下载处理程序并没有把所有下载的字节存储进本机代码副本,进一步降低了下载的 AssetBundle 的内存开销。

LZMA 压缩的 AssetBundles 将在下载期间解压缩并使用 LZ4 压缩进行缓存。可以通过设置Caching.CompressionEnabled 来更改此行为。

当下载完成后,AssetBundle 下载处理程序提供下载的AssetBundle,仿佛 AssetBundle.LoadFromFile 已经下载 AssetBundle。

如果向 UnityWebRequest 对象提供缓存信息,并且 Unity 缓存中已存在所请求的 AssetBundle,那么 AssetBundle 将立即可用,并且此 API 将与 AssetBundle.LoadFromFile 完全相同。

在Unity 5.6之前,UnityWebRequest 系统使用固定的工作线程池和内部作业系统来防止过多的并发下载。线程池的大小不可配置。在Unity 5.6中,已删除这些安全措施以适应更多现代硬件,并允许更快地访问 HTTP 响应代码和标头。

4. WWW.LoadFromCacheOrDownload

注意:从Unity 2017.1开始,WWW.LoadFromCacheOrDownload 简单地包装 UnityWebRequest。因此,使用Unity 2017.1或更高版本的开发人员应迁移到 UnityWebRequest。WWW.LoadFromCacheOrDownload 将在以后的版本中弃用。

以下信息适用于Unity 5.6或更早版本。

WWW.LoadFromCacheOrDownload 是一个允许从远程服务器和本地存储加载对象的 API。可以通过 file:// URL 从本地存储加载文件。如果 Unity 缓存中存在 AssetBundle,则此 API 的行为与 AssetBundle.LoadFromFile 完全相同。

如果尚未缓存 AssetBundle ,则 WWW.LoadFromCacheOrDownload 将从其源读取 AssetBundle。如果AssetBundle 被压缩,它将使用工作线程解压缩并写入缓存。否则,它将通过工作线程直接写入缓存。缓存 AssetBundle 后,WWW.LoadFromCacheOrDownload 将从缓存中解压缩 AssetBundle 加载标头信息。然后,API的行为与使用 AssetBundle.LoadFromFile 加载的 AssetBundle 完全相同。此缓存在WWW.LoadFromCacheOrDownloadUnityWebRequest 之间共享。使用一个 API 下载的任何 AssetBundle 也可通过其他 API 获得。

虽然数据将通过固定大小的缓冲区解压缩并写入缓存,但 WWW 对象将在本机内存中保留 AssetBundle 字节的完整副本。保留 AssetBundle 的这个额外副本以支持 WWW.bytes 属性。

由于在 WWW 对象中缓存 AssetBundle 字节的内存开销,AssetBundles 应该保持很小 - 最多只有几兆字节。有关AssetBundle 大小调整的更多讨论,请参阅 AssetBundle使用指南 一章中的 资产分配策略 部分。

与 UnityWebRequest 不同,每次调用此 API 都会产生一个新的工作线程。因此,在具有有限内存的平台(例如移动设备)上,应该使用此 API 一次只下载一个 AssetBundle,以避免内存峰值。多次调用此 API 时,请注意避免创建过多的线程。如果需要下载超过5个 AssetBundle,请在脚本代码中创建和管理下载队列,以确保仅同时运行少量 AssetBundle 下载。

5. 建议

通常,应尽可能使用 AssetBundle.LoadFromFile。在速度,磁盘使用和运行时内存使用方面,此 API 是最有效的。

对于必须下载或修补 AssetBundles 的项目,强烈建议对使用 Unity 5.3 或更高版本的项目使用UnityWebRequest,对使用 Unity 5.2 或更早版本的项目使用 WWW.LoadFromCacheOrDownload。如“ 分发” 部分所述,可以使用项目安装程序中包含的 Bundle 来填充 AssetBundle Cache。

使用 UnityWebRequestWWW.LoadFromCacheOrDownload 时,请确保在加载 AssetBundle 后下载程序代码正确调用 Dispose 。或者,C#的 using 语句是确保安全地处理 WWWUnityWebRequest 的最方便的方法。

对于需要具有独特,特定缓存或下载要求的大型工程团队的项目,可以考虑使用自定义下载程序。编写自定义下载程序是一项非常重要的工程任务,任何自定义下载程序都应与 AssetBundle.LoadFromFile 兼容有关更多详细信息,请参阅下一步的“ 分发” 部分。

0×04 从 AssetBundles 加载资产

UnityEngine.Objects 可以使用三个不同的 API 从 AssetBundles 加载,这些 API 都附加到 AssetBundle 对象,它们具有同步和异步变体:

这些 API 的同步版本将始终比其异步版本快至少一帧。

异步负载将每帧加载多个对象,直到其时间达到上限。有关此行为的基本技术原因,请参阅低级加载详细信息部分。

LoadAllAssets 应该加载多个独立 UnityEngine.Objects 时使用。它只应在需要加载 AssetBundle 中的大多数或所有对象时使用。与其他两个API相比,LoadAllAssets 比对 LoadAssets 的多个单独调用稍快。因此,如果要加载的资产数量很大,但是一次只需要加载少于66%的 AssetBundle,请考虑将 AssetBundle 拆分为多个较小的包并使用 LoadAllAssets

加载包含多个嵌入对象的复合资产时,应使用 LoadAssetWithSubAssets ,例如具有嵌入动画的 FBX 模型或嵌入了多个 spirit 的精灵图集。如果需要加载的对象全部来自同一资产,但存储在具有许多其他无关对象的 AssetBundle中,则使用此 API。

对于任何其他情况,请使用 LoadAssetLoadAssetAsync

1. 低级加载详细信息

UnityEngine.Object 加载是在主线程上执行的:从工作线程的存储中读取 Object 的数据。任何不接触 Unity 系统的线程敏感部分(Scripts,Graph)的东西都将在工作线程上转换。例如,VBO将从网格创建,纹理将被解压缩等。

从 Unity 5.3 开始,对象加载已经并行化。多个对象在工作线程上反序列化,处理和集成。当 Object 完成加载时,将调用其 Awake 回调,并且 Object 将在下一帧期间对 Unity Engine 的其余部分可用。

同步 AssetBundle.Load 方法将暂停主线程,直到对象加载完成。它们还会对对象加载进行时间分片,以便对象集成不占用超过一定毫秒的帧时间。毫秒数由属性 Application.backgroundLoadingPriority 设置:

  • ThreadPriority.High :每帧最多50毫秒
  • ThreadPriority.Normal :每帧最多10毫秒
  • ThreadPriority.BelowNormal :每帧最多4毫秒
  • ThreadPriority.Low :每帧最多2毫秒。

从 Unity 5.2 开始,加载多个对象,直到达到对象加载的帧时间限制。假设所有其他因素相等,资产加载API的异步变体将始终比可比较的同步版本更长,因为发出异步调用和引擎可用的对象之间的最小一帧延迟。

2. AssetBundle依赖项

AssetBundle 之间的依赖关系使用两个不同的API自动跟踪,具体取决于运行时环境。在 Unity Editor 中,可以通过 AssetDatabase API 查询 AssetBundle 依赖项。可以通过 AssetImporter API 访问和更改 AssetBundle 分配和依赖项。在运行时,Unity 提供了一个可选 API,用于通过基于 ScriptableObject 的 AssetBundleManifest API 加载在 AssetBundle 构建期间生成的依赖关系信息。

当一个或多个父 AssetBundle 的 UnityEngine.Object 引用一个或多个其他 AssetBundle 的 UnityEngine.Object 时,AssetBundle 依赖于另一个 AssetBundle。有关对象间引用的更多信息,请参阅“ 资产,对象和序列化” 步骤的“ 对象间引用” 部分。

如该步骤的序列化和实例部分所述,AssetBundles 充当由 AssetBundle 中包含的每个 Object 的 FileGUID和 LocalID 标识的源数据的源。

因为在首次取消引用其实例 ID 时加载了 Object,并且因为在加载 AssetBundle 时为 Object 分配了有效的实例 ID,所以 AssetBundle 的加载顺序并不重要。相反,在加载 Object 本身之前加载包含 Object 依赖关系的所有 AssetBundle 非常重要。加载父 AssetBundle 时,Unity 不会尝试自动加载任何子 AssetBundle。

例:

假设材料A 是指组织B 。材料A打包到 AssetBundle 1 中,纹理B打包到 AssetBundle 2 中。

在此用例中,必须在从 AssetBundle 1 加载材料A之前加载 AssetBundle 2。

这并不意味着必须在 AssetBundle 1 之前加载 AssetBundle 2,或者必须从 AssetBundle 2 显式加载 Texture B,在将 Asset A 加载到 AssetBundle 1 之前加载 AssetBundle 2 就足够了。

但是,在加载 AssetBundle 1 时,Unity 不会自动加载 AssetBundle 2。这必须在脚本代码中手动完成。

有关 AssetBundle 依赖关系的更多信息,请参阅手册页

3. AssetBundle构建

使用 BuildPipeline.BuildAssetBundles API 执行 AssetBundle 构建管道时,Unity 会序列化包含每个 AssetBundle 的依赖项信息的 Object。此数据存储在单独的 AssetBundle 中,该 AssetBundle 包含AssetBundleManifest类型的单个 Object 。

此资产将存储在 AssetBundle 中,其名称与构建 AssetBundle 的父目录的名称相同。如果项目将 AssetBundles 构建到 (projectroot)/ build / Client / 的文件夹,则包含清单的AssetBundle将保存为 (projectroot)/build/Client/Client.manifest

包含清单的 AssetBundle 可以像任何其他 AssetBundle 一样加载,缓存和卸载。

AssetBundleManifest 对象本身提供了 GetAllAssetBundles API,用于列出与清单同时构建的所有 AssetBundle,以及两种查询特定 AssetBundle 依赖关系的方法:

请注意,这两个 API 都分配字符串数组。因此,它们应该只是谨慎使用,而不是在应用程序生命周期的性能敏感部分使用。

4. 建议

在许多情况下,最好在玩家进入应用程序的性能关键区域(例如主游戏级别或世界)之前加载尽可能多的所需对象。这在移动平台上尤其重要,移动平台对本地存储的访问速度很慢,并且在播放时加载和卸载对象的内存流失可以触发垃圾收集器。

对于必须在应用程序交互时加载和卸载对象的项目,请参阅 AssetBundle使用指南 步骤的 管理已加载资源 部分,以获取有关卸载对象和AssetBundle的更多信息。

-EOF-