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即时更新。

  • 资源配置:

    alt

选中需要添加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包打包成功!");
    }
}

导出选项:

  1. None:无任何选项,默认压缩模式为LZMA
  2. UncompressedAssetBundle:开启这个选项就不会压缩AB包
  3. CollectDependencies:默认开启,开启依赖记录机制
  4. DeterministicAssetBundle:将AssetBundle的哈希校验值,存储在ID中(默认开启)
  5. ForceRebuildAssetBundle:强制重新导出所有的AB包
  6. ChunkBasedCompression:使用LZ4算法压缩AB包

压缩方式

  • 不压缩:AB包比较大,下载较慢,加载速度快(CPU不用运算解压缩)
  • LZMA算法压缩:默认压缩模式,流压缩方式(stream-based),文件尺寸居中,加载速度居中;使用LZMA算法压缩,压缩的包更小,但是加载时间更长,只支持顺序读取。使用之前需要整体解压。一旦被解压,这个包会使用LZ4重新压缩。使用资源的时候不需要整体解压。在下载的时候可以使用LZMA算法,一旦它被下载了之后,它会使用LZ4算法保存到本地上。
  • LZ4算法压缩:5.3以后可用,块压缩方式(chunk-based)压缩比例高,文件小,加载速度偏慢;使用LZ4压缩,压缩率没有LZMA高,但是我们可以加载指定资源而不用解压全部,可以实现实时解压随机存储。

alt

注意:使用LZ4压缩,可以获得跟不压缩想媲美的加载速度,而且比不压缩文件要小。

导出后的文件结构:

alt

和存储目录同名的文件和文件.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包的依赖关系) alt

AB包不能重复加载(需要对AB包进行卸载操作“Unload()”);当AB包使用完后需要卸载AB包;否则会妨碍其它AB包加载 报错如下:

alt

如果想处理依赖关系的加载,则必须加载主AB包(与目录名相同),因为依赖关系的存储,都存储在主AB包的配置文件中

  • 第一步(加载被依赖的AB包文件): 当所需AB包文件有被依赖的AB包时
    1. 加载主AB包
      AB包 = AssetBundle.LoadFromFile(AB包文件路径)
    2. 根据主AB包的配置文件,加载配置文件:
      AssetBundleManiifest 配置文件类 = LoadAsset("AssetBundleManiifest");
    3. 获得当前需要加载的AB包所依赖的AB包:
      string[] dependencies = mainManifest.GetAllDependencies("需要加载的AB包名称");
      //dependencies中存储的为所依赖的AB包的文件名
    4. 将所有的被依赖的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资源。

alt

卸载

一些ID Instance ID / GUID / Local ID(file ID) 参考链接

GUID:存在于unity中资源对应的.meta文件中,通过GUID就可以找到工程中的文件;表示这个文件,用来记录资源之间的关系。

alt

Local ID(file ID):资源内部的ID,用来记录资源之间的具体引用。

alt

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 当前实例的链接关系。

alt

如果稍后再次加载 AB 并且调用 AB.LoadAsset(),则 Unity 不会将现有 M 副本重新链接到新加载的材质。而是将加载 M 的两个副本。 alt

alt

如果再次加载该AB包时,分离了链接的物体不会受该新加载的AB包管理。因此如果不注意的话可能会导致一些不可控的问题。Unity中有Resources.UnloadUnusedAssets()方法可以很好地解决这个问题。