1. 概述
AssetBundle系统可以支持一个或多个文件存储到Unity,支持索引和序列化的存档格式的方法。AssetBundle是Unity安装后交付和更新非代码内容的主要工具。这允许开发人员提交较小的应用包,最大限度地减少运行时的内存压力,并选择性地加载针对最终用户设备优化的内容。
2. 存储内容
AssetBundle里面存储两种不同但相关的东西:
- 首先是磁盘的实际文件。对于这种情况,我们称之为AssetBundle存档,存档可以被视为一个容器,就像文件夹一样,可以在其中包含其他文件夹。这些附加文件包含两种类型:序列化文件和资源文件。序列化文件包含分解为各个对象并写入此单个文件的资源。资源文件只是为某些资源单独存储的二进制数据块,允许我们有效地在另一个线程上从磁盘加载他们
- 其次是通过代码进行交互以便从特定存档加载资源的实际AssetBundle对象。此对象包含一个映射,即从已添加到次存档的资源的所有文件路径到按需加载的资源所包含的对象之间的映射。Manifest文件是由对象名称为key的查找表,每个条目都提供了一个字节的索引来标识对象在Assetbundle数据段的位置,虽然这个算法在各个平台实现有差异,但是大部分都是平衡搜索树的变种,随着AssetBundle里对象数量的增长,构建清单的时间将大于线性增加(O(NLogN))
3. 加载AssetBundle
在Unity5中,可以通过4个不同的API来加载AssetBundle:
- AssetBundle.LoadFromMemoryAsync
- AssetBundle.LoadFromFile
- WWW.LoadFromCacheOrDownload
- UnityWebRequest 的
- DownloadHandlerAssetBundle(Unity5.3之后版本)
4个API可以用两个条件来区分:
- 压缩方式:LZMA压缩和LZ4压缩
- 加载的平台
3.1 AssetBundle.LoadFromMemoryAsync
此接口从托管代码的字节数组(c#的byte[])中加载AssetBundle,它总是会从本地内存中开辟一段连续内存,然后从托管代码的字节数组中拷贝源数据到新分配的内存中。如果AssetBundle是LZMA压缩格式的,拷贝过程中AssetBundle会被压缩,而LZ4雅安所格式或者不压缩的AssetBundle会直接拷贝过去。
这个API内存消耗的峰值最少是AssetBundle大小的两倍:一个是API创建本地内存,一个是源数据所占用的内存。用这个API加载资源之后,这个加载资源将会在内存中出现3份拷贝:1.托管代码字节数组 2.AssetBundle本地内存 3.在GPU或者系统内存中缓存的资源
3.2 AssetBundle.LoadFromFile
此API是一个被设计用来从本地存储中加载未压缩的AssetBundle、高效的API。如果AssetBundle未压缩或者使用LZ4雅座,这个API表现如下:
- 移动设备:API会加载AssetBundle的头部数据,其他的数据保留在磁盘中。当调用加载的方法(AssetBundle.Load)或者他们的Instance ID被引用时才会被按需加载,在这种情况下时没有额外的内存开销的
- 编辑器:这个API会将整个AssetBundle加载进内存,就像使用AssetBundle.LoadFromMemoryAsync,而不是按需从磁盘上读取资源。当AssetBundle被加载时,在编辑器的Profiler面板会出现一个峰值,但是在实际设备上面并不会出现。
3.3 WWW.LoadFromCacheOrDownload
此API对于从远端服务器和本地存储中加载对象来说很有用,可以使用file://连接地址从本地加载文件。如果AssetBundle已经在Unity的缓存中存在,则他会表现得根AssetBundle.LoadFromFile一样
如果没有被缓存,API会从AssetBundle的源地址读取它。如果AssetBundle是压缩格式,他会使用一个worker线程来解压AssetBundle并且写入到缓存当中,如果没有被压缩,worker线程会直接将它写入缓存中。一旦AssetBundle被缓存了,API会从缓存中加载Header信息和未压缩的AssetBundle,之后这个API表现就跟AssetBundle.LoadFromFile一样
每调用一次这个API都会产生一个新的worker线程,要当心多次调用这个API的时候产生多个线程的问题。如果有5到10个AssetBundle需要下载,建议代码只让少数几个AssetBundle同时下载
当数据解压并写道缓存的同时,WWW对象会在本地内存中保留一份AssetBundle字节的完整拷贝,这个额外的拷贝是用来支持WWW.bytes属性
由于WWW对象缓存AssetBundle字节数组的开销,这里推荐开发者使用WWW.LoadFromCacheOrDownload的AssetBundle尽可能小(最多几M)。也推荐开发者在内存有限的平台上,如移动设备,确保代码在同时只有一个AssetBundle在下载,避免内存峰值
3.4 AssetBundleDownloadHandler
在Unity5.3的移动平台上,Unity引入了UnityWebRequest,它比WWW更加灵活。UnityWebRequest可以让开发者指定Unity怎样处理数据和避免不必要的开销。使用UnityWebRequest去下载一个AssetBundle最简单的方式就是调用UnityWebRequest.GetAssetBundle
AssetBundleDownloadHandler使用时,它的行为跟WWW.LoadFromCacheOrDownload类似,它使用worker线程去下载数到固定大小Buffer中,然后根据DownLoad Handler设置,把Buffer中的数据写入到临时存储或者AssetBundler缓存中,LZMA格式压缩的AssetBundle会在下载和缓存过程中被解压
这些操作都是发生在Unity底层代码里,就避免浪费C#托管堆,另外,DownloadHandler在Untiy底层也不保留所有下载字节数组的拷贝,更加减少了下载AssetBundle的内存开销。当下载完成之后,DownloadHander的assetBundle属性用来访问已下载的AssetBundle,就像对下载后的AssetBundle执行了AssetBundle.LoadFromFile一样
UnityWebRequest也支持像WWW.LoadFromCacheOrDownload一样缓存机制,如果给UnityWebRequest对象提供了缓存信息,并且请求的AssetBundle已经在Unity的缓存中,AssetBundle会马上生效并且回想AssetBundle.LoadFromFile一样操作它
注意事项:
- AssetBundle缓存机制在 WWW.LoadFromCacheOrDownload 和UnityWebReuqest之间是共享的,一个API下载过AssetBundle就其它的API来说缓存也生效
- UnityWebRequest系统拥有一个内部的worker线程池,和内部的任务系统去确保开发者不会同时开启大量线程去下载,目前这个线程池大小是不能配置的
3.5 建议
一般的应该尽可能使用AssetBundle.LoadFromFile,这个API在速度和磁盘的使用率和运行时内存方面都是最高效的
对于需要的下载AssetBundle或者给AssetBundle打补丁的项目,强烈推荐在 Unity 5.3 或更新版本中使用 UnityWebRequest ,在Unity5.2或者更老版本中使用WWW.LoadFromCacheOrDownload,可以在项目安装的时候将AssetBundle包解压从而实现准备好AssetBundle缓存
当使用 WWW.LoadFromCacheOrDownload时,为了避免内存峰值引起的程序闪退,强烈推荐确保AssetBundle保持在项目最大内存预算的2-3%,对于大多数项目而言,AssetBundle的文件大小最好不要大于5M,并且不要超过2个AssetBundle同时在下载。
当使用 WWW.LoadFromCacheOrDownload 或者 UnityWebRequest 是,确保下载的代码在加载完 AssetBundle 后正确的调用 Dispose。C# 的 using 是确保 WWW 或者 UnityWebRequest 的 Dispose 方法会被调用 的最简便的做法。
4 从AssetBundle中加载资源
有以下几种方式来从AssetBundle中加载UnityEngine.Object对象:
- LoadAsset (Async)
- LoadAllAssets( Async)
- LoadAssetWithSubAssets (Async)
同步版本的API总是比异步版本的API完成时间至少快一帧,Unity5.2之后的版本,异步API可以在每帧中加载多个对象,但最高不超过时间片的限制
当加载多个独立的UnityEngine.Object对象时,应该要使用LoadAllAssets,它应该用在当一个AssetBundle里的大部分都需要加载的时候,相对于其他两个API,LoadAllAsets比多次调用LoadAssets会稍微快一些。因此,如果需要加载的资源数量比较多,并且在同一时间需要加载的数量小于 AssetBundle 内资源数量的 2/3,可以考虑将这个 AssetBundle 切分为更小的 AssetBundle, 然后调用 LoadAllAssets
LoadAssetWithSubAssets应该用于加载一个包含多个嵌入对象的资源,比如包含动画的FBX模型或者多个精灵的图集,如果需要加载的对象都来自同一个资源,但是他们存储在一个拥有很多其他不相关的资源的AssetBundle中,那么可以使用这个API
其他情况,请使用LoadAsset或者LoadAssetsAsync
4.1 底层加载细节
UnityEngine.Object的加载不是在主线程中执行,一个存储介质上的对象的数据时在worker线程里读取的,其他一切Unity中不需要在主线程执行的系统(脚本,图形)将会被切换到worker线程中执行,比如,从网格中创建VBO,解压纹理等
Unity5.3之前的版本中,对象加载是串行的,并且对象的加载过程的某些不放呢只能在主线程中执行,这部分叫做集成,当worker线程完成对象数据的加载,这部分就会暂停,并把新家在的对象集成进主线程,并且保持到主线程集成完成。
从Unity5.3开始,对象加载被并行化了,worker线程中可以反序列化,处理和继承多个对象。当一个对象的加载完成后,它的Awake回调函数就会被调用,并且从下帧开始,这个对象会在Unity引擎中编程立即可用的。
同步版本的AssetBundle.Load方法会暂停主线程,知道对象加载结束为止,在Unity5.3之前,异步方法AssetBundle.LoadAsync在它需要将对象集成到主线程之前,他不会暂停主线程。它们会将对象加载按时间分片,使对象继承不会超过一定毫秒数量的帧时间,这个毫秒的数量是靠Application.backgroundLoadingPriority 这个属性来设置的:
- ThreadPriority.High: 每帧最多 50 毫秒
- ThreadPriority.Normal: 每帧最多 10 毫秒
- ThreadPriority.BelowNormal: 每帧最多 4 毫秒
- ThreadPriority.Low: 每帧组多 2 毫秒
从Unity5.2开始,可以加载多个对象直到超过了帧中加载时间的最大上限,假设其他条件都一样的情况下,AssetBundle.LoadAsync总是比同步版本的 API 需要更多的时间,因为从调用 LoadAsync到对象在引擎中可用期间有最小1帧延迟