Unity最佳实践-AssetBundle基本原理

Best Practices(2) - AssetBundle fundamentals

  • 适用版本:2017.3
  • 原文地址:https://unity3d.com/cn/learn/tutorials/topics/best-practices/assetbundle-fundamentals?playlist=30089

本章主要讨论了与AssetBundles相关的知识, 它介绍了用于构建AssetBundles的基本系统,以及用于与AssetBundles进行交互的核心API。 具体来说,主要分析AssetBundles本身的加载和卸载以及AssetBundles中特定资产和对象的加载和卸载。

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

1.1 预览

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

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

1.2 AssetBundle结构

总的来说,一个AssetBundle由两部分组成:头部和数据段。

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

数据段包含通过序列化AssetBundle中的资产生成的原始数据。如果将LZMA指定为压缩方案,则将压缩所有序列化资产的完整字节数组。如果指定LZ4,每个资源的字节将被单独压缩。如果不使用压缩,数据段将保持为原始字节流。(解读一下,这里也就阐述了为什么使用LZ4压缩会节约内存的原理)

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

1.3 加载AssetBundle

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可以相互存在引用关系。

1.3.1 AssetBundle.LoadFromMemory(或异步)

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。它的功能没有改变。

1.3.2 AssetBundle.LoadFromFile(或异步)

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

在桌面平台(PC,Mac,Linux),控制台和移动平台上,API只会加载AssetBundle的头部,并将剩余的数据保留在磁盘上。如果有加载方法(例如AssetBundle.Load)被调用或它们的InstanceID被引用,AssetBundle的对象将按需加载。在这种情况下不会消耗过量的内存。在Unity编辑器中,API会将整个AssetBundle加载到内存中,就好像从磁盘读取字节并使用AssetBundle.LoadFromMemoryAsync一样。如果在Unity编辑器中对项目进行性能分析,此API可能导致在AssetBundle加载期间出现内存峰值。但不应该影响实际设备性能,并且在采取补救措施之前应该先在实际设备上对这些峰值进行重新测试。

注意:在Unity 5.3或更早版本的Android设备上,尝试从StreamingAssets路径加载AssetBundles时,此API将失败。 Unity 5.4中已解决该问题。有关更多详细信息,请参阅AssetBundle使用模式。

在Unity 5.3之前,这个API被称为AssetBundle.CreateFromFile,其功能尚未更改。

1.3.3 AssetBundleDownloadHandler

UnityWebRequest API允许开发人员准确指定Unity如何处理下载数据,并允许开发人员消除不必要的内存使用。使用UnityWebRequest下载AssetBundle的最简单方法是调用UnityWebRequest.GetAssetBundle。

就本文而言,感兴趣的类是DownloadHandlerAssetBundle。这个类使用工作线程,它会将下载的数据流式传输到固定大小的缓冲区,然后根据下载处理程序的配置方式将缓冲的数据缓冲到临时存储或AssetBundle缓存。所有这些操作都以native code形式进行,消除了扩展托管堆的风险。此外,此下载处理程序不会保留下载字节的native code副本,从而进一步减少下载AssetBundle的内存开销。

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

下载完成后,下载处理程序的assetBundle属性将提供对下载的AssetBundle的访问权限,就像已在下载的AssetBundle上调用了AssetBundle.LoadFromFile一样。

如果将缓存信息提供给UnityWebRequest对象(If caching information is provided to a UnityWebRequest object,这里不是很理解缓存信息指的是什么,handle对象?),并且所请求的AssetBundle已经存在于Unity的缓存中,则AssetBundle将立即变为可用,并且此API将以与AssetBundle.LoadFromFile相同的方式运行。

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

1.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对象将在native内存中保留AssetBundle字节的完整副本。 AssetBundle的额外副本保留为支持WWW.bytes属性。

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

与UnityWebRequest不同,每次调用此API都会产生一个新的工作线程。因此,在移动设备等内存有限的平台上,每次只能使用此API下载一个AssetBundle,以避免内存高峰。多次调用此API时,请小心创建过多的线程。如果需要下载超过5个AssetBundle,请使用脚本代码创建和管理下载队列,以确保同时运行的AssetBundle下载不要太多。

1.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兼容。有关更多详细信息,请参阅下一章的部分。

1.4 从AssetBundle中加载资产

181/5000
UnityEngine.Objects可以使用三个不同的API从AssetBundles加载,这些API都包含在AssetBundle对象中,它们同时具有同步和异步变体:

  • LoadAsset (LoadAssetAsync)
  • LoadAllAssets (LoadAllAssetsAsync)
  • LoadAssetWithSubAssets (LoadAssetWithSubAssetsAsync)

这些API的同步版本总是比异步版本快至少一帧。

异步加载将每帧加载多个对象,直到它们的时间片的限制。有关此行为的基本技术原因,请参阅下一节。

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

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

对于任何其他情况,请使用LoadAsset或LoadAssetAsync。

1.4.1 底层加载细节

UnityEngine.Object加载是在主线程之外执行的:从工作线程的存储中读取对象的数据。 任何不涉及
Unity系统的线程敏感部分(脚本,图形)的内容都将在工作线程上进行转换。 例如,VBOs将从网格创建,纹理将被解压缩等。

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

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

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

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

1.4.2 AssetBundle依赖

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

当一个或多个父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 1之前加载AssetBundle 2,或者必须从AssetBundle 2中显式加载Texture B。在从AssetBundle 1加载材料A之前加载AssetBundle 2就足够了。

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

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

1.4.3 AssetBundle manifests

当使用BuildPipeline.BuildAssetBundles API执行AssetBundle构建管道时,Unity会序列化一个包含每个AssetBundle的依赖性信息的对象。此数据存储在单独的AssetBundle中,其中包含一个单独的AssetBundleManifest对象。

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

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

AssetBundleManifest对象本身提供GetAllAssetBundles API来列出与清单并发构建的所有AssetBundles,以及两个方法来查询特定AssetBundle的依赖关系:

  • AssetBundleManifest.GetAllDependencies返回需要查询的AssetBundle的所有分层依赖项,其中包括AssetBundle的直接子项,子项的子项等的依赖项。

  • AssetBundleManifest.GetDirectDependencies只返回一个需要查询的AssetBundle的直接子项

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

1.4.4 建议

在大多情况下,在玩家进入应用程序的性能关键区域(如主场景或世界)之前,最好加载尽可能多的所需对象。 这在移动平台上尤其重要,因为访问本地存储的速度很慢,而且在播放时加载和卸载对象的内存流失可能会触发GC。

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

你可能感兴趣的:(最佳实践,Unity,Unity,最佳实践,AssetBundle)