AssetBundle
AssetBundle(阿赛特邦豆)是Unity官方推荐的资源加载方式,网上对AssetBundle的介绍有很多,且在了解了Unity对资源的加载机制后,其本身没有什么特别难以理解的地方了,因此在这不过多介绍,仅挑选几个关键点进行阐述。
AssetBundle的生成
生成AssetBundle有很多种方式,在此仅简单说一下比较常用的方式,使用BuildPipeline生成AssetBundle文件。
每一次调用BuildPipleLine.BuildAssetBundles时,将会生成一批AssetBundle文件,具体数量根据传递AssetBundleBuild数组决定,每一个AssetBundleBuild对象将对应一个AssetBundle及一个同名+.manifest后缀文件。其中AssetBundle文件的后缀用户自行设置,比如".unity3d",".ab"等等;而.manifest文件是给人看的,里面有这个AssetBundle的基本信息以及非常关键的资源列表。
除了AssetBundleBuild数组所定的AssetBundle外,还将额外在output路径下生成的一对与output文件夹同名的文件及一个同名.manifest后缀文件。这个同名文件可厉害了,它记录了这批次AssetBundle之间的相互依赖关系。当然.manifest文件还是给人看的,我们可以用它分析资源间的依赖关系,但是在项目实际运行时,Unity并不会关心它。
AssetBundle的加载
根据AssetBundle文件所在的位置(本地、远端),AssetBundle有不同的加载方式,在此仅总结最常用的本地AssetBundle文件加载。
AssetBundle可以拆分为:Bundle加载和Asset加载两部分。因为AssetBundle文件可以从功能上分为两大块:
1、记录文件标记、压缩信息、文件列表的Header部分;
2、记录资源实际内容的Data部分。
当使用AssetBundle.LoadFromFile或LoadFromFileAsync时,在pc平台及移动平台上,unity仅会为我们读取AssetBundle的header部分,并不会将bundle的data部分整个读入内存。
当调用上一步生成的AssetBundle对象读取具体资源时(LoadAsset, LoadAssetAsync, LoadAllAssets),Unity会参考已经缓存的文件列表,找到目标资源在data部分的位置并读入到内存中。
如果一个资源引用到了其他资源,则必须要先读入被引用资源的AssetBundle文件,否则就会发生引用Miss。为了避免上面Miss的情况,在加载资源时,首先需要将该资源的依赖项全部加载完毕,不过仅需加载依赖资源的AssetBundle文件。也就是说,我们只要将该依赖AssetBundle的Header部分加载(AssetBundle.LoadFromFile或LoadFromFileAsync)就可以,这样在真正读取Asset时,Unity会自动处理好真实依赖的Asset,我们不用操心。
AssetBundle的依赖关系如何读取呢?加载上面提到的那个很厉害的文件就可以了。
非常简单的获取依赖关系的方法,通常会在项目启动时将全部依赖关系保存下来。
AssetBundle的使用
当AssetBundle被成功加载后,调用该Assebbundle对象的LoadAsset、LoadAllAssets或对应的异步版本即可加载资源,也就是实例化对象。如果这个对象已经被加载过,Unity并不会重复加载,还记得之前所说的映射表么,被加载过的资源就好比挂上了数字牌的钥匙,直接对地址解引用即可。
AssetBundle的卸载
如果说AssetBundle真的有什么容易出问题的地方,那恐怕就是卸载了。
在这里只说最常用的这个卸载方法吧:
public void Unload(bool unloadAllLoadedObjects);
一个被加载过的AssetBundle可以通过调用Unload来卸载这个Bundle下所有的Asset。但是调用这个函数时传入的参数对卸载结果影响甚大。
Unity官方对这个函数的讲解非常详细,配图也非常直观,因此我只是简单总结一下。
相同点:
无论传入参数为 true 或是 false,调用Unload都可以Destroy当前AssetBundle对象,释放之前从AssetBundle文件中的Header部分所获取的信息。当然,被释放的AssetBundle对象无法再使用诸如LoadAsset、LoadAllAssets等函数加载资源。
不同点:
unloadAllLoadedObjects == true:
不仅Destroy了AssetBundle这个对象,而且这个AssetBundle下包含的所有对象,只要实例化了,有一个算一个,统统释放掉。
感觉就像
foreach(Object asset in assets)
{
if(asset != null)
{
delete asset;
asset = null;
}
}
比如你通过ab.LoadAsset(apple)后,将apple设置给go_0的一个Renderer,如果这时候ab.Unload(true),那go_0就傻了,咋回事儿啊,图咋没了呢?WTF啊。
它的好处是:不会有重复资源问题的情况发生,每次都处理的干干净净。
unloadAllLoadedObjects == false:
仅仅Destroy了AssetBundle这个对象,但是并没有释放这个AssetBundle下的任何Asset,因此如果有对象引用了这些Asset,也不会有问题。
它的风险(代价)是:下次再Load这个AssetBundle,并且通过这个AssetBundle重新读取了这个Asset,会在内存中重新创建一份,这样如果之前的Asset没有被释放,那么现在内存中就有两份Asset了。
这种情况如果频繁发生,便意味着内存中有很多资源将“不受控制”,容易引发内存占用过高的问题,而释放这种不受控的资源,仅有两种方式:
1、当没有对象引用到这些不受控资源时,每次调用Resources.UnloadUnusedAssets,回收之。
2、加载场景时,如果加载模式没有设置为LoadSceneMode.Additive,则会自动调用Resources.UnloadUnusedAssets。