unity的资源管理
在Unity中,一般来说,资源加载方式主要分为Resources加载和AssetBundle加载。
Resources是Unity的一个特殊文件夹,放在这个文件夹下的资源可以通过Resources.Load()来直接加载。即Resources加载资源方式。
AssetBundle是一种Unity提供的用于存放资源的包。通过将资源分布在不同的AB包中可以最大程度地减少运行时的内存压力,并且可以有选择地加载内容。
AssetBundle
AB包
官方文档
AB包的定义
- AssetBundle(简称AB包)是一个资源压缩包,把一些资源文件,场景文件或二进制文件以某种紧密的方式保存在一起的,独立于游戏主包存在的资源存储文件,使用内部资源时,需要单独下载和加载;可以在游戏运行的时候被加载。
- AssetBundle是Unity提供的一种资源更新技术,就是通过AssetBundle更新资源,也可以通过把脚本或者其他代码当成资源打成AssetBundle然后更新到客户端。
- AssetBundle自身保存着互相的依赖关系。
- 压缩包可以使用LZMA和LZ4压缩算法,减少包大小,更快的进行网络传输。
- 把一些可以下载内容放在AssetBundle里面,可以减少安装包的大小。
- AssetBundle内部不能包含C#脚本文件,AssetBundle可以配合Lua实现资源和游戏逻辑代码的更新
AB包的打包与加载
打包
-
将需要导入AB包的资源进行导入,不要将导入AB包的资源放置在Resources目录等特殊目录下,因为存储在Resources下的资源,最终会存储在游戏的主体包中,发送给用户,手机系统上,如果需要做资源的更新,是无法使用Resources即时更新。
-
资源配置:
选中需要添加AB包的资源,填写或选择已存在的AB包名称
AB包名称如果配置为这样的结构”ui/package”,ui会作为AB包存储的父目录,package是AB包的名称
AB包配置修改后或AB内部的资源修改后,都需要重新生成AB包
打包:相关方法:Buildpipeline.BuildAssetBundles(ab包文件存储路径,导出选项,导出平台(不同平台的ab包是不一样的))
例:
public class ExportAB
{
[MenuItem("工具/导出AB包")]
private static void Export()
{
string tempPath = Application.dataPath;
string AbPath = tempPath.Substring(0, tempPath.Length - 6) + "Ab";
//判断是否存在路径
if (!Directory.Exists(AbPath))//如果不存在路径
{
Directory.CreateDirectory(AbPath);//创建路径
}
#region 导出ab包核心代码
// BuildPipeline:通过编程方式构建AB包。
//参数一:ab包文件存储路径
//参数二:导出选项
//参数三:导出平台(不同平台的ab包是不一样的)
//制作多平台导出ab包
#if UNITY_EDITOR_WIN||UNITY_STANDALONE_WIN
BuildPipeline.BuildAssetBundles(AbPath, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
#elif UNITY_ANDROID
BuildPipeline.BuildAssetBundles(AbPath, BuildAssetBundleOptions.None, BuildTarget.Android);
#elif UNITY_IPHONE//苹果机
BuildPipeline.BuildAssetBundles(AbPath, BuildAssetBundleOptions.None, BuildTarget.iOS);
#endif
#endregion
Debug.Log("AB包打包成功!");
}
}
导出选项:
- None:无任何选项,默认压缩模式为LZMA
- UncompressedAssetBundle:开启这个选项就不会压缩AB包
- CollectDependencies:默认开启,开启依赖记录机制
- DeterministicAssetBundle:将AssetBundle的哈希校验值,存储在ID中(默认开启)
- ForceRebuildAssetBundle:强制重新导出所有的AB包
- ChunkBasedCompression:使用LZ4算法压缩AB包
压缩方式
- 不压缩:AB包比较大,下载较慢,加载速度快(CPU不用运算解压缩)
- LZMA算法压缩:默认压缩模式,流压缩方式(stream-based),文件尺寸居中,加载速度居中;使用LZMA算法压缩,压缩的包更小,但是加载时间更长,只支持顺序读取。使用之前需要整体解压。一旦被解压,这个包会使用LZ4重新压缩。使用资源的时候不需要整体解压。在下载的时候可以使用LZMA算法,一旦它被下载了之后,它会使用LZ4算法保存到本地上。
- LZ4算法压缩:5.3以后可用,块压缩方式(chunk-based)压缩比例高,文件小,加载速度偏慢;使用LZ4压缩,压缩率没有LZMA高,但是我们可以加载指定资源而不用解压全部,可以实现实时解压随机存储。
注意:使用LZ4压缩,可以获得跟不压缩想媲美的加载速度,而且比不压缩文件要小。
导出后的文件结构:
和存储目录同名的文件和文件.manifest,是主AB包及主AB包配置文件(存储所有AB包的配置信息)
无扩展名文件:AB包 文件名.manifest文件:AB包对应的配置文件
加载
依赖关系
在Unity5.0后,BuildAssetBundleOptions.CollectDependencies永久开启,即Unity会自动检测物体引用的资源并且一并打包,防止资源丢失遗漏的问题出现。
如果一个AB(名称为ui)包,使用到了另一个AB(名称为big)包的资源,那么两个AB包就产生了依赖关系。也就是ui依赖于big。如果ui加载了,big没有加载,就会导致ui中的资源出现资源缺少的问题。
AB包的依赖关系存储在各AB包所对应的配置文件中(在主AB包的配置文件中存储着所有AB包的依赖关系)
AB包不能重复加载(需要对AB包进行卸载操作“Unload()”);当AB包使用完后需要卸载AB包;否则会妨碍其它AB包加载 报错如下:
如果想处理依赖关系的加载,则必须加载主AB包(与目录名相同),因为依赖关系的存储,都存储在主AB包的配置文件中
- 第一步(加载被依赖的AB包文件): 当所需AB包文件有被依赖的AB包时
- 加载主AB包
AB包 = AssetBundle.LoadFromFile(AB包文件路径) - 根据主AB包的配置文件,加载配置文件:
AssetBundleManiifest 配置文件类 = LoadAsset("AssetBundleManiifest"); - 获得当前需要加载的AB包所依赖的AB包:
string[] dependencies = mainManifest.GetAllDependencies("需要加载的AB包名称");
//dependencies中存储的为所依赖的AB包的文件名 - 将所有的被依赖的AB包,加载进来(为加载的AB包提供依赖的资源)
- 加载主AB包
- 第二步(加载所需AB包文件)
- AB包 = AssetBundle.LoadFromFile(AB包文件路径)
- AssetBundle.LoadFromFileSync(AB包文件路径) //异步加载
- 第三步(加载所需AB包内部资源)
- 资源对象 = AB包对象.LoadAsset<资源类型>(“资源名称”)
- AB包对象.LoadAssetSync<资源类型>(“资源名称”) //异步加载
- 第四步 (卸载AB包文件)
- Unload(false); //不删除已经加载出的资源
- Unload(true);//会将加载出的资源也进行删除
具体加载代码:
/// <summary>
/// 简单加载ab包资源
/// </summary>
public class SimpleLoad : MonoBehaviour
{
private void Start()
{
//步骤一:从文件中加载ab包
AssetBundle assetBundle = AssetBundle.LoadFromFile(AB包的文件路径);//从文件中加载ab包
//步骤二:通过资源名称加载资源
Sprite UISprite = assetBundle.LoadAsset<Sprite(加载资源的类型)>(加载资源的名称);
GameObject.Find("Canvas/Image").GetComponent<Image>().sprite = UISprite;
assetBundle.Unload(false);//资源卸载
}
}
/// <summary>
/// 加载有依赖关系的AB包资源
/// </summary>
public class DependencyLoad : MonoBehaviour
{
public Transform canvasTF;
private AssetBundle dependencieAb;
private void Start()
{
DependencyLoadAB();
}
private void DependencyLoadAB()
{
//第一步:加载被依赖的ab包
//1.加载主ab包
AssetBundle mainAb = AssetBundle.LoadFromFile(主AB包路径);
//2.从Ab包中获取(加载)配置文件
AssetBundleManifest mainManifest = mainAb.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
//3.加载出被依赖的ab包文件
string[] dependencies = mainManifest.GetAllDependencies(需要加载的AB包名称);
//语句含义:查找所需ab包的依赖ab包文件;参数:所需要的ab包文件,返回值:所有被依赖的AB包文件名称所组成的数组
for (int i = 0; i < dependencies.Length; i++)
{
dependencieAb = AssetBundle.LoadFromFile(Config.AbPath + "/" + dependencies[i]);//参数AB包的文件路径
}
//第二步:加载所需要的ab包
AssetBundle needAB = AssetBundle.LoadFromFile(所需要的ab包的路径);
//第三步:加载所需ab包中的文件
GameObject needABGO = needAB.LoadAsset<GameObject(加载资源的类型)>(加载资源的名称);
Instantiate(needABGO).transform.SetParent(canvasTF);
//卸载ab包释放ab包
dependencieAb.Unload(false);
needAB.Unload(false);
}
}
/// <summary>
/// 异步加载AB包资源
/// </summary>
public class AsyncLoad : MonoBehaviour
{
private GameObject playerGO;
public Image image;
void Start()
{
//StartCoroutine(AsynLoadResources());
StartCoroutine(AsynLoadABResources());
}
/// <summary>
/// 异步加载Resources资源
/// </summary>
/// <returns></returns>
private IEnumerator AsynLoadResources()
{
//使用一个异步加载
ResourceRequest GoResourceRequest = Resources.LoadAsync<GameObject>("Player");
Debug.Log(Time.time);
//协同程序会在资源加载成功后,继续执行接下来的代码(底层封装了线程加载资源)
yield return GoResourceRequest;
Debug.Log(Time.time);
playerGO = GoResourceRequest.asset as GameObject;
Instantiate(playerGO);
}
/// <summary>
/// 异步加载AB包资源
/// </summary>
/// <returns></returns>
private IEnumerator AsynLoadABResources()
{
//异步加载ab包
AssetBundleCreateRequest assetBundleCreate = AssetBundle.LoadFromFileAsync(加载ab包文件路径);
yield return assetBundleCreate;
//加载ab包中的资源
AssetBundle assetBundle = assetBundleCreate.assetBundle;
image.sprite = assetBundle.LoadAsset<Sprite(加载资源的类型)>(加载资源的名称);
assetBundle.Unload(false);
}
}
内存占用
对于GameObject来说,通常情况下需要对其进行改动,所以它是完全复制一份该资源来进行的实例化。也就是说,当AB包中的GameObject从内存中卸载后,实例化的GameObject不会因此丢失。并且对实例化对象的修改不会影响到GameObject资源。
对于Shader和Texture来说,通常情况下不需要对其进行改动,所以它是通过引用来进行的实例化。也就是说,当AB包中的Shader和Texture资源从内存中卸载后,实例化的Shader和Texture会出现资源丢失的情况。并且对实例化对象的修改会影响到Shader和Texture资源。
对于Material和Mesh来说,有时候可能需要对其进行改动,所以它是通过引用+复制来进行的实例化。也就是说,当AB包中的Material和Mesh资源从内存中卸载后,实例化的Material和Mesh会出现资源丢失的情况。并且对实例化对象的修改不会影响到Material和Mesh资源。
卸载
一些ID Instance ID / GUID / Local ID(file ID) 参考链接
GUID:存在于unity中资源对应的.meta文件中,通过GUID就可以找到工程中的文件;表示这个文件,用来记录资源之间的关系。
Local ID(file ID):资源内部的ID,用来记录资源之间的具体引用。
Instance ID:资源的快捷访问ID,unity查找资源的时候会根据guid和local id与instance id的映射关系,直接使用instance id去查找,可以减少查找开销
卸载操作
当调用Resources.UnloadAsset()时,虽Object被销毁,但Instance ID被保留且包含有效的GUID和Local ID引用。
当调用AssetBundle.Unload(true)时,卸载从AssetBundle加载的所有游戏对象(及其依赖项)但是不包括已经实例化(复制)的对象,因为它们不再属于AssetBundle;从AssetBundle中加载的纹理(仍然属于AssetBundle)会从场景中的游戏资源消失,会被是为缺少材质。而且Instance ID的GUID和Local ID引用变无效。
当调用AssetBundle.Unload(false)时,虽Object不被销毁,但Instance ID的GUID和Local ID引用变无效。场景中的物体会与该AB包分离链接。即该物体的instance ID引用的GUID和Local ID会断开引用,无法再通过该instance ID找到GUID和Local ID。
假设材质 M 是从 AssetBundle AB 加载的,如下所示。
如果调用 AB.Unload(true),活动场景中的任何 M 实例也将被卸载并销毁。
如果改作调用 AB.Unload(false),那么将会中断 M 和 AB 当前实例的链接关系。
如果稍后再次加载 AB 并且调用 AB.LoadAsset(),则 Unity 不会将现有 M 副本重新链接到新加载的材质。而是将加载 M 的两个副本。
如果再次加载该AB包时,分离了链接的物体不会受该新加载的AB包管理。因此如果不注意的话可能会导致一些不可控的问题。Unity中有Resources.UnloadUnusedAssets()方法可以很好地解决这个问题。