Unity的资源管理是一个比较复杂的模块,如果管理不好,可能导致最终包体大小偏大,程序运行时候内存居高不下,因此了解并掌握Unity的资源管理显得特别重要。
Unity中资源一般存放在两个目录下,一个是Resource目录,另一个是StreamingAsset目录。放在Resource目录下的资源在打包的时候会被压缩并打包到安装包中(assets资源),只读,而在StreamingAsset目录下的资源在打包的时候不会被压缩但会被打包到安装包中(assetbundle资源),安装时候会被解压到相应平台的对应目录(通过Application.streamingAssetsPath可获得解压路径)下,只读。因此,可以根据Unity的资源格式,把Unity资源管理分为两种,一种是Resource模式,另外一种是AssetBundle模式。
Untiy官方推荐使用AssetBundle模式对资源进行管理,接下来通过三方面来了解AssetBundle的运行机制。
1.AssetBundle的内部结构:
我们说AssetBundle的时候一般是指两个东西,一个是指存储在磁盘上的资源,它由两部分组成:序列化文件和资源文件,序列化文件是由该资源集合下的资源文件拆分写入到一个文件得到,资源文件是可以高效加载的二进制存储的资源(如贴图、音效);另一个是指通过代码加载到内存的对象,它包含一张拥有你打包进来的全部资源的地址和当某个资源被加载时需要依赖加载的资源的Map(如加载模型时依赖的贴图和网格路径)。
普通的AssetBundle内部结构如下图:
场景AssetBundle内部结构如下图:
由于Unity并不是开源引擎,而且官方也未给出太多有关AssetBundle的内部实现细节,因此我们并无法知道关于AssetBundle具体的格式,不过云风早前对其内部格式有过一次探究,具体可参考该文章。
2.AssetBundle的打包流程:
AssetBundle打包涉及到一个打包策略的问题,打包策略会直接影响到打包后资源的数量和大小,从而影响到资源加载的效率(IO、解压、申请内存)和热更资源包的大小。因此我们需要对包的大小和数量做一个平衡,根据项目具体情况整理出一个合适的打包策略。常用的打包策略有如下几种:
在实际项目中,可以将上述几种策略交互使用,对应具体的应用需求来灵活的采用分组策略。
Unity5.0之前版本,Unity通过编译管线BuildPipeline提供了三个方法来创建AssetBundle文件:
// 将任意类型的Assets打包成一个AssetBundle,适用于对单个大规模场景的细分
// 第一个是主资源,第二个是资源数组,这两个参数必须有一个不为null,如果主资源存在于资源数组中,是没有任何关系的,如果设置了主资源,
// 可以通过Bundle.mainAsset来直接使用它;
// 第三个参数是生成路径,一般我们设置为 Application.streamingAssetsPath + Bundle的目标路径和Bundle名称
// 第四个参数是打包选项,有六个选项,分别是
// BuildAssetBundleOptions.None:采用LZMA的压缩格式,这种压缩格式要求资源在使用之前需要全部被解压;
// BuildAssetBundleOptions.CompleteAssets :强制包含整个资源,使每个Asset自身完备,包含所有的Components;
// BuildAssetBundleOptions.CollectDependencies:包含每个Asset依赖的所有其他Asset;
// BuildAssetBundleOptions.DisableWriteTypeTree:在AssetBundle中不包含类型信息;
// 需要注意的是,如果将AssetBundle发布到web平台上,则不能使用这个选项;
// BuildAssetBundleOptions.DeterministricAssetBundle:使每个Object具有唯一的、不变的HashID,便于后续查找可以用于增量发布
// AssetBundle,在打包依赖时会有用到,其他选项没什么意义;
// BuildAssetBundleOptions.UncompressedAssetBundle:不进行数据压缩,如果使用这个选项,因为没有压缩/解压的过程,
// AssetBundle发布和加载会更快,但是AssetBundle也会更大,导致下载变慢。
// 第五个参数是平台,在不同平台下我们需要传入不同的平台标识,以打出不同平台适用的包。注意,Windows平台下打出来的包,不能用于iOS。
// 常用参数有:BuildTarget.StandaloneWindows|BuildTarget.iOS|BuildTarget.Android
public static bool BuildPipeline.BuildAssetBundle(Object mainAsset, Object[] assets, string pathName,
BuildAssetBundleOptions options, BuildTarget targetPlatform);
// 将一个或多个场景中的资源及其所有依赖以流加载的方式打包成AssetBundle,一般适用于多单个或多个场景进行集中打包
// 第一个参数是场景资源数组;
// 第二个参数是生成路径;
// 第三个参数是平台,在不同平台下我们需要传入不同的平台标识,以打出不同平台适用的包。注意,Windows平台下打出来的包,不能用于iOS。
// 常用参数有:BuildTarget.StandaloneWindows|BuildTarget.iOS|BuildTarget.Android
// 第四个参数有14个选项,通过参数选取可以设置打包开发版本场景或者允许远程附加Debug脚本场景等,正式场景一般选择BuildOptions.None。
public static string BuildPipeline.BuildStreamedSceneAssetBundle(string[] levels, string locationPath,
BuildTarget target, BuildOptions options);
// 功能与BuildAssetBundle相同,但创建的时候可以为每个Object指定一个自定义的名字。(一般不太常用)
public static bool BuildPipeline.BuildAssetBundleExplicitAssetNames(Object[] assets, string[] assetNames,
string pathName, BuildAssetBundleOptions options, BuildTarget targetPlatform);
AssetBundle的依赖打包,Unity5.0之前版本提供了两个函数来实现,具体方法是像操作堆栈一样,先入栈,然后创建AssetBundle,再出栈,后压入栈中的元素依赖栈内的元素。出栈和入栈操作要一一对应。
public static void BuildPipeline.PushAssetDependencies();
public static void BuildPipeline.PopAssetDependencies();
AssetBundle资源间的依赖是通过Build时构建的栈来决定的,所以当B依赖A,而B需要更新的时候,A也要重新打一次,而A需要更新时,只需要重新打A即可,无需重打B,打包时必须开启DeterministicAssetBundle。
举例说明:
var options = BuildAssetBundleOptions.CollectDependencies
| BuildAssetBundleOptions.CompleteAssets;
// 入栈-共享资源
BuildPipeline.PushAssetDependencies();
// 先打被依赖资源
BuildPipeline.BuildAssetBundle(
AssetDataBase.LoadMainAssetAtPath("Assets/shared.png"),
null, "shared.ab", options);
// 入栈-目标资源A
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(
AssetDataBase.LoadMainAssetAtPath("Assets/A.fbx"),
null, "A.ab", options);
// 出栈-目标资源A
BuildPipeline.PopAssetDependencies();
// 入栈-目标资源
BuildPipeline.PushAssetDependencies();
BuildPipeline.BuildAssetBundle(
AssetDataBase.LoadMainAssetAtPath("Assets/B.fbx"),
null, "B.ab", options);
// 出栈-目标资源B
BuildPipeline.PopAssetDependencies();
// 出栈-共享资源
BuildPipeline.PopAssetDependencies();
所以在Unity5.0之前版本,项目基本上都会有一个打包策略配置,该策略中配置了哪些文件打包成一个AssetBundle文件,哪些文件执行依赖打包,然后执行打包的时候会根据策略配置,先判断文件间的依赖关系,将同一AssetBundle文件的统一打包,同时需要生成记录并维护AssetBundle之间的依赖关系。在游戏加载资源的时候,判断该资源是否有依赖其他资源,如果有需要先加载依赖资源。
Unity5.0之后版本中,Unity对AssetBundle的打包做了较大的改动,废弃了4.x那套复杂的创建AssetBundle文件方法,只提供一个精简后的创建AssetBundle文件方法。
// 第一个参数是生成路径,引擎自动根据资源的assetbundleName属性批量打包,自动建立Bundle以及资源之间的依赖关系。
// 第二个参数对于旧方法的第六个参数,新增了一些打包选项,且一些4.x中的旧有策略被默认开启:
// BuildAssetBundleOptions.CompleteAssets|BuildAssetBundleOptions.CollectDependencies|BuildAssetBundleOptions.
// DeterministicAssetBundle:和旧版本一样,不过默认开启;
// BuildAssetBundleOptions.ForceRebuildAssetBundle:用于强制重打所有AssetBundle文件,新增;
// BuildAssetBundleOptions.IgnoreTypeTreeChanges:用于判断AssetBundle更新时,是否忽略TypeTree的变化,新增;
// BuildAssetBundleOptions.AppendHashToAssetBundleName:用于将Hash值添加在AssetBundle文件名之后,开启这个选项可以直接
// 通过文件名来判断哪些Bundle的内容进行了更新(4.x下普遍需要通过比较二进制等方法来判断,但在某些情况下即使内容不变重新打包,
// Bundle的二进制也会变化),新增;
// BuildAssetBundleOptions.ChunkBasedCompression:用于使用LZ4格式进行压缩,5.3新增。
// 第三个参数是生成平台,和老方法一样。
public static AssetBundleManifest BuildPipeline.BuildAssetBundles(string outputPath,
BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);
Unity5.x在资源的Inpector界面最下方新增一个设置AssetBundle名字的选项(名字必须小写,否则Unity会自动处理成小写),相同的AssetBundleName会被打包到一个AssetBundle中。同时也可以设置Variants,在打包时Variants会作为后缀添加在AssetBundleName后面。Variants的作用在于:可以让一个资源能够基于版本、标准分辨率、语言、定位、或用户偏好等选择映射到不同的AssetBundle。因此相同AssetBundleName,不同Variants的Assetbundle是可以相互替换的。Unity5.x也允许开发者通过代码来设置AssetBundle的名字,具体设置方法可以参考这篇文章。
Unity5.x还新增了Manifest文件,每创建一个AssetBundle文件便会生成一个对应的.manifest文件,这个文件记录了版本,CRC校验码,Hash码,ClassTypes,包含的资源及其路径,依赖关系等信息。同时,在生成路径的目录内还会生成一个总的(根)manifest文件和AssetBundle文件,以该文件夹命名:[文件夹名].manifest,它包含了版本号,该文件夹内所有的AssetBundle的信息,以及它们之间互相依赖的信息。根据这些manifest文件,Unity5.x可以从根manifest文件开始轻易找到各AssetBundle之间的依赖关系,可用于依赖打包并供运行时加载依赖使用,从而实现增量更新(即B依赖A,B或A更新的时候A或B不在需要跟着被打包,只需要修改相应的manifest文件),所以Unity5.x不在需要开发人员自己再去记录维护AssetBundle之间的依赖关系了。对了,Unity5.x还根据manifest文件判断文件是否有修改,如果删除了AssetBundle而没有删除manifest并且资源文件没有修改,那有可能导致AssetBundle不再生成,踩过这个坑。
//AssetBundle.manifest
ManifestFileVersion: 0
AssetBundleManifest:
AssetBundleInfo:
Info_0:
Name: obj1.ab
Dependencies: {}
//obj1.ab.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/Resource/Obj/obj1.prefab
Dependencies: {}
同理,在我们加载AssetBundle的时候,只需要先把总的(根)manifest文件对应的AssetBundle加载进来,以确认各个子AssetBundle之间的依赖关系,提前加载被依赖的AssetBundle即可。
AssetBundle assetBundle = AssetBundle.LoadFromFile("manifestFilePath");
// 通过manifest文件获取依赖关系
AssetBundleManifest manifest = assetBundle.LoadAsset("AssetBundleManifest");
foreach (string assetBundleName in manifest.GetAllAssetBundles())
{
print(assetBundleName);
}
string [] dependencies = manifest.GetAllDependencies("obj1.ab");
foreach (var dependencyName in dependencies)
{
AssetBundle.LoadFromFile("AssetBundles/"+dependencyName);
}
在这里提一下打包AssetBundle时候的压缩类型,Untiy提供了2种类型的压缩算法:LZMA或LZ4(Untiy5.x新增)。LZMA是一种基于序列化流文件的压缩算法,是Untiy最早提供的压缩算法,也是打包AssetBundle时默认的压缩算法,该算法打出的AssetBundle包体积最小(高压缩比),但是使用的时候需要先解压,会增加解压缩时的时间,而且只能顺序读取。LZ4是基于块的压缩算法,资源数据被切割成相等大小的块(chunks),各块之间独立压缩,它的压缩比没有LZMA算法高,但是它实时解压快,随机读取开销很小,因此解压缩的时间相对要短。当然我们也可以选择不压缩AssetBundle,这样包体积最大,但是访问速度最快。这个也是要根据项目具体情况选择合适的压缩方式。
3.AssetBundle的加载卸载:
可以通过下图先对AssetBundle的加载卸载方法做个大概的了解:
根据上图我们可以知道,Unity对两种资源管理模式使用了两种不同的加载方式,对于Resource模式,直接使用Resource.Load(path) 加载如内存中得到Asset资源;对于AssetBundle模式,加载AssetBundle的可以分为两种类型:
同样通过上图我们可以知道,Unity4.x和Unity5.x(图中红色方法标注)对于资源的加载和释的放差异不是很大,Unity5.x修改了几个加载接口的名字,使它更加的容易辨别,同时新加了两个异步接口,使功能更加完备。下面分别对Unity提供的相同加载类型的不同接口之间的差异做个对比。
直接获取AssetBundle对象:Untiy4.x提供了三个常用接口来获取AssetBundle对象。
// 将磁盘上的未压缩的AssetBundle文件读入内存,同步创建内存中的AssetBundle对象,方式最快,完成后只会在内存中创建较小的
// SerializeFile,后续的AssetBundle.Load需要通过I/O从磁盘读取。
public static AssetBundle AssetBundle.CreateFromFile(string path);
// 将内存中的AssetBundle二进制文件流,异步创建内存中的AssetBundle对象,完成后在内存中创建较大的WebStream,
// 该方法一般用在加密的数据上,经过加密的数据流经过解密后通过该方法创建AssetBundle对象。
public static AssetBundleCreateRequest AssetBundle.CreateFromMemory(byte[] binary);
// 该方法是CreateFromMemory的同步版本
public static AssetBundle AssetBundle.CreateFromMemoryImmediate(byte[] binary);
Unity5.x对以上接口名字做了修改,分别与上面接口一一对应:
同时增加了一个LoadFromFile的异步版本:
稍微有点不同的是:LoadFromFile 可以直接加载未压缩或者使用Unity提供的两种压缩格式压缩的AssetBundle文件,针对使用默认的LZMA压缩格式压缩的AssetBundle文件,该方法会先将AssetBundle文件解压后再加载,而LZ4格式的数据则会保持其压缩的状态。
先获取WWW对象,再通过WWW.assetBundle获取AssetBundle对象:Untiy4.x提供了两个常用接口来获取WWW对象。
Unity5.x新增了一个接口,用于取代LoadFromCacheOrDownload:
注意(参考UWA):
得到AssetBundle之后,Unity4.x提供了3个常用接口来获取Asset对象:
Unity5.x对以上接口名字做了修改,分别与上面接口一一对应:
同时新增了一个LoadAllAssets的异步版本:
最后说下AssetBundle的卸载,Unity提供了以下几个卸载接口:
参考:
你应该知道的AssetBundle管理机制(UWA)
揭开Unity4.x AssetBundle庐山真面目(一)
Unity资源打包学习笔记(一)、详解AssetBundle的流程
再详细的介绍一下Unity5的AssetBundle(关于依赖打包图解)
Unity 5.x AssetBundle零冗余解决方案
Unity官方手册AssetBundle管理部分译文