AssetBundle相关知识
什么是AssetBundle
一个AssetBundle可以当做一个文件集合,它包含了Unity可以在运行时加载的特定于平台的非代码资产(例如模型、纹理、预制组件、音频,甚至整个场景)。AssetBundle可以表示彼此之间的依赖关系;例如,一个AssetBundle中的material可以引用另一个AssetBundle中的texture。为了减轻网络传输压力,可以根据需求选择内置算法(LZMA和LZ4)来压缩AssetBundle。
AssetBundle对于可下载内容(DLC)、减少初始安装大小、加载针对最终用户平台优化的资产以及减少运行时内存压力都很有用。
AssetBundle里有什么
“AssetBundle”可以指两个不同但相关的东西。
首先是磁盘上的实际文件。这称为AssetBundle archive。AssetBundle archive是一个容器,就像一个文件夹一样,其中包含了额外的文件。这些额外的文件包括两类:
- 一个序列化文件,它包含您的资产分解成它们各自的对象并写入到这个文件中。
- 另一个是资源文件,这是二进制数据块单独存储的某些资产(纹理和音频),以允许Unity高效地在另一个线程从磁盘上加载他们。
“AssetBundle”也可以指通过代码与实际的AssetBundle对象交互,从特定的AssetBundle加载资产。这个对象包含了所有我们当初添加到包里面的内容。
Unity资产映射
Unity把序列化拆分成两个表达部分,第一部分叫做File GUID。标识这个资产的位置,这个GUID是由Unity根据内部算法自动生成的,并且存放在原始文件的同目录、同名但是后缀为.meta的文件里。
确定了资产文件之后,还需要一个Local IDs来表示当前的Objects在资产里的唯一标识。File GUID确保了资产在整个Unity工程里唯一,Local ID确保Objects在资产里唯一,这样就可以通过二者的组合去快速找到对应的引用。
Unity还在内部维护了一张资产GUID和路径的映射表,每当有新的资源进入工程,或者删除了某些资源。又或者调整了资源路径,Unity的编辑器都会自动修改这张映射表以便正确的记录资产位置。所以如果.meta文件丢失或者重新生成了不一样的GUID的话,Unity就会丢失引用,在工程内的表现就是某个脚本显示“Missing”,或者某些贴图材质的丢失导致场景出现粉红色。
File GUID和Local ID确实已经能够在编辑器模式下帮助Unity完成它的规划了,与平台无关、快速定位和维护资源位置以及引用关系。但若投入到运行时,则还有比较大的性能问题。也就是说运行时还是需要一个表现更好的系统。
于是Unity又弄了一套缓存(还记得前面那套缓存嘛,是用来记录GUID和文件的路径关系的)PersistentManager,用来把File GUIDs 和 Local IDs转化为一个简单的、Session唯一的整数。这些整数就是Instance Id。Instance Id很简单,就是一个递增的整数,每当有新对象需要在缓存里注册的时候,简单的递增就行。
简单的来说:
PersistentManager会维护Instance ID和File GUID 、Local ID的映射关系,定位Object源数据的位置以及维护内存中(如果有的话)Object的实例。只要系统解析到一个 Instance ID,就能快速找到代表这个Instance ID的已加载的对象。如果Object没有被加载的话,File GUID 和 Local ID也可以快速的定位到指定的Asset资源从而即时进行资源加载。
Object加载
当Unity的应用程序启动的时候,PersistentManager的缓存系统会对项目立刻要用到的数据(比如启动场景里的这些或者它的依赖项),以及所有包含在Resources 目录的Objects进行初始化。如果在运行时导入了Asset或者从AssetBundles(比如远程下载下来的)加载Object都会产生新的Instance ID。
另外Object在满足下列条件的情况时会自动加载,比如:
1、某个Object的Instance ID被间接引用了。
2、Object当前没有被加载进内存。
3、可以定位到Object的源位置(File GUID 和 Local ID)。
另外,如果File GUID和LocalID没有Instance ID,或者有Instance ID,但是对应的Objects已经被卸载了,并且这个Instance ID引用了无效的File GUID和LocalID,那么这个Objects的引用会被保留,但是实际Objects不会被加载。在Unity的编辑器里会显示为:“(Missing)”引用,而在运行时,根据Objects类型不一样,有可能会是空指针,有可能会丢失网格或者纹理贴图导致场景或者物体显示粉红色。
Object卸载
除了加载之外,Objects会在一些特定情况下被卸载。
1、当没有再使用的Asset在执行清理的时候,会自动卸载对应的Object。一般是由切场景或者手动调用了Resources.UnloadUnusedAssets的 API时候触发的。但是这个过程只会卸载那些没有任何引用的Objects。
2、从Resources目录下加载的Objects可以通过调用Resources.UnloadAsset API进行显式的卸载。但这些Objects的 Instance ID会保持有效,并且仍然会包含有效的File GUID 和 LocalID 。当任何Mono的变量或者其他Objects持有了被Resources.UnloadAsset卸载的Objects的引用之后,这个Object在被直接或者间接引用之后马上被加载。
3、从AssetBundles里得到的Objects在执行了AssetBundle.Unload(true) API之后,会立刻自动的被卸载。并且这会立刻让这些Objects的File GUID 、 Local ID以及Instance ID立马失效。任何试图访问它的操作都会触发一个NullReferenceException。但如果调用的是AssetBundle.Unload(false)API的话,那么生命周期内的Objects不会随着AssetBundle一起被销毁,但是Unity会中断 File GUID 、Local ID和对应Object的Instance IDs之间的联系,也就是说,如果这些Objects在未来的某些时候被销毁了,那么当再次对这些Objects进行引用的时候,是没法再自动进行重加载的。
另外,如果Objects中断了它和源AssetBundle的联系之后,那么再次加载相同Asset的时候,Unity也不会复用先前加载的Objects,而是会重新创建 Instance ID,也就是说内存里会有多份冗余的资源。
AssetBundle依赖与冗余介绍
Unity 5.x版本里提供了更加人性化的依赖自动管理机制——对指定打包的资源,Unity会自动收集并分析其依赖的资源,如果该资源依赖的某个资源没有被显式指定打包到ab中,就将其依赖的这个资源打包进该资源所在的ab里;如果已经被指定打包进其他ab里,那么这两个ab之间就会构成依赖关系,加载ab时,先加载其依赖的ab即可。
请避免ab循环依赖,比如a依赖b,b也依赖a,那么加载a的时候会去先加载a在b中的依赖资源,那么就得去加载b,加载b前又得去加载a,造成死循环。
这一套依赖管理机制使用方便的同时也会带来一个问题,如果两个ab A和B中的一些资源都依赖了一个没有被指定要打包的资源C,那么C就会同时被打进ab A和B中,造成资源的冗余,增大ab和安装包的体积。
AssetBundle工作流
打包AB
Unity - Manual: AssetBundle workflow (unity3d.com)
加载AB
Unity中资源相关目录介绍
1、Resources 是Unity3D系统指定文件夹,如果你新建的文件夹的名字叫Resources,那么里面的内容在打包时都会被打到发布包中。文件夹特点:
- 只读,即不能动态修改。所以想要动态更新的资源不要放在这里。
- 会将文件夹内的资源打包集成到.asset文件里面。
- 主线程加载。
- 资源读取使用Resources.Load()。
2、StreamingAssets 也是Unity3D系统指定文件夹 ,和Resources文件的区别就是Resources文件夹中的内容在打包时会被压缩和加密。而StreamingAsset文件夹中的内容则会原封不动的打入包中,因此StreamingAssets主要用来存放存放打包的AB资源,然后用户安装包之后把这些AB资源是放到手机内。文件夹特点:
- 只读,可以放一些压缩的AB资源。
- 只能用过WWW类来读取。
3、PersistentDataPath 是可读写路径。在iOS上就是应用程序的沙盒,但是在Android可以是程序的沙盒,也可以是sdcard。并且在Android打包的时候,ProjectSetting页面有一个选项Write Access,可以设置它的路径是沙盒还是sdcard。文件夹特点:
- 内容可读写。
- 无内容限制。一般是从StreamingAsset中读取二进制文件或者从AssetBundle读取文件来写入PersistentDataPath中。
- 一般游戏活动通过WWW动态下载的图片也可以缓存到此目录,方便下次快速打开。
资源相关路径介绍
Unity 3D 中的资源路径 | 介绍 |
---|---|
Application.dataPath | 此属性用于返回程序的数据文件所在文件夹的路径。例如在Editor中就是Assets |
Application.streamingAssetsPath | 此属性用于返回流数据的缓存目录,返回路径为相对路径,适合设置一些外部数据文件的路径。放在Unity工程StreamingAssets文件夹中的资源发布后都可以通过这个路径读取出来 |
Application.persistentDataPath | 此属性用于返回一个持久化数据存储目录的路径,可以在此路径下存储一些持久化的数据文件 |
Application.temporaryCachePath | 此属性用于返回一个临时数据的缓存目录 |
Android 平台资源路径
Android 平台 | 具体路径 |
---|---|
Application.dataPath | /data/app/xxx.xxx.xxx.apk |
Application.streamingAssetsPath | jar:file:///data/app/xxx.xxx.xxx.apk/!/assets |
Application.persistentDataPath | /data/data/http://xxx.xxx.xxx/files |
Application.temporaryCachePath | /data/data/http://xxx.xxx.xxx/cache |
iOS 平台资源路径
iOS 平台 | 具体路径 |
---|---|
Application.dataPath | Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/xxx.app/Data |
Application.streamingAssetsPath | Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/xxx.app/Data/Raw |
Application.persistentDataPath | Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Documents |
Application.temporaryCachePath | Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Library/Caches |
可以看出
dataPath 和 streamingAssetsPath 的路径位置一般是相对程序的安装目录位置,persistentDataPath 和 temporaryCachePath 的路径位置一般是相对所在系统的固定位置。
加载AB的API
- AssetBundle.LoadFromFile(path):同步加载,path为本地路径
- AssetBundle.LoadFromFileAsync(path):异步加载,path为本地路径
- AssetBundle.LoadFromMemory(byte[] binary):从字节数组加载,binary为目标ab二进制流
- AssetBundle.LoadFromMemoryAsync(byte[] binary):从字节数组异步加载,binary为目标ab二进制流
- UnityWebRequest.GetAssetBundle(string uri):url为ab文件路径,可为本地,也可为云端,
从AB中加载具体的资产(Asset)API
- assetBundle.LoadAsset<T>(name):T为目标资产类型,name为资产名称,会返回一个T实例
- assetBundle.LoadAsset(name,type):name为资产名,type为资产类型
- assetBundle.LoadAllAssets<T>():T为目标资产类型,会返回一个assetBundle中所有T类型资产数组
- assetBundle.LoadAllAssets():加载assetBundle中所有资产,返回一个assetBundle中所有资产数组
卸载AB的API
- assetbundle.Unload(bool unloadAllLoadedObjects):unloadAlLoadedlObjects:是否卸载所有加载的资源,参数为false时,AssetBundle内的文件内存镜像会被释放,实例化的物体还都保持完好。简单的说就是断开了AssetBundle内存镜像和实例之间的联系。如果再次实例化对象,也不会返回以前初例化过的AssetBundle内存镜像,而是重新实例化一个新的AssetBundle内存镜像,那么这样就出现了冗余,同样的资源,内存中会出现多份。参数为true时,卸载AssetBundle,并且删除被引用的资源。这种卸载方式,最为彻底,完全从内存移除,缺点是需要一套机制(目前流行的是引用计数),来关注是不是还有资源引用,会不会引起异常。
- AssetBundle.UnloadAllAssetBundles(bool unloadAllObjects):unloadAllObjects:是否卸载所有加载的资源,如果为true,则会卸载所有资源,包括正在使用着(被依赖)的资源。,如果为false,则会卸载未被依赖的资源,被其他资源依赖的资源不会被卸载。
总结:AssetBundle.Unload(false)适用于一次性使用的资源,获得资源引用后直接调用,当删除引用后,下次调用Resources.UnloadUnusedAssets后就删除了。AssetBundle.Unload(true)在使用中,最好的做法是给创建出来的实例都添加计数,当计数不为0时,表示场景或代码中仍有引用,而当计数为0时,表示没有引用了,这样就可以AssetBundle.Unload(true)了。