文章目录
前言
本文主要记录笔者学习Unity资源管理的过程。
一、AssetBundle是什么?
1.定义
AssetBundle即资源包,它可以把多个游戏对象或者资源以二进制形式保存到Assetbundle文件中。AssetBundle中的单个资源称为Asset。
Assetbundle支持所有unity可识别的格式:比如预制体、模型、声音、贴图、材质、Shader、场景、lua脚本等。
2.为什么需要AssetBundle
1)将资源打包放到远程服务器上,需要的时候在进行下载。可以减小安装包大小,提升首包的下载速度。
2)可以进行资源和业务逻辑的热更新。
3)可以按需加载和释放 Asset ,减少内存压力。
3.AssetBundle内部格式
AssetBundle 包含了两个部分:数据头以及数据段。数据头内包含了 AssetBundle 的元数据信息,比如它的标识符、压缩类型、manifest 等等。
这里的 manifest 是一个以 Object 名称作为键的查找表,用以指定 AssetBundle 中给定 Object 的位置。数据段内包含了序列化 Asset 生成的原始数据,内容会依据压缩算法变化。
一个AssetBundle本质上是将一些对象组合成一个序列化文件,根据是普通包(normal bundle)还是场景包(scene bundle),Assetbundle的数据文件展开略有不同。
二、AssetBundle打包
1)可以使用Asset Bundle Browser插件进行打包。
2)Unity提供了一套API来实现打包功能。
1.设置资源AB名称
可以在Inspector窗口下手动设置资源的AB名,和变体(Variant)名。
变体的作用参考:
AssetBundle 变体与压缩
Unity 变体探秘
设置AB名称示例代码:
首先清除之前设置的AB名称,避免产生不必要的资源也打包
然后批量设置资源AB名,和变体名。这里是根据资源名称设置AB名。
public static void ClearAssetBundlesName()
{
string[] oldAssetBundleNames = AssetDatabase.GetAllAssetBundleNames();
for (int j = 0; j < oldAssetBundleNames.Length; j++)
{
EditorUtility.DisplayProgressBar("清除AssetName名称", "正在清除AssetName名称中...", 1f * j / oldAssetBundleNames.Length);
AssetDatabase.RemoveAssetBundleName(oldAssetBundleNames[j], true);
}
EditorUtility.ClearProgressBar();
}
public static void SetAssetBundlesName()
{
string fullPath = Application.dataPath + "/GameRes/"; //将Assets/GameRes/文件夹下的所有资源进行打包
if (Directory.Exists(fullPath))
{
EditorUtility.DisplayProgressBar("设置AssetName名称", "正在设置AssetName名称中...", 0f); //显示进程加载条
DirectoryInfo dir = new DirectoryInfo(fullPath); //获取目录信息
FileInfo[] files = dir.GetFiles("*", SearchOption.TopDirectoryOnly); //获取所有的文件信息
for (var i = 0; i < files.Length; ++i)
{
FileInfo fileInfo = files[i];
EditorUtility.DisplayProgressBar("设置AssetName名称", "正在设置AssetName名称中...", 1f * i / files.Length);
if (!fileInfo.Name.EndsWith(".meta")) //判断去除掉扩展名为“.meta”的文件
{
string basePath = "Assets" + fileInfo.FullName.Substring(Application.dataPath.Length); //编辑器下路径Assets/..
string assetName = fileInfo.FullName.Substring(fullPath.Length); //预设的Assetbundle名字,带上一级目录名称
assetName = assetName.Substring(0, assetName.LastIndexOf('.')); //名称要去除扩展名
assetName = assetName.Replace('\\', '/'); //注意此处的斜线一定要改成反斜线,否则不能设置名称
AssetImporter importer = AssetImporter.GetAtPath(basePath);
if (importer && importer.assetBundleName != assetName)
{
importer.assetBundleName = "ui/" + assetName; //资源的AssetBundleName名称
importer.assetBundleVariant = "normal"; //设置资源的变体名称
}
}
}
EditorUtility.ClearProgressBar(); //清除进度条
}
}
2.资源打包
资源打包代码示例:
static void CreateAllAssetBundles()
{
Caching.ClearCache(); //清除AssetBundle缓存
string uiAssetBundlesPath = ClientConfig.GetResPath; //获取需要打包的资源目录
if (!Directory.Exists(uiAssetBundlesPath))
{
Directory.CreateDirectory(uiAssetBundlesPath);
}
//第三个参数BuildTarget参数用来选择针对的平台,因为AB包在不同平台下是不兼容的。
BuildPipeline.BuildAssetBundles(uiAssetBundlesPath, BuildAssetBundleOptions.ChunkBasedCompression, EditorUserBuildSettings.activeBuildTarget);
//清除manifest文件
int value = 1;
string[] strFileName = Directory.GetFiles(uiAssetBundlesPath, "*.manifest", SearchOption.AllDirectories);
foreach (var item in strFileName)
{
File.Delete(item);
EditorUtility.DisplayProgressBar("清除manifest文件", "正在清除manifest文件...", 1f * value / strFileName.Length);
value++;
}
EditorUtility.ClearProgressBar(); //清除进度条
}
注意:如果A资源依赖了B资源,且B没有设置AB名,则B会被打进A所在的包中。
若B被多个资源引用,则B重复打进多个AB包,造成资源冗余。
依赖详情看官方文档
各种ID
序列化后,资源用GUID和Local ID管理。
GUID对应Asset。GUID存在.meta文件中。提供了文件特定位置的抽象。是一种映射。无需关心资源在磁盘上的存放位置。
Local ID对应Asset内的每一个Object。(Asset中)
虽然GUID和Local ID比较好用,但是毕竟因为存在磁盘上,读取比较耗时。因此Unity缓存一个instance ID对应Object,通过instance ID快速找到Object。instance ID是一种快速获取对象实例的ID,包含着对GUID和Local ID的引用。解析instance ID可以快速返回instance表示的已加载对象,如果为加载目标对象,则可以将文件GUID和Local ID解析为对象源数据,从而允许Unity即时加载对象。每次AB包重新加载时,都会为每个对象创建新的instance ID。
3.资源压缩格式
打包的时候支持两种压缩格式:
1)BuildAssetBundleOptions.None 默认LZMA压缩(流压缩)
流压缩在处理整个数据块时使用同一个字典。优点是压缩率高,压出来的包体小。缺点是只支持顺序读取,加载的时候需要一次解压整个包(造成卡顿和额外内存占用)。
2)BuildAssetBundleOptions.ChunkBasedCompression 使用LZ4(块压缩)
将原始数据分成大小相同的子块并单独压缩。包体相对LZMA较大。优点是支持实时解压,随机读取。
推荐使用LZ4压缩。当然如果不在意包体大小的话,也可以选择不压缩。使用BuildAssetBundleOptions.UncompressedAssetBundle ,读取速度最快。
4.分包策略
1)将公共依赖的资源打包到一个公共AssetBundle中,独立的资源打包到一个AssetBundle中。
2)将相同类型的资源(Shader、Atlas、Prefab、Material、Scene、动画、声音、特效等)打包成一个AssetBundle。
3)地图、Monster、Npc、角色、坐骑、动作、模型等根据配置进行打包 。
4)经常更新的资源打包到一个AssetBundle中。
5)按照生命周期,同一个UI界面的资源打包到一个AssetBundle中。
打包时需要注意AB包整体数量和单个AB包的大小。
按照有经验的前辈的说法,一个AB包的大小控制在1~2M左右比较合适。
AB包数量较多,包内资源较少 | AB包数量较少,包内资源较多 |
---|---|
加载一个AB包到内存的时间短,玩家不会有卡顿感,但每个资源实际上加载时间变长。 | 加载一个AB包到内存的时间较长,玩家会有卡顿感,但之后包内的每个资源加载很快。 |
热更新灵活,要更新下载的包体较小。 | 热更新不灵活,要更新下载的包体较大。 |
IO次数过多,增大了硬件设备耗能和发热压力。 | IO次数不多,硬件压力小。 |
5.避免资源冗余
引用自:鹅厂程序小哥 Unity用户手册-AssetBundle
建立三个对应关系的字典:
Dictionary<string, Bundle> m_BundleDic = new Dictionary<string, Bundle>();
Dictionary<string, Asset> m_AssetDic = new Dictionary<string, Asset>();
Dictionary<string, string> m_AssetMapBundleDic = new Dictionary<string, string>();
m_BundleDic:维护了BundleName-Bundle对象对应关系的字典,其中Bundle对象中,维护了当前Bundle中所有的资源列表AssetList。
m_AssetDic:维护了AssetName-Asset对象对应关系的字典
m_AssetMapBundleDic:维护了AssetName-BundleName对应关系的字典
具体操作步骤:
1.创建一个HashSet,确保所有的资源Asset在Bundle中是唯一的,不产生冗余。
2.遍历m_BundleDic中每一个Bundle对象的AssetList;
1)如果不在HashSet中,把Asset加入到HashSet中,否则,执行删除冗余操作;
2)如果HashSet已经存在相同的Asset时,通过m_AssetMapBundleDic字典获得当前Asset对应的BundleName;
3. 如果当前Asset对应的BundleName在m_BundleDic中,表示多个ab包依赖了这个Asset,从对应的Bundle的AssetList中移除这个Asset。
4.如果当前Asset对应的BundleName不在m_BundleDic中,表示虽然多个ab包依赖了这个Asset,但是其中有一些并不需要打到ab包中,这时,需要把对应的Bundle从m_AssetMapBundleDic中移除。
5.代码实现:TODO
6.AB包加密
TODO
三、AssetBundle资源加载
1.各平台资源文件目录介绍
1)Resources:
Unity编辑器下目录
全部资源都会被压缩,转化成二进制。打包后该路径不存在,不可写也不可读。
只能使用Resources.Load加载资源。
2) Application.dataPath:
在Unity下对应为:/Assets
在iOS下对应为:/var/containers/Bundle/Application/appsandbox/xxx.app/Data 此目录是只读的。
在Android下对应为:/data/app/package name-1/base.apk APK程序包路径。
3)Application.StreamingAssets:
在Unity下对应为:/Assets/StreamingAssets,该目录下全部资源原封不动打包。
在Android下对应为:jar:file:///data/app/xxx.apk!/assets。使用"Application.streamingAssetsPath"进行访问。
在iOS下对应为:Application/…/xxx.app/Data/Raw。使用"file://{Application.streamingAssetsPath}“进行访问
在移动平台下,是只读的,不能写入数据,其他平台下可以使用System.File类进行读写。
在任意平台都可以使用AssetBundle.LoadFromFile来从此文件夹读取加载AB包。
4)Application.persistentDataPath:
对应的是应用持久化数据存储文件夹路径。应用更新、覆盖安装时,这里的数据都不会被清除。(可读可写)
在Unity下对应为:/该Unity项目文件夹路径。
在Android下对应为:/…/data/应用名/files。
在iOS下对应为:Application/…/Documents。
我们一般将下载的AssetBundle存放于此。移动平台可以使用”{Application.persistentDataPath}/{Application.productName}/"进行访问,非移动平台直接使用Application.persistentDataPath即可访问。
5)Application.temporaryCachePath:
iOS会自动将persistentDataPath路径下的文件上传到iCloud,会占用用户的iCloud空间。如果persistentDataPath路径下的文件过多,苹果审核可能被拒,所以,iOS平台,有些数据得放temporaryCachePath路径下。
2.加载、卸载资源API
加载资源的时候必须先加载它依赖的资源,否则会出现引用丢失异常。
可以通过总manifest文件获取AB包的依赖关系,预加载依赖的AB包。
加载AB的API:
AssetBundle.LoadFromFile(path):同步加载,path为本地路径
AssetBundle.LoadFromFileAsync(path):异步加载
AssetBundle.LoadFromMemory(byte[] binary):从字节数组加载,binary为目标ab二进制流
AssetBundle.LoadFromMemoryAsync(byte[] binary):异步加载
AssetBundle.LoadFromStream(Stream stream):从流中加载AB包
AssetBundle.LoadFromStreamAsync(Stream stream):异步加载
WWW.assetBundle:www加载(已过时)
UnityWebRequest.GetAssetBundle(string uri):url为ab文件路径,可为本地,也可为云端,
从AB中加载具体的Asset的API:
assetBundle.LoadAsset<T>(name):T为目标资产类型,name为资产名称,会返回一个T实例
assetBundle.LoadAssetAsync<T>(string name):异步加载
assetBundle.LoadAsset(name,type):name为资产名,type为资产类型
assetBundle.LoadAssetAsync(name,type):异步加载
assetBundle.LoadAllAssets<T>():T为目标资产类型,会返回一个assetBundle中所有T类型资产数组
assetBundle.LoadAllAssets():加载assetBundle中所有资产,返回一个assetBundle中所有资产数组(object)
卸载AB和Asset的API:
Resources.UnloadAsset(obj) : 释放指定资源
Resources.UnloadUnusedAssets():再切场景或者内存峰值的时候调用,卸载没有被引用的资源。
assetBundle.Unload(false):释放AB自己,不会影响已经加载的资源。
assetBundle.Unload(true):不但会释放自己,还会释放所有从AB加载的资源。
AssetBundle.UnloadAllAssetBundles(bool unloadAllObjects):卸载所有资源,消耗较大,不建议调用。
注意:assetbundle.Unload(bool unloadAllLoadedObjects);
unloadAlLoadedlObjects:表示是否卸载所有加载的资源。
参数为false时,AssetBundle内的文件内存镜像会被释放,实例化的物体还都保持完好。简单的说就是断开了AssetBundle内存镜像和实例之间的联系。
如果再次实例化对象,也不会返回以前初例化过的AssetBundle内存镜像,而是重新实例化一个新的AssetBundle内存镜像,那么这样就出现了冗余,同样的资源,内存中会出现多份。
参数为true时,就简单多了,卸载AssetBundle,并且删除被引用的资源。如果这时AssetBundle中有资源在场景中被引用,则会出现资源丢失的情况。这种卸载方式,最为彻底,完全从内存移除,缺点是你需要一套机制(目前流行的是引用计数),来关注是不是还有资源引用,会不会引起异常。
AssetBundle.UnloadAllAssetBundles(bool unloadAllObjects):unloadAllObjects:是否卸载所有加载的资源,如果为true,则会卸载所有资源,包括正在使用着(被依赖)的资源。如果为false,则会卸载未被依赖的资源,被其他资源依赖的资源不会被卸载。
3.内存分析
分析资源在内存中的生命周期。先贴出两张很经典的图。
可以看到,AB包先要从硬盘或者网络中加载并解压到内存中,产生AssetBundle文件内存镜像(紫色部分)。
通过assetBundle.Load将单个资源加载到内存中(虚线框绿色部分)。
有的资源通过复制,需要实例化(GameObject),有的资源通过引用(shader等)。
GameObject资源通过Destroy释放。
Resources.UnloadAsset(obj) , 释放绿色区域指定资源 。
Resources.UnloadUnusedAssets(),释放绿色区域没有被引用的资源。
调用assetbundle.Unload(false)会卸载AssetBundle文件内存镜像镜像,其加载的Asset不会被释放。
调用assetbundle.Unload(true)会卸载AssetBundle文件内存镜像镜像,其加载的Asset也会被释放。容易造成引用丢失(场景中出现粉红色)。
总结:
1)对于加载完后不再需要主动和被动依赖加载的资源,在加载完成后调用AssetBundel.Unload(false),立即释放掉AssetBundle资源,当资源使用完毕,再用Resources.UnloadAsset()或Resources.UnloadUnusedAssets()释放资源内存。
2)Resources.UnloadUnusedAssets()有较大消耗,尽量减少调用次数。
3)不能调用AssetBundle.Unload(false)后再调用AssetBundle.Unload(true).
4)不再使用的GameObject直接Destroy即可。
5)对于shader、通用图集等常驻且需要保留依赖关系的资源,在合适时机加载进来,不调用AssetBundle.Unload()接口。
6)对于界面等存在明确生命周期,又可能动态加载的资源,在生命周期结束后调用AssetBundle.Unload(true),将全部资源一起释放。
7)AssetBundle.Unload(true)在使用中,最好的做法是给创建出来的实例都添加计数,当计数不为0时,表示场景或代码中仍有引用,而当计数为0时,表示没有引用了,这样就可以放心大胆的AssetBundle.Unload(true)了。
具体内存分析可参考:Unity5-ABSystem(五):AssetBundle内存
总结
本文只是粗糙的记录了笔者在学习Unity资源管理过程中遇到的一些知识点,具体每一个知识点有待深剖。
如有错误,希望大家在评论区指出,一起学习进步。
参考链接:
Unity官方文档
王江荣-Unity-Asset简介
Unity5-ABSystem AssetBundle原理
UWA-你应该知道的AssetBundle管理机制
烟雨-AssetBundle热更新完整工作流与知识点解析
Unity——浅谈AB包(AssetBundle)
林新发-3种资源加载方式