赞
踩
本系列的前一章介绍了AssetBundles的基础知识,其中包括各种加载API的底层行为。 本章讨论在实践中使用AssetBundles的各个方面的问题和可能的解决方案。
在内存敏感的环境中仔细控制加载的对象的大小和数量非常重要。从当前激活的场景中移除时,Unity不会自动卸载对象。资产清理只在特定时间触发,当然也可以手动触发。
AssetBundles本身必须小心管理。由本地存储上的文件(在Unity缓存中或通过AssetBundle.LoadFromFile加载的文件)支持的AssetBundle具有最小的内存开销(这里应该是说,如果AB来自本地,加载AB只会加载头文件,相较于从网络上下载的AB),很少超过几万字节。但是,如果存在大量的AssetBundles,则此项开销仍是一个大问题。
由于大多数项目允许用户重新体验内容(例如重新调整关卡),因此知道何时加载或卸载AssetBundle非常重要。如果AssetBundle卸载不当,可能会导致内存中的对象重复。在某些情况下不当卸载AssetBundles也会导致不良行为,例如导致纹理丢失。要理解为什么会发生这种情况,请参阅资产,对象和序列化章节的“对象间引用”部分。
管理资产和AssetBundles时要了解的最重要的一点是,在为AssetBundle.Unload的unloadAllLoadedObjects参数设置true或false时的行为差异。
该API将卸载正在调用的AssetBundle的头文件。 unloadAllLoadedObjects参数确定是否也卸载从此AssetBundle实例化的所有对象。如果设置为true,那么从该AssetBundle实例化的所有对象也将立即被卸载 - 即使它们当前正在活动场景中使用。
例如,假设材质M是从AssetBundle AB加载的,并且假设M当前处于活动场景中。
如果调用了AssetBundle.Unload(true),那么M将从场景中移除,销毁并卸载。 但是,如果调用AssetBundle.Unload(false),则AB的头文件将被卸载,但M仍将保留在场景中并且仍然有效。 调用AssetBundle.Unload(false)会中断M和AB之间的链接。 如果稍后再次加载AB,则AB中包含的对象的新副本将被加载到内存中。
如果稍后再次加载AB,则将重新加载AssetBundle头文件的新副本。 但是,M并未从AB的这个新副本中加载。 Unity没有在AB和M的新副本之间建立任何关联。
如果调用AssetBundle.LoadAsset()来重新加载M,则Unity不会将M的旧副本解释为AB中数据的一个实例。 因此,Unity将加载M的新副本,并且在场景中将有两个相同的M副本。
对于大多数项目来说,这种行为是不可取的。大多数项目应该使用AssetBundle.Unload(true)并采用一种方法来确保对象不重复。两种常用方法是:
在应用程序的整个生命周期中特定的时间点(例如在关卡之间或在转场读条期间)卸载临时的AssetBundles。这是更简单和最常见的选择。
为每个实例维护一个引用计数,这样就可以仅在所有实例对象都不使用的时候,卸载AssetBundle。这允许应用程序卸载并重新加载单个对象而不重复消耗内存。
如果应用程序必须使用AssetBundle.Unload(false),那么单个对象只能通过两种方式卸载:
在场景和代码中消除对不需要的对象的所有引用。然后,调用Resources.UnloadUnusedAssets。
非叠加地加载场景。这将销毁当前场景中的所有对象并自动调用Resources.UnloadUnusedAssets。
如果一个项目有明确定义的卸载时间点,可以使用户等待对象加载和卸载(例如在游戏转场之间),那么应使用这些点来卸载尽可能多的对象并加载新的对象。
最简单的方法是将项目的离散块打包到场景中,然后将这些场景及其所有依赖项构建到AssetBundles中。然后,应用程序可以进入“加载”场景,完全卸载包含旧场景的AssetBundle,然后加载包含新场景的AssetBundle。
虽然这是最简单的流程,但有些项目需要更复杂的AssetBundle管理。由于每个项目都不同,因此没有通用的AssetBundle设计模式。
在决定如何将对象分组为AssetBundles时,如果必须同时加载或更新对象,通常最好先将对象捆绑到AssetBundles中。例如,考虑角色扮演游戏。个别地图和过场动画可按场景分组为AssetBundles,但在大多数场景中都需要一些对象。可以构建AssetBundles来提供肖像,游戏中的用户界面以及不同的角色模型和纹理。后面的对象和资产可以被分组到第二组资产包中,这些资产包在启动时加载并在应用的整个生命周期内保持加载状态。
如果Unity必须在AssetBundle卸载后从它的AssetBundle重新加载对象,则可能会出现另一个问题。在这种情况下,重新加载将失败,对象将作为(缺失)对象出现在Unity编辑器的层次结构中。
这主要发生在Unity丢失并恢复对其图形上下文的控制时,例如,当移动应用程序被暂停或用户锁定其PC时。在这种情况下,Unity必须将纹理和着色器重新上传到GPU。如果这些资产的源AssetBundle不可用,则应用程序将以洋红色呈现场景中的对象。
将项目的AssetBundles分发给客户端有两种基本方式:与项目同时安装或在安装后下载。
移动项目通常选择后一种。 主机和PC项目通常选择前一种。
正确的程序框架允许在安装后将新内容或修补的内容作为补丁更新,而不用关心初始的AssetBundles。 有关这方面的更多信息,请参阅Unity手册的“使用AssetBundles进行修补”部分。
将AssetBundles与项目一起发布是最简单的发布方式,因为它不需要额外的下载管理代码。 为什么一个项目需要在安装时包含AssetBundles有两个主要原因:
减少项目构建时间并允许更简单的迭代开发。 如果这些AssetBundles不需要与应用程序本身分开更新,那么AssetBundles可以通过将资产包存储在StreamingAsset中从而包含在应用程序中。 请参阅下面的StreamingAsset部分。
发布可更新内容的初始版本。 通常这样做是为了节省最终用户在初次安装后的时间,或者作为以后打补丁的基础。 SteamingAsset对于这种情况并不是最理想的方案。 但是,如果不愿意编写一个自定义下载和缓存的系统,那么可以使用这种方法,从StreamingAssets将可更新内容的初始修订加载到Unity缓存中。
想在安装时,让Unity应用程序包含任何类型的内容(包括AssetBundles)的最简单方法是在构建项目之前将内容构建到/Assets/StreamingAssets 文件夹中。构建时包含在StreamingAssets文件夹中的任何内容都将被复制到最终的应用程序中。
本地存储的StreamingAssets文件夹的完整路径可在运行时通过属性Application.streamingAssetsPath访问。然后可以在大多数平台上通过AssetBundle.LoadFromFile加载AssetBundles。
Android开发人员:在Android上,StreamingAssets文件夹中的资源会存储到APK中,并且可能需要更多时间才能加载(因为存储在APK中的文件可能使用不同的存储算法)。使用的算法可能会因Unity版本而异。您可以使用7-zip等解压工具打开APK以确定文件是否被压缩。如果被压缩,AssetBundle.LoadFromFile()执行得更慢。在这种情况下,您可以使用UnityWebRequest.GetAssetBundle作为一个优化方法来检索是否有已经缓存的版本。通过使用UnityWebRequest.AssetBundle将在第一次运行期间解压缩并缓存,从而使后续执行速度更快。请注意,这将需要更多的存储空间,因为AssetBundle将被复制到缓存中。或者,您可以导出您的Gradle项目,并在构建时向您的AssetBundles添加扩展。然后,您可以编辑build.gradle文件并将该扩展名添加到noCompress部分。完成后,您应该可以使用AssetBundle.LoadFromFile()而无需支付解压缩性能成本。
注意:StreamingAsset在某些平台上不是可写文件。如果安装后需要更新项目的AssetBundles,则可以使用WWW.LoadFromCacheOrDownload或编写自定义下载程序。
将AssetBundles部署到移动设备的最佳方法是在安装应用程序后下载它们。这也允许在安装后更新内容而不强制用户重新下载整个应用程序。在许多平台上,应用程序二进制文件必须经过昂贵且冗长的重新认证过程。因此,开发一个良好的安装后下载系统至关重要。
交付AssetBundles的最简单方法是将它们放置在Web服务器上并通过UnityWebRequest部署。 Unity会自动将下载的AssetBundles缓存在本地存储上。如果下载的AssetBundle是LZMA压缩的,则AssetBundle将以未压缩或重新压缩为LZ4(取决于Caching.compressionEnabled设置)的形式存储在缓存中,以便将来加载更快。如果下载的捆绑包压缩了LZ4,则AssetBundle将被压缩存储。如果缓存填满,Unity将从缓存中删除最近最少使用的AssetBundle。有关更多详细信息,请参阅下面的内置缓存部分。
通常建议尽可能使用UnityWebRequest,或者仅在使用Unity 5.2或更早版本时使用WWW.LoadFromCacheOrDownload。如果内置API的内存消耗,缓存行为或性能对于特定项目影响很大,或者项目必须运行特定于平台的代码以实现其要求,那么只能定制下载系统了。
使用UnityWebRequest或WWW.LoadFromCacheOrDownload可能不理想的情况示例:
当需要对AssetBundle缓存进行细粒度控制时
当项目需要实施自定义压缩策略时
当项目希望使用平台特定的API来满足某些要求时,例如需要在后台传输数据。
- 示例:使用iOS的后台任务API在后台下载数据。
如果AssetBundles必须通过SSL下载,但是Unity没有正确的SSL支持(如PC)。
Unity有一个内置的AssetBundle缓存系统,可用于缓存通过UnityWebRequest API下载的AssetBundles,该API包含一个接受AssetBundle版本号作为参数的重载。此版本号不存储在AssetBundle内部,并且不由AssetBundle系统生成。
缓存系统跟踪传递给UnityWebRequest的最新版本号。当调用此API时传入一个版本号,高速缓存系统通过比较版本号来检查是否存在缓存的AssetBundle。如果这些数字匹配,系统将加载缓存的AssetBundle。如果版本号不匹配,或没有缓存的AssetBundle,Unity将下载一个新副本。这个新副本将与新版本号相关联。
缓存系统中的AssetBundles仅由其文件名来标识,而不是由其下载的完整URL标识。这意味着具有相同文件名的AssetBundle可以存储在多个不同的位置,例如CDN(内容分发网络,居然还可以有这种思路)。只要文件名称相同,缓存系统就会将它们识别为相同的AssetBundle。
每个应用程序都要决定将版本号分配给AssetBundles的适当策略,并将这些版本号传递给UnityWebRequest。这些数字可能来自各种唯一标识符,例如CRC值。请注意,虽然AssetBundleManifest.GetAssetBundleHash()也可用于此目的,但我们不建议使用此功能进行版本控制,因为它仅提供估算值,而不是真正的Hash值计算)。
有关更多详细信息,请参阅Unity手册的“使用AssetBundles进行修补”部分。
在Unity 2017.1以后,缓存API已经扩展到提供更精细的控制,允许开发人员从多个缓存中选择一个活动缓存。以前的Unity版本只能修改Caching.expirationDelay和Caching.maximumAvailableDiskSpace来删除缓存的资源(Unity 2017.1中这些属性保留在Cache类中)。
expirationDelay是自动删除AssetBundle之前必须经过的最小秒数。如果在此期间没有访问AssetBundle,它将被自动删除。
maximumAvailableDiskSpace指定本地存储空间量(以字节为单位),这个量是指缓存在删除那些已经超过expirationDelay时间,没有使用的AssetBundle之前,可以使用的空间量。达到限制时,Unity将删除最近最少打开的缓存中的AssetBundle(或通过Caching.MarkAsUsed标记为已使用)。 Unity会删除缓存的AssetBundles,直到有足够的空间完成新的下载为止。
由于AssetBundles使用文件名作为标识,所以可以使用应用程序附带的AssetBundles“填充”缓存。 为此,请将每个AssetBundle的初始版本或基本版本存储在/Assets/StreamingAssets /中。 该过程与“跟随项目发布”部分中详细介绍的过程相同。
第一次运行应用程序时,可以通过从Application.streamingAssetsPath加载AssetBundles来填充缓存。 此后,应用程序可以正常调用UnityWebRequest(UnityWebRequest也可用于最初从StreamingAssets路径加载AssetBundles)。
编写自定义下载程序可以让应用程序完全控制AssetBundles的下载,解压缩和存储方式。 由于所涉及的工程工作不是必要的,所以我们只为大型团队推荐此方法。 编写自定义下载器时有四个主要考虑事项:
下载机制
存储位置
压缩类型
修补
有关修补AssetBundles的信息,请参阅使用AssetBundles修补部分。
对于大多数应用程序,HTTP是下载AssetBundles最简单的方法。 但是,实现基于HTTP的下载程序并不简单务。 自定义下载程序必须避免过多的内存分配,过多的线程使用和过多的线程唤醒。 Unity的WWW类不适合的原因在这里就不详细描述了。
在编写自定义下载器时,有三个选项:
C#的HttpWebRequest和WebClient类
自定义原生插件
Asset Store包
如果应用程序不需要使用HTTPS/SSL,那么C#的WebClient类提供了下载AssetBundles最简单的机制。 它能够将任何文件直接异步下载到本地存储,而无需过多管理内存分配。
要使用WebClient下载AssetBundle,请分配该类的一个实例,并将其传递给AssetBundle的URL以下载和目标路径。 如果需要对请求参数进行更多控制,可以使用C#的HttpWebRequest类编写下载程序:
从HttpWebResponse.GetResponseStream获取一个字节流。
在堆栈上分配一个固定大小的字节缓冲区。
从返回流(reponse)中读入缓冲区。
使用C#的File.IO API或任何其他流式IO系统将缓冲区写入磁盘。
很多插件包提供了本地代码的实现,以通过HTTP,HTTPS和其他协议下载文件。 在为Unity编写自定义本机代码插件之前,建议您先评估可用的Asset Store包。
编写自定义原生插件是在Unity中下载数据最耗时,但最灵活的方法。 由于编程时间花费高且技术风险高,只有在没有其他方法能够满足应用程序的要求时才推荐此方法。 例如,如果应用程序必须在Unity中没有C#SSL支持的平台上使用SSL通信,则可能需要定制本机插件。
自定义本机插件通常会包装目标平台的原生下载API。 示例包括iOS上的NSURLConnection和Android上的
java.net.HttpURLConnection。 请查阅每个平台的本地文档以获取有关使用这些API的更多详细信息。
在所有平台上,Application.persistentDataPath指向一个可写的位置,适合用于存储在应用程序运行时保持的数据(不要误解为不运行就会删除数据)。 在编写自定义下载器时,强烈建议使用Application.persistentDataPath的子目录来存储下载的数据。
Application.streamingAssetPath不可写,对于AssetBundle缓存来说是一个糟糕的选择。 streamingAssetsPath的示例位置包括:
OSX:在.app包内; 不可写。
Windows:在安装目录中(例如Program Files); 通常不可写
iOS:在.ipa包内; 不可写
Android:在.apk文件中; 不可写
如何将项目资产划分为AssetBundles并不简单。 经常采用简单的策略,比如将所有对象都单独生成一个AssetBundle或仅使用一个AssetBundle,但这些解决方案具有明显的缺点:
拥有太少的AssetBundles …
增加运行时内存使用量
增加加载时间
需要更大的下载量
拥有太多的AssetBundles …
增加构建时间
可能会使开发复杂化
增加总下载时间
关键的决定是如何将对象分组为AssetBundles。 主要战略是:
逻辑实体
对象类型
合并内容
有关这些分组策略的更多信息可以在手册中找到。
本节介绍使用AssetBundles项目中常见的几个问题。
Unity 5的AssetBundle系统将在对象构建到AssetBundle中时查找对象的所有依赖关系。此依赖关系信息用于确定将包含在AssetBundle中的一组对象。
明确分配给AssetBundle的对象只会构建到该AssetBundle中(就是创建了一个AssetBundleBuild)。当Object的AssetImporter的assetBundleName属性设置为非空字符串时,对象将被“显式分配”。这可以在Unity Editor中通过在对象的检查器中选择一个AssetBundle或从编辑器脚本中完成。
也可以将对象分配给AssetBundle,方法是将它们定义为AssetBundle building map的一部分,该map要与重载的BuildPipeline.BuildAssetBundles()函数一起使用,该函数接受一组AssetBundleBuild。
在AssetBundle中未明确分配的对象都将包含在所有包含对其引用的1个或多个对象的AssetBundle中(就是没有在AssetBundleBuild数组中的,都将作为冗余打包,包含在数组中的作为依赖打包,不会重复)。
例如,如果将两个不同的对象分配给两个不同的AssetBundles,但都具有对公共依赖项Object的引用,则该依赖Object将被复制到两个AssetBundles中。重复的依赖关系也将被实例化,这意味着依赖关系对象的两个副本将被视为具有不同标识符的不同对象。这将增加应用程序AssetBundles的总大小。如果应用程序加载它的父项,这也会导致Object的两个不同副本被加载到内存中。
有几种方法可以解决这个问题:
确保构建到不同AssetBundles中的对象不共享依赖关系。任何共享依赖关系的对象都可以放置在同一个AssetBundle中,而不需要重复依赖关系。
对于具有许多共享依赖的项目,此方法通常不可行。它导致很多的AssetBundles为了方便和高效的使用,必须经常重建和重新下载。
分段AssetBundles,要确保不会同时加载由相同依赖的两个AssetBundles。
此方法可能适用于某些类型的项目,例如基于关卡的游戏。但是,它仍然会不必要地增加项目的AssetBundles的大小,并增加构建时间和加载时间。
确保所有依赖项资产都生成一个单独的AssetBundles中。这完全消除了重复资产的风险,但也带来了复杂性。应用程序必须跟踪AssetBundles之间的依赖关系,并确保在调用任何AssetBundle.LoadAsset API之前加载了正确的AssetBundles。
通过位于UnityEditor名称空间中的AssetDatabase API跟踪对象依赖关系。正如命名空间所暗示的,这个API仅在Unity编辑器中可用,而不是在运行时。 AssetDatabase.GetDependencies可用于查找特定对象或资产的所有直接依赖关系。请注意,这些依赖关系可能有其自己的依赖关系。此外,AssetImporter API可用于查询分配有任何特定对象的AssetBundle。
通过组合AssetDatabase和AssetImporter API,可以编写一个编辑器脚本,以确保将所有AssetBundle的直接或间接依赖关系都生成了AssetBundle,或者共享同一个依赖的两个资产都生成了AssetBundle。由于冗余资产会产生的内存成本,建议所有项目都有这样的脚本。
任何自动生成的图集将被分配给包含从其生成图集的Sprite对象的AssetBundle。如果Sprite对象被分配给多个AssetBundles,那么图集将不会只分配给一个AssetBundle,而是多个。如果Sprite对象未分配给AssetBundle,则图集也不会被分配给AssetBundle。
为了确保图集没有重复,请检查标记在同一图集中的所有Sprite分配到同一个AssetBundle。
请注意,在Unity 5.2.2p3及更早版本中,自动生成的图集将永远不会分配给AssetBundle。因此,它们将被包含在使用了该Sprite的任何AssetBundles中,以及任何引用其组成Sprite的AssetBundles中。由于这个问题,强烈建议所有使用Unity的sprite打包程序的Unity 5项目升级到Unity 5.2.2p4,5.3或任何更新版本的Unity。
由于Android的碎片化严重,通常需要将纹理压缩为多种不同的格式。虽然所有Android设备都支持ETC1,但ETC1不支持带alpha通道的纹理。如果应用程序不需要OpenGL ES 2支持,则解决该问题的最简单方法是使用所有Android OpenGL ES 3设备支持的ETC2。
大多数应用程序需要在不支持ETC2的旧设备上发布。解决此问题的一种方法是使用Unity 5的AssetBundle变体(有关其他选项的详细信息,请参阅Unity的Android优化指南)。
要使用AssetBundle变体,所有无法使用ETC1进行压缩的纹理必须分离为仅包含纹理的AssetBundles。接下来,使用特定于供应商的纹理压缩格式(如DXT5,PVRTC和ATITC)创建相应的这些AssetBundles变体以支持Android中不支持非ETC2的设备。对于每个AssetBundle变体,将包含的纹理的TextureImporter设置更改为适合Variant的压缩格式。
在运行时,可以使用SystemInfo.SupportsTextureFormat API检测对不同纹理压缩格式的支持。并使用此信息来选择和加载包含以受支持格式压缩的纹理的AssetBundle变体。
有关Android纹理压缩格式的更多信息可以在这里找到。
Unity的当前版本不受此问题影响。
在Unity 5.3.2p2之前的版本中,Unity会在AssetBundle加载的整个过程中持有一个打开的文件句柄。 这在大多数平台上都不是问题。 但是,iOS将进程可以同时打开的文件句柄数限制为255.如果加载AssetBundle会导致超出限制,则加载调用将失败,并显示“Too Many Open File Handles”错误。
对于尝试将内容分成数百或数千个AssetBundles的项目,这是一个常见问题。
对于无法升级到补丁版本的Unity的项目,临时解决方案是:
通过合并相关的AssetBundles来减少使用的AssetBundles的数量
使用AssetBundle.Unload(false)关闭AssetBundle的文件句柄,并手动管理加载的对象的生命周期
AssetBundle系统的一个关键特性是引入了AssetBundle变体。变体的目的是允许应用程序调整其内容以更好地适应其运行时环境。当加载对象和解析实例ID引用时,变体允许不同的AssetBundle文件中的不同UnityEngine.Objects显示为“相同”对象。从概念上讲,它允许两个UnityEngine.Objects显示为共享相同的文件GUID和本地ID,并标识实际的UnityEngine.Object以字符串变体ID加载。
这个系统有两个主要用例:
变体允许应用程序在同一平台上加载不同的内容,但使用不同的硬件。
这是支持各种移动设备的关键。在现实的应用程序中,iPhone 4都不能像最新的iPhone一样显示相同的清晰度。
在Android上,AssetBundle 变体可用于解决设备间屏幕纵横比和DPI之间巨大的分割问题。
AssetBundle 变体系统的一个关键限制是它需要使用不同的资产来构建变体。即使这些资产之间的唯一差异是其导入设置,也适用此限制。如果内置到变体A和变体B中的纹理之间的唯一区别是在Unity纹理导入器中选择的特定纹理压缩算法,但是变体A和变体B依然是完全不同的资产。这意味着变体A和变体B是磁盘上的两个文件。
这种限制使大型项目的管理复杂化,因为特定资产的多个副本必须保存在源代码管理中。当开发人员希望更改资产的内容时,必须更新资产的所有变体。这个问题没有内置的解决方法。
大多数团队都有他们自己的AssetBundle变体形式。这是通过建立AssetBundles来完成的,通过在文件名后面添加了特定的后缀,以便识别给定AssetBundle所代表的特定变体。在构建这些AssetBundles时,通过代码更改包含的资产的导入器设置。一些开发者已经扩展了他们的定制系统,以便能够改变附属于预制件的组件上的参数。
是否压缩AssetBundles需要一些重要的考虑因素,其中包括:
- 加载时间:从本地存储或本地缓存加载时,未压缩的AssetBundles比加载压缩的AssetBundles要快得多。
构建时间:在压缩文件时,LZMA和LZ4非常缓慢,Unity Editor按顺序处理AssetBundles。具有大量AssetBundles的项目将花费大量的时间压缩它们。
应用程序大小:如果AssetBundles包含在应用程序中发布,则压缩它们将减少应用程序的总大小。或者,可以在安装后下载AssetBundles。
内存使用情况:在Unity 5.3之前,所有Unity的解压缩机制都要求在解压缩之前将整个压缩的AssetBundle加载到内存中。如果内存使用率很重要,请使用未压缩或LZ4压缩的AssetBundles。
下载时间:如果AssetBundles很大,或者用户处于带宽受限的环境中,例如在低速或数据连接上下载,那么压缩可能需要。如果只有几兆字节的数据通过高速连接传送到PC,则可能会忽略压缩。
主要由使用Crunch压缩算法的DXT压缩纹理组成的Bundles应该被构建为未压缩的。
Unity强烈建议开发人员不要在WebGL项目上使用压缩的AssetBundles。
WebGL项目中的所有AssetBundle解压缩和加载必须在主线程上进行。这是因为Unity的WebGL导出选项当前不支持工作线程。 AssetBundles的下载使用XMLHttpRequest委托给浏览器,XMLHttpRequest在Unity的主线程上执行。这意味着压缩的AssetBundles在WebGL上加载非常昂贵。
如果您使用的是Unity 5.5或更高版本,请考虑避免使用LZMA作为您的AssetBundles,并使用LZ4进行压缩,而不是按需解压缩。如果您需要较小的压缩大小,那么LZ4可以提供,您可以配置您的Web服务器以在HTTP协议层面(在LZ4压缩之上)对文件进行gzip压缩。 Unity 5.6删除了LZMA作为WebGL平台的压缩选项。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。