Unity资源打包学习笔记(⼀)、详解AssetBundle的流程
转载请标明出处:
  本⽂参照unity官⽹上对于assetBundle的⼀系列讲解,主要针对assetbundle的知识点做⼀个梳理笔记,也为后续的资源打包设计做⼀个基础学习,本⽂的代码和图⽚均来⾃unity官⽹,详情可以查看Unity的DOCUMENTATION。
⼀、什么是AssetBundle
  AssetBundle就像⼀个ZIP压缩⽂件,⾥⾯存储着不同平台的特殊资源(models/texture/prefabs/materials/audio ),这些资源都可以在运⾏时进⾏加载。具体的assetBundle中主要包含什么?主要包含两种互相关联的东西:
1. 磁盘上的⽂件:也就是assetbundle⽂档,可以将其视为⼀个容器或者⽂件夹,其中包含两类⽂件:序列化⽂件和资源⽂件,序列化⽂件就是资源在打包后对应的各个平台的序列化操作后的⽂件,资源⽂件主要是针对textures/audio等较⼤的⽂件打包的⼆进制⽂件,这类⽂件在加载的时候是在其他线程执⾏的(效率更⾼)。
2. 就是实际的assetbundle对象了,可以通过代码来进⾏资源加载,其中主体是各个资源在进⾏加载的时
候的存储路径图。⽤图表⽰:
⼆、如何对资源进⾏分组便于打包AssetBundle吕良伟个人资料
  虽然可以对AssetBundle⾃由的进⾏规划,但是在进⾏项⽬的资源管理的时候,还是有⼀些规划建议可以采⽤:
1.依据逻辑实体进⾏分组
  这种资源分类⽅式是依据资源的功能进⾏分类,例如 UI/⾓⾊/场景/code等具体各⾃功能规划的部分来进⾏资源分组,可以将所有的textures/layout data都打⼊UI相关分类中,可以将所有的模型和动画都打⼊⾓⾊相关的资源中,将场景相关的贴图和模型都打⼊场景资源中。
  采⽤逻辑实体分组,对于资源的下载更新更为有利,由于资源的分类,可以在进⾏资源更新的时候,
只更新对应的资源,⽽⽆需更新冗余的其他资源。
  使⽤这种分类⽅式最合适的策略,就是将资源进⾏详细的分类(when and where will be used)
2.类型分组
  主要依据资源的类型来进⾏分组,这样对于不同的应⽤平台都具有⼀定的适⽤性。⽐如对于audio⽂件的压缩设置,在mac和windows 上都是⼀致的,那么可以将audio⽂件都归类为⼀类⽂件,实现⽂件资源的复⽤(不同平台的打包设置),对于shaders⽽⾔,对于不同平台需要不同的编译设置,那么就需要分类处理。这类分类⽅法,对于在不同的版本中变动频率较低的代码⽂件和prefabs显得更有优势。
3.相互关联的内容分组
  这种策略的,就是将需要同时进⾏加载的资源都归类为⼀个分组,例如将不同场景中的⾓⾊都依据场景来进⾏分组,这就要求单独⼀个场景中的资源只能⽤于该场景,各个分组之间没有互相关联的关系。这种分类⽅式,对于资源的加载时间有较⼤的缩减,这种分类⽅式的使⽤场合主要在场景资源中,在不同的场景资源中,其包含的资源各⾃互相不关联。
在⼀个项⽬中,可以将上述的⼏种策略都交互使⽤,对应具体的应⽤需求来灵活的采⽤分组策略,当然unity也提供了⼀些资源分组的tips:
分离⾼频和低频更新的资源;
将需要同时下载的资源合并进⼀个组,例如Model以及其关联的animations;
韩磊简介
如果出现多个bundle中的多个object都依赖于另⼀个完全不同的bundle,那么将这些依赖关系都移动到⼀个单独的bundle,这样可以降低依赖关系的复杂度;多个bundle均依赖于另⼀个bundle中的资源,那么将这些bundle以及其依赖的资源归类到⼀个资源,这样可以降低资源的重复率(避免⼀份资源被拷贝到多个不同的bundle中);
不可能同时加载的资源,需要归类的各⾃的assetbundle中,例如标准和⾼配的资源;
如果⼀个assetbundle中资源在加载的时候低于50%需要被加载,那么可以考虑将这些需要被加载的资源单独分类为⼀个资源(避免冗余的加载);
如果⼀组Objects对应的是⼀个资源的不同版本,那么可以考虑assetbundle variants
三、如何打包AssetBundle
  unity资源打包的接⼝,就是BuildPipeline.BuildAssetBundles函数,其对应具体的三个参数,第⼀个是bundle的输出路径,下⾯来详细分析⼀下剩下的2个参数:
1、BuildAssetBundleOptions
  具体的参数设置,可以参看API当中的详细信息,下⾯主要集中说三个参数,分别对应三种压缩格式的选择。
1)BuildAssetBundleOptions.None :
  采⽤LZMA的压缩格式,这种压缩格式要求资源在使⽤之前需要全部被解压,这就会带来在使⽤⼀个极⼩的⽂件的时候会额外带来较长的解压时间消耗。⽐较蛋疼的是,⼀旦这个bundle被解压之后,在磁盘上⼜会以LZ4的格式重新压缩,LZ4的压缩格式,在使⽤资源的时候不需要全部解压。
这种压缩格式主要⽤于⼀个bundle中资源都需要被加载的时候,例如打包⾓⾊或者场景资源的时候,这种压缩格式在初始化下载的时候被推荐(更⼩的包体),这些资源在被解压后,⼜会以LZ4的格式被缓存。
2) BuildAssetBundleOptions.UncompressedAssetBundle:
  ⽆压缩的打包,加载的⽂件更⼤,但是时间更快(省去解压的时间)
3)BuildAssetBundleOptions.ChunkBasedCompression:
  采⽤LZ4的压缩格式,相⽐于LZMA⽽⾔⽂件体积更⼤,但是不要求在使⽤之前整个bundle都被解压。LZ4使⽤chunk based 算法,这就运⾏⽂件以chunk或者piece的⽅式加载,只解压⼀个chunk⽂件,⽽⽆需解压bundle中其余不相关的chunk。
2、BuildTarget
  也就是当前资源需要被使⽤的平台的分类,在打完资源后,会发现⽂件夹中有更多的⽂件,⼀般是2*(n+1), 对于各个不同的资源,都会有⼀个manifest⽂件,⼀般是 bundlename+".manifest",除此之外,还会有⼀个额外的manifest⽂件,对应不同的平台会有不同的额外的manifest(这是⼀个总体的manifest⽂件)。
对于manifest中内容,可以在⽂本⽂件中打开,⼀般举例:
ManifestFileVersion: 0
CRC: 2422268106
Hashes:
AssetFileHash:
serializedVersion: 2
Hash: 8b6db55a2344f068cf8a9be0a662ba15
TypeTreeHash:
serializedVersion: 2
Hash: 37ad974993dbaa77485dd2a0c38f347a
HashAppended: 0
ClassTypes:
- Class: 91
Script: {instanceID: 0}
Assets:
Asset_0: Assets/ller
Dependencies: {}
  ⾸先是版本,然后是CRC校验码,hash码,classtypes,包含的资源及其路径,依赖关系(dependencies)。
PS:
  对于依赖关系就多详细讲解⼀下,所谓的依赖关系,指的是⼀个bundle中的⼀个或者多个UnityEngine.Objects包含对另⼀个bundle中的⼀个UnityEngine.Object的引⽤,如果⼀个bundle中的object对包含对其他⾮该bundle的object的引⽤,但是该object不属于任何⼀个bundle,那么这种依赖关系就不存在(这⼉会带来⼀个问题,如果多个bundle均引⽤⼀个不属于bundle的object,那么在加载的时候,各个bundle均会将该object进⾏copy到其内存中,这就带来内存的额外冗余占⽤),所以依赖关系就是两个或者多个bundle之间的object的引⽤关系。
  对于依赖资源的加载,需要在该资源被加载前加载,unity不会⾃动的加载依赖资源,这需要在代码中实现依赖资源的加载。
四、如何使⽤AssetBundle
  在说完bundle的分类,打包后,接下来就是如何在实际的游戏中加载使⽤这些bundle。在unity5以后,
提供了4种不同类型的加载接⼝,下⾯逐⼀分析⼀下这四种不同接⼝的使⽤:
1、AssetBundle.LoadFromMemoryAsync(byte[] binary, unit crc = 0)
  这个⽅法⽤来加载ab数据的bytes数组,如果数据是使⽤LZMA的压缩格式,那么在加载的时候会进⾏解压的操作,LZ4格式的数据则会保持其压缩的状态,使⽤⽰例:
using UnityEngine;
using System.Collections;
using System.IO;
public class Example : MonoBehaviour
{
IEnumerator LoadFromMemoryAsync(string path)
{
AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
yield return createRequest;
AssetBundle bundle = createRequest.assetBundle;
var prefab = bundle.LoadAsset<GameObject>("MyObject");
Instantiate(prefab);
}
}
  当然,对于bytes数组,也可以使⽤File.ReadAllBytes(path)的⽅式来加载数组。
2、 AssetBundle.LoadFromFile
  在加载⾮压缩⽂件或者LZ4压缩类型⽂件的时候,该接⼝效率极⾼,对于LZMA压缩格式的⽂件,也会在加载的时候执⾏解压的操作,使⽤⽰例:
public class LoadFromFileExample extends MonoBehaviour {
function Start() {
var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle"));
if (myLoadedAssetBundle == null) {
Debug.Log("Failed to load AssetBundle!");
return;
}
var prefab = myLoadedAssetBundle.LoadAsset.<GameObject>("MyObject");
Instantiate(prefab);
}
}
ps: 在unity5.3及更早的版本中,在安卓平台上如果从streaming assets路径中加载⽂件会失败(路径⽂件夹中会额外包含.jar⽂件)。
3、WWW.LoadFromCacheOrDownload
  这个接⼝会被淘汰(被UnityWebRequest替换),那么就不过多的讲解这个接⼝(注意这个接⼝会进⾏存储分配的操作以容纳资源,如果分配不⾜以存储会使得加载失败)。
4、UnityWebRequest
  这个接⼝,会有两步操作,⾸先是创建⼀个web request(调⽤UnityWebRequest.GetAssetBundle), 然后进⾏资源的获取(调⽤DownloadHandlerAssetBundle.GetContent),unity提供的使⽤⽰例为:
IEnumerator InstantiateObject()
{
资源整合string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
范玮琪婚纱照Instantiate(cube);
Instantiate(sprite);
}
  使⽤这种⽅式,可以使得开发者更为灵活的操作下载数据,同时进⾏内存使⽤分配,会逐渐的被⽤来WWW接⼝。
  在加载完assetBundle后,接下来,就是如何从bundle中获取资源(asset),其基本的接⼝模板为:
T objectFromBundle = bundleObject.LoadAsset<T>(assetName);
  如果想获取所有的assets则可以使⽤接⼝:
Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();
  ⼀旦获取到asset,那么就可以在游戏中使⽤这些资源了(⼀般是实例化创建操作)。
5、加载AssetBundle Manifest
  除了加载assetbundle,⼀般还会加载其对应的manifest(与其存储在同⼀个⽂件夹下的相同名字的manifest),⼀般加载manifest的操
作⽰例:
AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
  在前⽂也提及到,如果⼀个assetbundle依赖于另⼀个assetbundle,那么需要提前加载依赖相关的bu
ndle,那么依据manifest,可以加载其依赖的assetbundle:
物流投诉最狠的方式
AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies("assetBundle"); //Pass the name of the bundle you want the dependencies for. foreach(string dependency in dependencies)
{
AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));
}
  现在,已经加载了assetbundle, 也获取了assetbundle的dependencies,以及其中assets,这样就可以管理这些assetbundle了。
五、管理AssetBundle
  unity在场景中的Object被移除的时候不⾃动释放objects,资源的清理需要再特定的时间触发(场景切换)或者⼿动的管理。所以怎么加载和卸载资源显得尤为重要,不合适的加载可能会导致资源的重复加载,不合适的卸载可能会带来资源的缺失(⽐如丢失贴图)。
  对于assetbundle的资源管理,最重要的是掌握什么时候调⽤AssetBundle.Unload(bool)这个函数,传⼊true/false会有不同的卸载策略。这个API会卸载对应的assetbundle的头部信息,参数对应着是否同时卸载从该assetbundle中实例化的所有Objects。
  AssetBundle.Unload(true)会卸载assetbundle中的所有gameobjects以及其依赖关系,但是并不包括基于其Objects实例化(复制)的Object(因为这些object不属于该assetbundle,只是引⽤),所以当卸载贴图相关的assetbundle的时候,场景中对其引⽤的实例化物体上会出现贴图丢失,也就是场景中会出现红⾊的区域,unity都会将其处理成贴图丢失。
  举例说明,假设材质M来⾃于assetbundle AB, 如果 AB.Unload(true), 那么场景中任何M的实例都会被卸载和销毁,如果
AB.Unload(false),那么就会切断材质M实例与AB之间的关系:
  那么如果该assetbundle AB在后⾯再次被加载,unity不会重新关联其关系,这样在后续的使⽤中,就会出现⼀份材质的多个实例:
  所以通常情况下,AssetBundle.Unload(false) 并不能带来较为合理的释放结果,AssetBundle.Unloa
d(true)通常⽤来确保不会在内存中多次拷贝同⼀个资源,所以其更多的被项⽬所采纳,此外还有两个常⽤的⽅法⽤来确保其使⽤:
1)在游戏中,对于场景的卸载有明确的规划,⽐如在场景切换中或者场景加载中;
2)管理好对每个单独的object的计数,只有在没有引⽤的时候才卸载该assetbundle,这样可以规避加载和卸载过程中的多份内存拷贝问题。
许秀琴近况
  如果要使⽤AssetBundle.Unload(false),那么这些实例化的对象可以通过2中途径卸载:
1)清除对不需要物体的所有引⽤,场景和代码中都需要清楚,然后调⽤Resources.UnloadUnusedAssets;
2) 在场景加载的时候采⽤⾮增量的⽅式加载,这会清楚当前场景中的所有Objects,然后反射⾃动调⽤Resources.UnloadUnusedAssets
如果你不想管理这些assetbundle,unity推出了AssetBundle Manager,可以学习了解⼀下,此外Unity还推出了⼀些AssetBundle Browser Tool, 也可以学习了解⼀下。