1 基本API
1.1 打包唯一API
public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform)
调用BuildPipeline.BuildAssetBundles,引擎将自动根据资源的assetbundleName属性(以下简称abName)批量打包,自动建立Bundle以及资源之间的依赖关系。
1.2 收集依赖API
AssetDatabase.GetDependencies(string path, [default bool recursive = true]);
默认开启,true会收集依赖及其间接依赖,false只会收集直接依赖。
1.3 打包选项API
ForceRebuildAssetBundle 用于强制重打所有AssetBundle文件; IgnoreTypeTreeChanges 用于判断AssetBundle更新时,是否忽略TypeTree的变化; AppendHashToAssetBundleName 用于将Hash值添加在AssetBundle文件名之后,开启这个选项包名变为{xxx_hash},就可以直接通过文件名来判断哪些Bundle的内容进行了更新(4.x下普遍需要通过比较二进制等方法来判断,但在某些情况下即使内容不变重新打包,Bundle的二进制也会变化)。
5.x下默认会将TypeTree信息写入AssetBundle,因此在移动平台上DisableWriteTypeTree选项可优化。
1.4 Manifest文件
namespace UnityEngine { public class AssetBundleManifest : Object { public extern string[] GetAllAssetBundles(); public extern string[] GetAllAssetBundlesWithVariant(); public Hash128 GetAssetBundleHash(string assetBundleName); public extern string[] GetDirectDependencies(string assetBundleName); public extern string[] GetAllDependencies(string assetBundleName); } }
在打包后生成的文件夹中,每个Bundle都会对应一个manifest文件,记录了Bundle的一些信息,但这类manifest只在增量式打包时才用到;同时,根目录下还会生成一个同名manifest文件及其对应的Bundle文件,通过该Bundle可以在运行时得到一个AssetbundleManifest对象,而所有的Bundle以及各自依赖的Bundle都可以通过该对象提供的接口进行获取。
1.5 Variant参数
在Inspector界面最下方,除了可以指定abName,在其后方还可以指定Variant。打包时,Variant会作为后缀添加在Bundle名字之后。相同abName,不同variant的Bundle中,资源必须是一一对应的,且他们在Bundle中的ID也是相同的,从而可以起到相互替换的作用。
variant可以根据不同性能设备设置不同分辨率的资源,借助Variant的特性,只需创建两个文件夹,分别放置两套不同的资源,且资源名一一对应,然后给两个文件夹设置相同的abName和不同的variant。在加载该abName的时候指定对应的Variant即可。
1.6 包体结构
Unity官网也给出的详细说明,上面普通包体结构文件分为两个部分:序列化信息和资源信息。下面是场景包体结构:Unity说了该图Main scene和Shared data的内容是相反的
序列化信息包含标识符、压缩类型和内容清单。清单是一个以Objects name为键的查找表。每个条目都提供一个字节索引,用来指示该Objects在AssetBundle数据段的位置。在加载AssetBundle时会优先加载序列化信息。
资源信息包含通过序列化AssetBundle中的Assets而生成的二进制原始数据。如果指定LZMA压缩,则对所有序列化Assets后的完整字节数组进行压缩。如果指定了LZ4,则分段压缩每个Assets的字节。如果不使用压缩,数据段将保持为原始字节流。
2 打包需要注意的问题
2.1 资源冗余问题
这里说的资源冗余是指同一份资源被打包到多个AB里,这样就造成了存在多份同样资源。在Unity运行时加载后内存中会出现多份同样的资源,造成内存开销
资源冗余造成的问题:
- 冗余造成内存中加载多份同样的资源占用内存。
- 同一份资源通过多次加载,IO性能消耗。
- 导致包体过大。
解决冗余方案是依赖打包:
依赖打包是指资源间虽有依赖关系,但该资源被多个assetbundle重复依赖,在打包时要将被重复依赖的资源单独打成assetbundle,这样就形成了AB与AB之间的依赖。Unity4.x版本提供的API是:
BuildPipeline.PushAssetDependencies
BuildPipeline.PopAssetDependencies
Unity5.x及以后提供的是指定AssetBundle Name的形式以及增量打包,Note:关于增量打包,Unity维护了一张全局.manifest文件,它能让我们知道更新了哪些AB,但是并没有告诉更新的AB中用到了哪些Assets,所以用到哪些Assets依然需要自己解决,因为只有知道了一个AB存储了哪些Asset信息,我们才能在加载AB的时候对特定的Asset做缓存、释放等操作。毕竟只有在知道包名的前提下才能加载所需的Asset。出现这种问题,一般都是人为的拖拽了某个Asset到了新目录、或者删除某些Asset但是代码却没有更新。这都是隐藏的问题需要注意。针对这个问题也有解决办法,见下文。
第二个就是增量打包也是热更新涉及的范畴。
2.2 打包策略问题
虽然可以对AssetBundle打包策略自由的进行规划,但是在进行项目的资源管理的时候,Unity官网提供了一些建议可以:
2.2.1 依据逻辑实体进行分组
这种资源分类方式是依据资源的功能进行分类,例如 UI/角色/场景/code等具体各自功能规划的部分来进行资源分组,可以将所有的textures都打入UI相关分类中,可以将所有的模型和动画都打入角色相关的资源中,将场景相关的贴图和模型都打入场景资源中。
采用逻辑实体分组,对于资源的下载更新更为有利,由于资源的分类,可以在进行资源更新的时候,只更新对应的资源,而无需更新冗余的其他资源。
使用这种分类方式最合适的策略,就是将资源进行详细的分类
2.2.2 类型分组
主要依据资源的类型来进行分组,这样对于不同的应用平台都具有一定的适用性。比如对于audio文件的压缩设置,在mac和windows上都是一致的,那么可以将audio文件都归类为一类文件,实现文件资源的复用(不同平台的打包设置),对于shaders而言,对于不同平台需要不同的编译设置,那么就需要分类处理。这类分类方法,对于在不同的版本中变动频率较低的代码文件和prefabs显得更有优势。
2.2.3 相互关联的内容分组
这种策略的,就是将需要同时进行加载的资源都归类为一个分组,例如将不同场景中的角色都依据场景来进行分组,这就要求单独一个场景中的资源只能用于该场景,各个分组之间没有互相关联的关系。这种分类方式,对于资源的加载时间有较大的缩减,这种分类方式的使用场合主要在场景资源中,在不同的场景资源中,其包含的资源各自互相不关联。
在一个项目中,可以将上述的几种策略都交互使用,对应具体的应用需求来灵活的采用分组策略,当然unity也提供了一些资源分组的tips:
- 分离高频和低频更新的资源;
- 将需要同时下载的资源合并进一个组,例如Model以及其关联的animations;
- 如果出现多个bundle中的多个object都依赖于另一个完全不同的bundle,那么将这些依赖关系都移动到一个单独的bundle,这样可以降低依赖关系的复杂度;多个bundle均依赖于另一个bundle中的资源,那么将这些bundle以及其依赖的资源归类到一个资源,这样可以降低资源的重复率(避免一份资源被拷贝到多个不同的bundle中);
- 不可能同时加载的资源,需要归类的各自的assetbundle中,例如标准和高配的资源;
- 如果一个assetbundle中资源在加载的时候低于50%需要被加载,那么可以考虑将这些需要被加载的资源单独分类为一个资源(避免冗余的加载);
- 如果一组Objects对应的是一个资源的不同版本,那么可以考虑assetbundle variants
上面三点也仅是给出了一个参考方向,具体项目需要具体分析。
2.3 AssetBundle压缩格式问题
AB压缩不压缩问题,主要考虑的点如下:
- 加载时间
- 加载速度
- 资源包体大小
- 打包时间
- 下载AB时间
到底要不要压缩,采用LZMA还是LZ4,也是需要具体项目具体分析。LZMA适合从服务器下载。
3 实施打包
3.1 给定入口目录,指定根资产
入口目录和根资产决定一个包体。为什么要指定根资产?这涉及到AB包的命名,而根资产也是包体内的mainAsset。下面给出一个自定义的数据结构
public class AssetInfo : System.IComparable<AssetTarget> { // 目标Object public Object asset; // 文件路径 public FileInfo file; // 相对目录 public string assetPath; // 是否是内置的 public bool isBuiltin = false; // 资产类型 public AssetBundleBuildType buildType = AssetBundleBuildType.Asset; // 保存地址 public string bundleSavePath; // BundleName public string bundleName; // 短名 public string bundleShortName; // 目标文件是否已改变 private bool _isFileChanged = false; // 是否已分析过依赖 private bool _isAnalyzed = false; // 依赖树是否改变(用于增量打包) private bool _isDepTreeChanged = false; // 上次打包的信息(用于增量打包) private AssetCacheInfo _cacheInfo; // .meta 文件的Hash private string _metaHash; // 上次打好的AB的CRC值(用于增量打包) private string _bundleCrc; // 是否是第一次打包
private bool _isNewBuild; // 我依赖的项 private HashSet<AssetTarget> _imDependSet = new HashSet<AssetTarget>(); // 依赖我的项 private HashSet<AssetTarget> _dependMeSet = new HashSet<AssetTarget>(); }
既然有了根资产(Root),那就能确定该Root的所有依赖资产(普通资产DepAsset),进而就能确定哪些资产能组成一个包。这就涉及到了依赖分析,见下。
3.2 依赖分析
上面提到了根资产Root、普通资产DepAsset能组成一个包,没错。但是由于冗余问题,DepAsset可能会被多个Root、或多个DepAsset依赖,也就是会被多个AB包依赖,那么该资产就要单独打包。伪代码
void Analyze() { 第一步 //先获取该asset的所有依赖,它可能是root、也可能是普通depAsset Object[] deps = EditorUtility.CollectDependencies(new Object[] { asset }); //然后过滤一次脚本、光照内置资源,这里也可以过滤shader资源 list<Object> realDeps; for(i < deps.count) { Object o = deps[i]; if(o is 脚本 || o is 光照 || /*o is shader*/) continue; //过滤内置资源 Resources/builtin 与 Library/builtin path = getAssetPath(o); if(path.StartWith("Resources" || "Library")) continue; realDeps.add(o); } 第二步 //拿到所有依赖项之后,再分析依赖项的依赖 for(i < realDeps.count) { //把object转换为assetInfo depAsset = Load(realDep[i]); //双向依赖添加 //添加asset的"我依赖的项",这里面不能自己依赖自己 asset._imDependSet.add(depAsset); //添加depAsset的"依赖我的项" depAsset._dependMeSet.add(Asset); //分析depAsset依赖。 就这样循环下去,分析出所有 depAsset.Analyze(); } }
上面分析出了以根资产为入口的依赖,及其依赖的依赖。 接下来要分析重复依赖,伪代码
void 合并依赖()
{
//该assetInfo的”依赖我的项”个数是否大于x?这里x可以配置,有些项目可能允许一定的重复打包。这里先x=1.
//单独成包
if _dependMeSet.count > x
{
//依赖我的项就只直接依赖我,而不能再间接依赖我依赖的项,就需要从它们的“我依赖的项”中删除那一部分,且双向删除
//换句话讲,A依赖C,B依赖C,C依赖D。那么依赖C的A和B的“我依赖的项”就不需要D只要C了。A与B删除我依赖的项D,D删除依赖我的项A与B。
//这样C单独成包,就只有包与包直接的依赖
//取出直接依赖我的项parent
foreach(parent dependMeSet)
{
//取出我直接依赖的项
foreach(child imDependSet)
{
parent._imDependSet.remove(child);
child._depentMeSet.remove(parent);
}
}
}
}
上面合并了依赖之后,就要识别普通depAsset是否被依赖了多次,是就要单独成包。然后执行打包。
3.3 缓存增量打包数据
增量打包需要记录版本号;然后是每个包的hash,以及它的依赖包。
增量打包也涉及到资源热更新。
if 第一次打包 缓存所有包的hash信息,及其他必要信息 else 后续打包,识别hash是否更改 if hash有更改 单独记录下此包信息 //收集完所有增量信息后,就可以写上传至资源服务器等相关逻辑
3.4 缓存所有依赖信息
缓存AB包信息,缓存的依赖信息在加载时有用
//asset path //bundle name //rootAsset name //hash //依赖项个数 //依赖项