这一章来说说AssetBundles,介绍下它的基础系统,还有一些和AssetBundles进行交互的核心API。尤其是AssetBundles的加载、卸载,以及从AssetBundles中加载、卸载指定的Asset和Objects。
3.1. 概述
AssetBundles系统提供了一种Unity可以索引的打包格式。这是为了提供了一个可以和Unity序列化系统兼容的数据传递(交付)方法。AssetBundles是Unity用来在安装后交付和更新非代码内容的主要工具。它使得开发者可以减少安装包中的资源大小、减少运行时的内存压力,还可以为终端设备做优化:选择性的加载内容。
了解AssetBundles的工作方式是构建成功Unity项目(移动设备上)的必要因素。
3.2. AssetBundle中都有些什么?
一个AssetBundle中包含两个部分:文件头和数据段。
文件头是在构建AssetBundle时由Unity生成的。它包含了AssetBundle的信息,比如:AssetBundle的标识,AssetBundle是否被压缩,还有一个清单。
这个清单是一个查询表,以对象的名字为键。每一项提供一个字节的索引来标识在AssetBundle的数据段中给定对象的位置。在大多数平台,这个查询表使用STL的std::multimap实现的。虽然具体算法依赖不同平台STL实现方式,但大多数都是平衡树的一种。Windows和OSX衍生系统(包括IOS)使用的是红黑树。所以,构建清单的时间和AssetBundles中资源数量,不是线性关系那么乐观。
数据段包含本AssetBundle中序列化的Assets原始数据。如果数据段被压缩,那么LZMA算法将会被应用到所有序列化的字节,也就是先序列化所有的assets,然后对整个字节数组进行压缩。
Unity5.3之前,AssetBundle中的Objects不会被单独压缩。因此,5.3之前的Unity若是想从压缩的AssetBundle中读取一个或多个Objects就需要将整个AssetBundle解压缩。一般来说,为了提高后续在同一个AssetBundle加载的性能,Unity会缓存一个AssetBundle解压后的副本。
Unity5.3添加了LZ4压缩选项。由LZ4压缩选项构建的AssetBundles将会单独压缩其中的Objects,来让Unity在硬盘上存储压缩的AssetBundles。这也允许Unity单独解压Objects,而无须解压整个AssetBundle。
3.3. AssetBundle管理器
Unity在Bitbucket上开发并维护了一个AssetBundle管理器的实现方式作为参考。这个管理器采用了许多这个章节中描述的概念和API。为那些必须将AssetBundles整合到资源管理工作流程中的项目,提供了一个非常有用的起点。
“模拟模式”就是其中一个引人注意的特性。当在Unity编辑器中激活,这个模式将AssetBundle中Assets的加载请求重定位至工程文件夹/Assets/下,当然这一切对开发者来说都是透明的。这样就不需要开发者在开发阶段不停地重新构建AssetBundles了。
AssetBundle管理器是个开源项目,你可以在这找到。
3.4.加载AssetBundles
在Unity5中,AssetBundles可以通过四个不同的API加载。这四个API根据下面两个条件情况将有不同的行为:
- AssetBundle是否被压缩,是使用LZMA压缩还是LZ4压缩
- AssetBundle在什么平台上加载
这四个API是:
- AssetBundle.LoadFromMemoryAsync
- AssetBundle.LoadFromFile
- WWW.LoadFromCacheOrDownload
- UnityWebRequest的DownloadHandlerAssetBundle(至少是Unity5.3)
3.4.1. AssetBundle.LoadFromMemoryAsync
Unity建议不要使用这个函数
Unity3.3.3更新:这个API在Unity5.3.3.中重命名了。在Unity5.3.2(及之前版本),这个API是AssetBundle.CreateFromMemory。功能没有发生变化。
AssetBundle.LoadFromMemoryAsync从托管代码的字节数组中加载AssetBundle(C#中的byte[]).它总是将源数据从托管代码的字节数组中拷贝到一个新分配连续的本地内存块中.如果AssetBundle是LZMA压缩格式,它将在拷贝时将AssetBundle解压。未压缩和LZ4压缩的AssetBundles将逐字拷贝。
这个API所消耗的内存峰值至少是AssetBundle大小的两倍:一份是API所创建的本地内存中的副本,一份是传递给API的托管字节数组。通过这个API从AssetBundle中加载的Assets,因此会在内存中出现3次:一次是从托管代码的字节数组,一次是AssetBundle的本地内存副本,还有一次是GPU或系统内存中。
3.4.2. AssetBundle.LoadFromFile
Unity5.3更新:这个API在Unity5.3中重命名了。在Unity5.2及之前版本,这个API是AssetBundle.CreateFromFile。它的功能未变。
AssetBundle.LoadFromFile是一个用来从本地(一块硬盘或一个SD卡)加载未压缩AssetBundle的高效API。如果AssetBundles是未压缩的或LZ4压缩,这个API的行为如下:
移动设备:API只会加载AssetBundle的文件头,不会从硬盘中加载剩余的数据。AssetBundle的Objects只有被加载方法调用(比如AssetBundle.Load),或实例ID被间接引用时,才会被加载。在这个情境中没有额外的内存被消耗。
Unity编辑器:这个API将会把整个AssetBundle加载到内存中,就如AssetBundle.LoadFromMemoryAsync被调用一样,将所有字节从硬盘中读出。如果项目在Unity编辑器中进行评估,这个API在AssetBundle加载时,将会引起内存峰值。不过这不会影响实机上的表现,在实机上需要重新测试下,确定会遇到峰值问题再进行补救行为。
备注:对LZMA压缩的AssetBundles调用AssetBundle.LoadFromFile将会失败。
3.4.3. WWW.LoadFromCacheOrDownload
WWW.LoadFromCacheOrDownload是一个从远端服务器或本地存储加载Objects的API。本地文件可以通过添加file://URL进行加载。如果AssetBundle已经在Unity缓存中了,这个API的处理方式和AssetBundle.LoadFromFile一模一样。
如果AssetBundle还未被缓存,WWW.LoadFromCacheOrDownload将会从他的源地址读取AssetBundle。如果AssetBundle被压缩了,将会在工作线程中解压并写入缓存。否则,他将通过工作线程被直接写入缓存。
一旦AssetBundle被缓存,WWW.LoadFromCacheOrDownload将从缓存中加载头信息,解压AssetBundle。接着API的表现与通过AssetBundle.LoadFromFile加载AssetBundle一致。
备注:当数据被解压并通过一个固定大小的缓冲区写入缓存,WWW对象将在本地内存中保持一份完整的AssetBundle字节副本。保留这个额外的AssetBundle副本是为了支持WWW.bytes这个属性。
由于在WWW对象中缓存一份AssetBundle内容字节的内存开销,建议所有使用WWW.LoadFromCacheOrDownload的开发者确保他们的AssetBundles小一些:最多几兆。接下来还有个建议,对那些对内存有限制的平台(比如移动设备)的开发者:为了防止内存峰值,确保同一时间代码中只下载一个AssetBundle。对于AssetBundle大小的更多讨论,见[AssetBundle使用模式](AssetBundle usage patterns)章节中的Asset分配策略部分。
备注:每调用一次这个API将会创建一个新的工作线程。当多次调用这个API时,注意此时创建了过多的线程。如果超过5-10个AssetBundles需要下载,建议你的代码写成可以确保只有少量的AssetBundle在同时下载。
3.4.4. AssetBundleDownloadHandler
UnityWebRequest在Unity5.3中引入到移动平台上,相比于WWW 它提供了另一种更灵活的选择。UnityWebRequest允许开发者指定如何处理下载的数据,并且让开发者避免不必要的内存使用。通过UnityWebRequest下载一个AssetBundle最简单的方法就是调用UnityWebRequest.GetAssetBundle。
根据这篇文章的主题,我们最关心的类就是DownloadHandlerAssetBundle。使用时,这个DownloadHandler表现的和WWW.LoadFromCacheOrDownload一样。使用一个工作线程,他将下载的数据放入一个固定大小的缓冲,然后根据DownloadHandler的配置,将缓冲数据移至一个临时存储或AssetBundle缓存。LZMA压缩的AssetBundles在下载时将会被解压,然后缓存起来。
所有的这些操作都发生在本地代码,避免了扩大托管堆的风险。而且,这个DownloadHandler没有在本地代码中保存下载字节的副本,这样减少了下载AssetBundle带来的内存占用。
当下载结束了,可以通过DownloadHandler的assetBundle属性,访问下载好的AssetBundle,就像对下载好的AssetBundle调用AssetBundle.LoadFromFile一样。
UnityWebRequest也支持像WWW.LoadFromCacheOrDownload一样的缓存方式。如果提供给UnityWebRequest对象的缓存信息中,正在请求的AssetBundle已经在Unity缓存中了,那么AssetBundle将会立即可用,并且这个API将会进行和AssetBundle.LoadFromFile一样的操作。
备注:Unity的AssetBundle缓存在WWW.LoadFromCacheOrDownload和UnityWebRequest之间是共享的。在一个API中下载的任何AssetBundle在另一个API中也是有效的。
备注:不像WWW,UnityWebRequest系统它内部维护一个工作线程池,还有一个内部任务系统来确保开发者不会同时启动过量的下载。线程池的大小目前无法配置。
3.4.5. 建议
一般来说,应该尽可能的采用AssetBundle.LoadFromFile。这个API在速度,硬盘和运行内存的使用上是效率最高的。
对于那些必须下载和对AssetBundles进行打补丁的项目来说,强烈建议:在Unity5.3以及更新的版本上使用UnityWebRequest,而那些使用Unity5.2或更老的项目则使用WWW.LoadFromCacheOrDownload。在下一章的发布这节中我们将详细描述,如何将AssetBundle打包在在项目的安装包中。
当使用WWW.LoadFromCacheOrDownload时,强烈建议你确保项目的AssetBundles小于项目最大内存预算的2-3%,来防止应用由于内存使用峰值被强行终止。对于大部分项目来说,AssetBundles的文件大小应该不超过5MB,并且同时下载的AssetBundles不超过1-2个。
不论是用WWW.LoadFromCacheOrDownload还是UnityWebRequest,确保下载部分的代码在加载完AssetBundle后适时调用Dispose。还有一种选择:C#的using声明是最方便的方法来确保一个www或UnityWebRequest被安全Dispose掉。
对于那种大型团队和特殊缓存及下载需求的项目来说,定制一个Downloader是有必要的。自己写个Downloader是一项大工程,并且自己写的这些downloader应该和AssetBundle.LoadFromFile兼容。详情请见下篇的发布部分
3.5. 从AssetBundles中加载Assets
UnityEngine.Objects可以通过三个不同的API从AssetBundles中加载,这三个方法分别是AssetBundle对象的:LoadAsset,LoadAllAssets和LoadAssetWithSubAssets。这些API也有异步形式,以-Async结尾:LoadAssetAsync, LoadAllAssetsAsync和LoadAssetWithSubAssetsAsync。
在Unity5.1和更老的版本中,同步接口总会比异步接口快,最少快一帧。因为在Unity5.2之前,所有异步接口每帧最多加载一个对象。这意味着LoadAllAssetsAsync和LoadAssetWithSubAssetAsync都会明显的慢于相应的同步接口。在Unity5.2中这种行为被修正了。异步加载每帧将会加载多个Object,直到时间片的限制。查看底层加载细节这部分,来了解造成这种行为的底层技术原因,和关于时间片的更多细节。
当加载多个独立的UnityEngine.Object时,可以使用LoadAllAssets。这个接口应该只在同一个AssetBundle中多数或全部Objects需要被加载时再去调用。相比于其他两个接口,LoadAllAssets比多次单独调用LoadAssets要快一点点。因此,如果需要同时加载大量assets,但是这些assets又不超过AssetBundle内容的三分之二,可以考虑将AssetBundle细分为多个小点的包,然后使用LoadAllAssets。
当去加载一个复杂的Asset(包含许多内嵌的Object,比如一个FBX模型内嵌动画,或一个图集,内嵌多个精灵)时,可以使用LoadAssetWithSubAssets。如果这些需要加载的Objects都来自同一个Asset,同时这个AssetBundle中还有很多其他不相关的Objects,那就用这个接口。
对于其他情况,就用LoadAsset或LoadAssetAsync吧。
3.5.1. 底层加载细节
UnityEngine.Object的加载不是在主线程进行的:Object的数据是在工作线程从存储中读取出来的。Unity系统中任何线程不敏感的部分都会放在工作线程中。比如,从mesh中创建VBO,纹理解压,等。
在Unity5.3之前的版本,Object是一个个加载的,并且Object的某些部分只能在主线程中加载。这称为“整合”。当工作线程加载完Object数据,它将暂停,去把新加载好的Object整合到主线程中,在此期间工作线程一直保持着暂停状态,不会去加载下一个Object,直到主线程整合结束。
从Unity5.3起,Object的加载是并行的。多个Objects可以在工作线程中反序列化,处理和整合。当一个Object完成加载,它的Awake函数将被调用,Object在下一帧对于Unity系统来说就变成了有效对象。
同步方法AssetBundle.Load将暂停主线程,直到Object加载结束。在5.3版本之前。异步函数AssetBundle.LoadAsync在将Objects整合到主线程时,才会暂停主线程。加载对象会有一个时间片,来确保Object整合不会占用每帧帧太多的时间(毫秒级别)。具体这个时间通过Application.backgroundLoadingPriority属性去设置。单位是毫秒。
- ThreadPriority.Hight: 最多50毫秒每帧
- ThreadPriority.Normal: 最多10毫秒每帧
- ThreadPriority.BelowNormal: 最多4毫秒每帧
- ThreadPriority.Low: 最多2毫秒每帧
在Unity5.1及更老的版本中,异步函数每帧只会整合一个Object。这被认为是一个bug,并且在Unity5.2的时候已经修复了。从Unity5.2起,多个Object可以被加载,除非对象加载时到了帧率的限制。AssetBundle.LoadAsync完成的总会比相应的同步API慢(假设其他因素一致),因为在LoadAsync调用后和object对引擎生效,之间至少需要一帧。
实际测试下来看看其中的差距。在5.2之前,在某个低端设备上加载大量纹理:同步API需要7毫秒,异步API需要70毫秒。5.2之后,观察到的差距近乎为零。
3.5.2. AssetBundle依赖
在Unity5的AssetBundle系统中,AssetBundles间的依赖,根据不同的运行环境,可以通过两个不同的API来跟踪。在Unity编辑器中,AssetBundle依赖关系可以通过AssetDatabaseAPI去查询。AssetBundle的分配和依赖可以通过AssetImporterAPI访问和改变。运行时,Unity提供一个 ScriptableObject-based AssetBundleManifest 去加载构建AssetBundle时生成的依赖信息。
当AssetBundle的父AssetBundle(直接引用)中Object有引用其他的AssetBundle中的Object,那么就存在了间接引用关系(这个关系并没有记录在清单中)[1]。关于对象间引用的更多信息,请查看Assets,Objects and Serialization一文中的对象间的引用章节。
如Assets, Objects and Serialization一文里序列化和实例段落中描述的一样,Object数据被糅合在AssetBundle中,以FileGUID和LocalID作为唯一标识。
Object在他的实例ID被第一次引用时被加载;Object在其AssetBundle被加载时,被赋予一个有效的实例ID。由此看来,加载一个Object之前应该先把其依赖项所在的AssetBundles全都加载了。而并不是说拥有依赖关系的AssetBundle之间必须按顺序加载。(译者注:如果这里有迷惑,可以先向下看,例子中有再次提到这句话,结合例子就容易理解一些)Unity不会在一个父AssetBundle加载时,帮你把其子AssetBundles都自动加载的。
示例:
假设材质A引用纹理B。材质A被打包至AssetBundle1,而纹理B被打包到AssetBundle2。
在此用例中,从AssetBundle1中加载材质A之前,必须要先加载AssetBundle2。
这并不意味着AssetBundle2必须要先于AssetBundle1加载,或者纹理B必须显式的从AssetBundle2中加载。只有从AssetBundle1中加载材质A时,AssetBundle2才需要先加载。
在AssetBundle1加载时,Unity不会自动加载AssetBundle2。这必须通过脚本手动完成。上面提到的AssetBundle的API都可以任意用于加载AssetBundles1和AssetBundles2上,随意组合。通过WWW.LoadFromCacheOrDownload加载AssetBundles,可以任意和通过AssetBundle.LoadFromFile或AssetBundle.LoadFromMemoryAsync加载AssetBundles组合在一起。
3.5.3. AssetBundle清单(manifests)
当通过接口BuildPipeline.BuildAssetBundles执行AssetBundle的构建流程时,Unity会序列化一个对象,它包含每个AssetBundle的依赖信息。这个数据存储在一个单独的AssetBundle中,而这个AssetBundle只包含一个AssetBundleManifest类型的对象。
这个Asset所在的AssetBundle和构建的AssetBundles在同一个目录中,其(这个Asset所在的AssetBundle)名称就是这个目录名。如果项目将AssetBundles构建至(工程目录)/build/Client,那么包含清单的AssetBundle将会保存为(工程目录)/build/Client/Client.manifest。(译者注:应该没有manifest的后缀名吧?)
包含清单的这个AssetBundle可以被加载,缓存,卸载。就像其他的AssetBundle一样。
AssetBundleManifest对象本身提供GetAllAssetBundles接口,用来罗列所有和清单一起构建的AssetBundle。还有两个方法,用来查询指定AssetBundle的依赖:
AssetBundleManifest.GetAllDependencies返回AssetBundle所有的依赖,包括迭代依赖。
翻译:莫铭
原文地址:AssetBundle fundamentals
AssetBundleManifest.GetDirectDependencies只返回AssetBundle直接依赖。
注意这些API都会分配字符串数组。不要多用,尤其是在性能紧张的时候。
3.5.4. 建议
在用户进入应用的性能严峻区(比如游戏主场景或世界)之前,尽可能多的加载所需的对象,这被认为是最佳实践。这在移动平台上尤为重要,因为访问本地存储很慢,并且在play-time时加载和卸载对象引起的内存变更,会引起垃圾回收机制。
对于那些必须在应用交互时加载和卸载对象的项目,可以看看AssetBundle使用模式一文中的管理已加载的assets段落,来获取更多关于卸载Objects和AssetBundles的信息。
-
这段看不懂,于是按照个人理解写下的,建议对照原文自行理解。 ↩