【Unity】AssetBundle

1 前言

        稍微写了下AssetBundle(AB包)的笔记。介绍了AB包是什么,以及如何使用的。后续有一部分基本是翻译的官方文档,侧重于概念类的,计划等后续有实际应用时再单独写出来。

2 AssetBundle

2.1 什么是AssetBundle

        AssetBundle是一个存档文件,包含可在运行时由Unity加载的特定于平台的非代码资源(比如模型、纹理、预制件、音频剪辑甚至整个场景)。AssetBundle可以表示彼此之间的依赖关系;例如,一个AssetBundle中的材质可以引用另一个 AssetBundle中的纹理。为了提高通过网络传输的效率,可以根据需要,选用内置压缩算法(LZMA 和 LZ4)来压缩 AssetBundle。
        AssetBundle可用于可下载内容(DLC),减小初始安装包的大小,加载针对最终用户平台优化的资源,以及减轻运行时内存压力。
        PS:简称,AB包。后面会混用,知道是同一个东西就好。

2.2 AssetBundle中包含什么

        在讨论AssetBundle中包含什么之前,需要明确AssetBundle可以表示什么?虽然前面说了其为存档文件,是一个文件,但有时候它还是会表示一些其他东西的。

2.2.1 实际的文件

        AssetBundle也可以指代磁盘上的实际文件。这称为AssetBundle存档。AssetBundle存档是一个容器,就像文件夹一样,可以在其中包含其他文件。这些被包含的文件有两种类型:

  • 一个序列化文件,其中包含分解为各个对象并写入此单个文件的资源。(人话:代码对象实例的序列化数据、以及一些预设体、模型等资源被打碎后的序列化数据,会放入此文件中。另外,此文件仅有一个。)
  • 资源文件,这是为某些资源(纹理和音频)单独存储的二进制数据块,允许 Unity 高效地在另一个线程上从磁盘加载它们。(资源文件会有多个,比如一个图片是一个资源文件,一段音频是一个资源文件。)

2.2.2 代码中的对象

        AssetBundle也可以指代通过代码进行交互以便从特定AssetBundle存档加载资源的实际AssetBundle对象。该对象包含添加到此存档文件的资源的所有文件路径的映射。

        PS:在代码中我们就通过此对象来加载使用资源。

2.3 如何创建AssetBundle

2.3.1 为AssetBundle分配资源

        为Project视图中的资源指定AssetBundle,包含主体与变体两部分(可以理解为名称、后缀名称),在AssetBundle打包出来后,主体与变体就对应包的名称与后缀。后缀随意写。
        那么如何指定呢?这里将给一个Cube预设体指定AssetBundle,如下:

然后我指定了名称、后缀、标签。如下:

PS:名称、后缀只能小写,即使输入大写也会被转为小写。

2.3.2 构建AssetBundle

        使用代码构建。代码要在Editor模式下运行,这部分是编辑器扩展的知识点,不懂可以查一查。代码如下:

using System.IO;
using UnityEditor;//编辑器头文件

public class CreateAssetBundles 
{
    //将此方法加入菜单栏中
    [MenuItem("Assets/Build AssetBundles")]
    static void BuildAllAssetBundle()
    {
        //打包后的保存路径
        string assetBundleDirectory = "Assets/AssetBundles";
        //若保存路径不存在,则创建路径上的文件夹
        if (!Directory.Exists(assetBundleDirectory))
        {
            //创建文件夹
            Directory.CreateDirectory(assetBundleDirectory);
        }
        //打包方法(参数:保存路径、打包选项、打包平台)
        //会将Project中所有指定了AssetBundle的资源进行打包
        BuildPipeline.BuildAssetBundles(assetBundleDirectory,
            BuildAssetBundleOptions.None,
            BuildTarget.StandaloneWindows64);
        
        //BuildAssetBundles参数说明:
        //--------------------------------------------------------
        //保存路径:字面意思。
        //-------------------------------
        //打包选项:可选参数有很多,可查看官方文档,这里最主要介绍三个。

        //BuildAssetBundleOptions.None:
        //使用LZMA算法压缩。
        //LZMA压缩包小,但加载时间长。LZMA压缩包使用之前需要整体解压,比如包中有资源x、y,在只需使用x的情况下,需要将x、y一起解压。LZMA压缩包一旦被解压,这个包会使用LZ4重新压缩。在使用资源时,LZ4压缩包不需要整体解压,用谁解谁。
        //所以一般在下载的时候下载LZMA压缩包,下载下来之后,再使用LZ4算法压缩保存到本地。通过UnityWebRequestAssetBundle加载的LZMA压缩格式Asset Bundle会自动重新压缩为LZ4压缩格式并缓存在本地文件系统上。如果通过其他方式下载并存储AB包,则可以使用AssetBundle.RecompressAssetBundleAsync API对其进行重新压缩。
        
        //BuildAssetBundleOptions.UncompressedAssetBundle:
        //不压缩,包大,加载快。
        
        //BuildAssetBundleOptions.ChunkBasedCompression:
        //使用LZ4算法压缩。
        
        //PS:注意使用LZ4压缩,可以获得可以跟不压缩相媲美的加载速度,而且比不压缩文件要小。
        //-------------------------------
        //打包平台:字面意思。选什么平台,那么就只能在什么平台使用。
        //--------------------------------------------------------
    }
}

然后不用运行Unity,只需要在菜单栏中执行即可。

前往我们指定的路径下,即可看到创建的AB包:

注意,不带.manifest后缀的才是AB包。带.manifest的文件是辅助文件,其记录AB包的一些信息,比如资源信息、依赖的AB包信息等,可以用文本编辑器打开看一下文件内容。另外注意额外生成的AssetBundles AB包,其会记录其他所有打包的AB包的相关信息,这里先不用管,后续会用到这个文件,到时候就知道其作用了。

2.3.3 使用AB包名称来指定文件夹

如图,若在指定AB包名称时使用这种方式,则会在目标目录下创建一个test文件夹,再在其中创建cube的AB包。

PS:可以多级目录。

2.3.4 多资源打包问题

        当多个资源指定同一个AssetBundle时(同名、同后缀),它们将被打包到一个包中。

2.4 AB包依赖问题

        AB包依赖问题的是资源的依赖问题,是资源的优化问题。
        如果一个或多个资源(UnityEngine.Objects)引用了(使用了)另一个AB包中的资源,则此资源在打包为AB包时,会自动依赖于刚才那个AB包,打包的AB包中不存在引用的资源。
        若在打包时,引用的资源不存在于其他AB包中,则引用的资源将被打包到当前AB包中。
        若是有多个资源打包为多个AB包,这多个资源引用了一个不存在于其他AB包中的资源,则所引用的资源将会分别打包进当前AB包中,即存在多个副本,每个AB包里有一个副本。这将影响内存资源和加载时间。要想不要副本,就需要将引用的资源本身打包为AB包,注意这里的本身,本身的意思是以自己为对象打包,而不是以被引用的自己为对象打包(以自己为对象不意味着只有自己,也可以和别的资源一起打包),比如前面说的多个资源打包,第一个打的包里有了引用资源的副本,但其也不能为后续的AB包提供“依赖”功能,因为其是作为资源的引用打包的。

        如果一个AB包中的资源x在另外的AB包中有依赖项(即x引用的资源),则在加载x对象之前,需要先加载包含这些依赖项的AB包,否则将会出现引用丢失问题。Unity不会尝试自动加载依赖项,所以需要我们自己注意。
        AB包之间无加载顺序要求 ,只需要保证资源在加载前,其依赖项所在的AB包已经被加载。

2.4.1 AB包依赖与否对比

        这里我创建了Cube、Sphere预设体,它们使用了同一个材质、纹理,并分别将其打包为两个AB包。打包后结果:

可以看到两个AB包的大小都是46KB,总计92KB。

        然后,将其删除,准备重新打包。但这一次除了打包两个预设体外,还将它们共用的材质和纹理打包为一个AB包,即使用“AB包依赖”。打包后的结果:

可以看到cube、sphere的AB包大小由46KB变为了3KB,材质、纹理AB包(图中share.myab)的大小为45KB,总计51KB。

2.4.2 依赖的AB包需要提前加载演示

        开头我们说过,若有依赖关系需要注意:加载资源前,资源依赖项所在的AB包需要提前加载。
        前面我们打包好了cube、sphere、share三个AB包,其中cube和sphere是依赖share的,接下来将使用从这三个AB包中加载资源,在场景中创建一个Cube和一个Sphere,分别演示share有无提前加载的情况。

没有提前加载

代码:

using UnityEngine;

public class InstantiateAssetBundles : MonoBehaviour
{
    void Start()
    {
        //加载cube AB包
        AssetBundle cubeAB = AssetBundle.LoadFromFile("Assets/AssetBundles/cube.myab");
        //加载sphere AB包
        AssetBundle sphereAB = AssetBundle.LoadFromFile("Assets/AssetBundles/sphere.myab");

        //没有提前加载share AB包
        
        //加载AB包中的资源
        var cubePrefab = cubeAB.LoadAsset<GameObject>("Cube");
        var spherePrefab = sphereAB.LoadAsset<GameObject>("Sphere");

        //使用资源创建对象
        Instantiate(cubePrefab);
        Instantiate(spherePrefab);
    }
}

运行结果:

没有提前加载的后果,引用的资源丢失了。

提前加载

代码:

using UnityEngine;

public class InstantiateAssetBundles : MonoBehaviour
{
    void Start()
    {
        //(AB包之间无顺序要求,顺序任意)
        //加载cube AB包
        AssetBundle cubeAB = AssetBundle.LoadFromFile("Assets/AssetBundles/cube.myab");
        //加载sphere AB包
        AssetBundle sphereAB = AssetBundle.LoadFromFile("Assets/AssetBundles/sphere.myab");
        //加载share AB包(在资源前面,提前加载)
        AssetBundle shareAB = AssetBundle.LoadFromFile("Assets/AssetBundles/share.myab");

        //加载AB包中的资源
        var cubePrefab = cubeAB.LoadAsset<GameObject>("Cube");
        var spherePrefab = sphereAB.LoadAsset<GameObject>("Sphere");
    
        //使用资源创建对象
        Instantiate(cubePrefab);
        Instantiate(spherePrefab);      
    }
}

运行结果:

但是!

        若我们加载share AB包,即是放到资源加载之后,甚至放到Instantiate之后,但只要我们加载了,材质依旧可以成功获取到。这个目前不清楚为什么,官方文档上的说明是要在资源加载之前。暂时先按照文档来操作吧。

2.5 加载AB包中资源(本地)

        不是从网络服务器上加载,而是在本地电脑中加载。

2.5.1 从内存中加载

AssetBundle.LoadFromMemoryAsync

        此函数异步加载内存中包含AB包数据的字节数组。也可以根据需要传递CRC值。如果AB包采用的是LZMA压缩方式,将在加载时解压缩AB包。LZ4压缩包则会以压缩状态加载。

异步加载代码:

using System.Collections;
using System.IO;
using UnityEngine;

public class LoadFromMemoryExample : MonoBehaviour
{
    void Start()
    {
        //开启协程,异步加载AB包
        StartCoroutine(LoadFromMemoryAsync("Assets/AssetBundles/cube.myab"));
    }

    IEnumerator LoadFromMemoryAsync(string path)
    {
        //先使用File读取AB包的字节数组,然后异步加载此数组
        AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
        //等待加载完成
        yield return createRequest;
        //获取加载的AB包
        AssetBundle bundle = createRequest.assetBundle;
        //加载资源
        var prefab = bundle.LoadAsset<GameObject>("Cube");
        //使用资源创建对象
        Instantiate(prefab);
    }
}

注意,File.ReadAllBytes(path) 可以被替换为获得字节数组的任何方式。除此之外,我们也可以使用同步加载的方式。

同步加载代码:

using System.IO;
using UnityEngine;

public class LoadFromMemoryExample : MonoBehaviour
{
    void Start()
    {
        //先使用File读取AB包的字节数组,然后同步加载此数组,获取AB包
        AssetBundle bundle = AssetBundle.LoadFromMemory(File.ReadAllBytes("Assets/AssetBundles/cube.myab"));
        //加载资源
        var prefab = bundle.LoadAsset<GameObject>("Cube");
        //使用资源创建对象
        Instantiate(prefab);
    }
}

2.5.2 从文件中加载

AssetBundle.LoadFromFile

        从本地存储中加载未压缩的AB包时,此API非常高效。如果AB包未压缩或采用了数据块 (LZ4) 压缩方式,LoadFromFile将直接从磁盘加载AB包。使用此方法加载LZMA压缩的AB包时,将首先解压缩AB包,然后再将其加载到内存中。

加载代码:

using UnityEngine;

public class LoadFromFileExample : MonoBehaviour
{
    void Start()
    {
        //加载AssetBundle对象
        AssetBundle myLoadedAssetBundle = AssetBundle.LoadFromFile("Assets/AssetBundles/cube.myab");
        //若为空,则加载失败
        if (myLoadedAssetBundle == null)
        {
            Debug.Log("Failed to load AssetBundle!");
            return;
        }
        //加载AssetBundle对象中的资源
        var prefab = myLoadedAssetBundle.LoadAsset<GameObject>("Cube");
        //使用资源创建对象
        Instantiate(prefab);
    }
}

上述方法是同步加载,除此之外,还可以异步加载,这里就不列代码了,跟从内存中加载的异步方法使用方式差不多。

2.6 加载AB包中的资源(网络)

        从网络中加载AB包的方式有两种:

其中前者基本不在使用了,所以这里介绍第二个。

        UnityWebRequestAssetBundle 有一个特定 API 调用来处理 AssetBundle。首先,需要使用 UnityWebRequestAssetBundle.GetAssetBundle 来创建 Web 请求。返回请求后,请将请求对象传递给 DownloadHandlerAssetBundle.GetContent(UnityWebRequest)。GetContent 调用将返回 AssetBundle 对象。

示例代码:

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class LoadFromWeb : MonoBehaviour
{
    void Start()
    {
        //开启协程(以本地路径为例,注意path应是绝对路径)
        StartCoroutine(InstantiateObject("file:///" + path));
    }

    IEnumerator InstantiateObject(string url)
    {
        //创建AB包下载请求
        UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(url, 0);
        //开始请求下载并等待
        yield return request.SendWebRequest();
        //下载完成后,从请求中获取AB包对象
        AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
        //从AB包中加载资源对象
        GameObject cube = bundle.LoadAsset<GameObject>("Cube");
        //使用资源对象实例化游戏对象
        Instantiate(cube);
    }
}

        下载AB包后,除了使用GetContent返回对象的方式获取AB包对象,还可以使用 DownloadHandlerAssetBundle 类对象的 assetBundle 属性,从而以 AssetBundle.LoadFromFile 的效率加载 AssetBundle。只需要替换上述代码中GetContent那行即可:

//下载完成后,从请求中获取AB包对象
AssetBundle bundle = (request.downloadHandler as DownloadHandlerAssetBundle).assetBundle;

        另外,通过request我们也可以获取到下载的AB的字节数组,有了字节数组我们就可以做很多事情,比如通过字节数组将下载的AB包存储到本地。

//获取字节数组
byte[] data = request.downloadHandler.data;//字节数组,可以用于保存到本地

2.7 从AB包中加载资源

        这个实际上在前面加载AB包的案例代码中都已经看到过了,这里再单独讲下。

        加载资源的函数主要有以下几种: LoadAsset、LoadAllAssets 及其各自的异步对应选项 LoadAssetAsync 和 LoadAllAssetsAsync。

同步方法代码演示:

//加载单个资源对象(泛型T)
T gameObject = loadedAssetBundle.LoadAsset<T>(assetName);

//加载所有资源对戏
Object[] objectArray = loadedAssetBundle.LoadAllAssets();

异步方法代码演示:

//加载单个资源对象(泛型T)
AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<T>(assetName);//加载
yield return request;//等待加载
T loadedAsset = request.asset as T;//获取资源对象

//加载所有资源
AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync();//加载
yield return request;//等待加载
Object[] loadedAssets = request.allAssets;//获取资源对象

2.8 加载AssetBundles的清单

        AssetBundles清单中包含了AB包的相关信息,这些信息是非常有用的,比如有哪些AB包,这些AB包所依赖的AB包等信息。所以加载清单,很必要。
        具体要怎么获取清单对象呢?我们需要首先加载AssetBundles AB包(就是我创建AB包时额外给我们生成的那个),然后再从加载的AB包中获取清单对象。这里,清单对象可以视为资源对象。这么一说一切就通透了。代码:

//加载AssetBundles AB包
AssetBundle assetBundle = AssetBundle.LoadFromFile(AssetBundle文件Path);
//加载清单
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");//此字符串不可修改

此时获取到了一个清单对象(manifest),我们便可以通过其获取相关信息。

        清单常用于获取某AB包所依赖的AB包的名称,然后借助这些名称将依赖的AB包加载进来。代码:

using UnityEngine;

public class LoadFromFileExample : MonoBehaviour
{
    void Start()
    {
        //加载cube AB包
        AssetBundle cubeAB = AssetBundle.LoadFromFile("Assets/AssetBundles/cube.myab");
        //加载AssetBundles AB包
        AssetBundle AB = AssetBundle.LoadFromFile("Assets/AssetBundles/AssetBundles");
        //加载清单
        AssetBundleManifest manifest = AB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        //从清单中获取cube AB包所依赖的AB包名称
        string[] dependencies = manifest.GetAllDependencies("cube.myab");
        //加载所依赖的AB包
        foreach (string dependency in dependencies)
        {
            Debug.Log(dependency);//输出名称
            AssetBundle.LoadFromFile("Assets/AssetBundles/" + dependency);//加载
        }
        //加载cube AB包中的资源
        var prefab = cubeAB.LoadAsset<GameObject>("Cube");
        //使用资源创建对象
        Instantiate(prefab);
    }
}

2.9 .manifest文件

        .manifest文件是在创建AB包时一起生成的辅助文件(清单文件),包含了AB包的一些信息。我们用文本编辑器打开AssetBundles.manifest文件就能发现其里面的内容就是我们上面清单中的内容。这些文件的作用就是提给我们在文件层面查看里面的内容,换言之,即使我们把.manifest删除,也不会影响我们代码的执行。

2.10 卸载AB包

2.10.1 卸载API

        我们可以通过AssetBundle.Unload(bool)或者AssetBundle.UnloadAsync(bool)函数来卸载AB包,通过AB对象卸载的话只会卸载当前AB包,也可以通过AssetBundle类卸载所有AB包。

//使用cubeAB包对象卸载cubeAB包
cubeAB.Unload(true);

//卸载所有AB包
AssetBundle.UnloadAllAssetBundles(true);

        另外,卸载方法是有一个bool参数需要填写的,此参数含义是“是否卸载从AB包中加载的对象。”注意这里的对象是什么,是资源对象,即一个类对象。注意!注意!注意!是类对象,不是在场景中实例化的游戏对象!一定要分清楚。实例化的游戏对象是不再属于AB包的,所以无法通过卸载AB包来卸载。当我们以ture卸载一个AB包时,其资源对象将会被销毁,但以资源对象在场景中实例化的游戏对象不会从场景中删除。
        分一下层次的话,大概是这种:AB包→加载的对象→实例化的对象(场景中)。

2.10.2 参数详解

        这里将讨论参数选择(是否卸载加载的对象)会造成什么影响。还以之前的例子,一个Cube预设体,身上有一个材质文件。

整体AB包(无依赖)

        打包一个cube AB包,预设体与材质都在其中。我们在场景中实例化一个对象,那么此时我们需要注意,明确目前存在的内容:

  1. 加载的cube AB包
  2. 加载的Cube预设体对象
  3. 加载的image材质对象
  4. 加载的image纹理对象
  5. 场景中实例化的Cube

        然后我们unload(true),此时场景中的对象会怎样呢?答案是:材质丢失(即贴图没了)。先看看上述内容卸载后的状态:

  1. 加载的cube AB包(销毁)
  2. 加载的Cube预设体对象(销毁)
  3. 加载的image材质对象(销毁)
  4. 加载的image纹理对象(销毁)
  5. 场景中实例化的Cube(存在)

可以看到,我们的Cube依旧存在,但其所使用的材质、纹理已经被销毁了,所以就出现了材质丢失的情况。

        若是unload(false),场景中的对象又会怎样呢?答案是:没有变化。看状态:

  1. 加载的cube AB包(销毁)
  2. 加载的Cube预设体对象(存在)
  3. 加载的image材质对象(存在)
  4. 加载的image纹理对象(存在)
  5. 场景中实例化的Cube(存在)

可以看到,只有AB包被销毁了,其他对象依旧存在,所以我们的实例化对象是没有影响的。这些对象依旧可以使用。

带有依赖的AB包

        打包cube AB包、share AB包,share中是材质、纹理,cube AB包是依赖share AB包的,跟前代码案例的情况一样。明确内容:

  1. 加载的cube AB包
  2. 加载的Cube预设体对象
  3. 场景中实例化的Cube
  4. 加载的share AB包
  5. 加载的image材质对象
  6. 加载的image纹理对象

这里有一个关系需要提前说明,Cube预设体对象依赖image材质、纹理对象。

        那么这里unload(true) share AB包,此时场景中的对象会怎样呢?答案是:材质丢失。状态:

  1. 加载的cube AB包(存在)
  2. 加载的Cube预设体对象(存在)
  3. 场景中实例化的Cube(存在)
  4. 加载的share AB包(销毁)
  5. 加载的image材质对象(销毁)
  6. 加载的image纹理对象(销毁)

因为材质、纹理对象被销毁了,所以材质丢失。

        若unload(flase) share AB 包,此时场景中的对象会怎样呢?答案是:没有变化。状态:

  1. 加载的cube AB包(存在)
  2. 加载的Cube预设体对象(存在)
  3. 场景中实例化的Cube(存在)
  4. 加载的share AB包(销毁)
  5. 加载的image材质对象(存在)
  6. 加载的image纹理对象(存在)

材质、纹理对象依旧存在。

        若unload(false) cube AB、unload(false) share AB,然后再实例化一个对象,此时新对象是怎样的?答案是:带材质的对象。状态:

  1. 加载的cube AB包(销毁)
  2. 加载的Cube预设体对象(存在)
  3. 场景中实例化的Cube(存在)
  4. 加载的share AB包(销毁)
  5. 加载的image材质对象(存在)
  6. 加载的image纹理对象(存在)

虽然AB包都被销毁了,但Cube预设体对象依赖的是材质、纹理对象,所以没影响,我们依旧可以通过预设体对象来正确的实例化对象。

        若unload(true) cube AB、unload(false) share AB,然后我们再次加载cube AB包,再次加载Cube预设体对象,并以此对象在场景中实例化对象,此时新对象是怎样的?答案是:材质丢失。状态不就列了。那么这里为什么是材质丢失?虽然我们的材质、纹理对象还存在,但依赖它们的是之前的Cube预设体对象,我们ture卸载之后它就不复存在了,新加载的cube AB包中的Cube预设体对象是需要通过AB包来建立依赖(建立依赖时,若材质、纹理对象不存在便会被创建),但此时没有了share AB包对象,所以也就无法建立依赖,因此此时的Cube预设体对象是没有材质、纹理的(即使内存中存在上一个share AB包遗留的材质、纹理对象),实例化出来自然是材质丢失。需要注意,若我们在加载新Cube预设体之前,再次加载share AB包,那么新Cube预设体是能依赖到材质、纹理对象的,但是,这些材质、纹理对象不是之前的对象,而是新的对象,换言之是由新share AB包中诞生的材质、纹理对象。此时内存中存在两份材质、纹理对象,一份老的、一份新的,老的那份已经无法通过卸载AB包的API卸载了,因为其已经与AB包断开联系了。

        通常,建议使用AssetBundle.Unload(true)比较好,AssetBundle.Unload(false)往往会带来比较复杂的情况。但倘若真的需要使用AssetBundle.Unload(false),那么我们应该知道如何卸载那些与AB包脱离关系的对象(设为rab对象)。卸载方法如下:

  • 在场景和代码中消除对不需要的对象的所有引用。完成此操作后,调用 Resources.UnloadUnusedAssets。(消除对rab对象的所有引用后,调用Resources.UnloadUnusedAssets方法即可)
  • 以非附加方式加载场景。这样在切换场景时会销毁当前场景中的所有对象并自动调用 Resources.UnloadUnusedAssets。(切换场景,在当前场景销毁的时候顺带给我们把rab对象销毁了)

2.11 AB包压缩与缓存

        (这部分基本是官方文档翻译,然后加了些自己的补充)

2.11.1 压缩

        AssetBundle 文件是一种归档格式,由一个小的标题数据结构和一个包含虚拟文件的内容部分组成。文件头(Header)部分从不压缩,内容部分可以选择压缩。默认情况下,Unity 采用全文件压缩(LZMA)方式压缩内容部分,并采用基于块的压缩(LZ4)方式缓存AB包。

        LZMA压缩:使用 LZMA 压缩时,AssetBundle 文件的整个内容部分将作为一个单一流进行压缩。这种全内容压缩方式的文件大小小于基于块的压缩方式。这是从内容交付网络(CDN)下载的AB包的首选格式。缺点是必须将整个文件解压到 RAM 中,才能从这些AB包中读取资源。当一个AB包包含的资源是只要使用就需要加载AB包中所有资源时,就可以使用这种压缩方式,如将角色或场景的所有资产打包。这是调用 BuildPipeline.BuildAssetBundles 时,没有指定特定的压缩方式所使用的压缩方式,即参数为BuildAssetBundleOptions.None。

        不压缩:AB包也可以完全不压缩数据的方式构建。未压缩的缺点是文件下载量较大,因为某些类型的内容在AB包内可以高度压缩。不过,由于无需解压缩,下载后的加载时间会快很多。当从一个较大的AB包中只加载几个对象时,这一点尤其有用。未压缩的AB包是16字节对齐的。在调用 BuildPipeline.BuildAssetBundles 时,指定 BuildAssetBundleOptions.UncompressedAssetBundle 标志即可不压缩。

        LZ4压缩:LZ4 采用基于块的算法,允许以片段或 "块 "的形式解压缩AB包。在写入AB包时,每个 128KB 的内容块都会在保存前被压缩。由于每块内容都是单独压缩的,因此整体文件大小比用 LZMA 压缩的AB包要大。但这种方法可以有选择性地检索和加载请求对象所需的内容块,而不是解压缩整个AB包。LZ4 的加载时间与未压缩的AB包相当,但好处是可以减小磁盘大小。这种压缩格式是AB包缓存的首选格式,下文将对此进行介绍,对于作为安装的一部分分发的AB包(什么意思?为什么?)或在大小不是最重要的其他情况下,这种格式也是不错的选择。在调用 BuildPipeline.BuildAssetBundles 时,指定 BuildAssetBundleOptions.ChunkBasedCompression 标志,即可使用这种压缩方式。

        由于不同的数据在压缩时会有不同程度的压缩比例,因此可以通过使用不同的压缩算法来测试不同压缩算法的压缩比例,然后根据结果决定使用哪种格式。

        如果使用自定义缓存解决方案下载和存储数据,可以使用 AssetBundle.RecompressAssetBundleAsync 更改压缩方式,例如在下载后将 LZMA 格式的 AssetBundle 转换为未压缩或 LZ4 格式。

        注意:WebGL 不支持AB包的 LZMA 压缩。在 WebGL 平台上对AB包使用 LZ4 压缩。有关更多信息,请参阅 Reduce load times with AssetBundles

2.11.2 缓存

        从网络服务下载AB包时,需要考虑缓存问题,这样设备就不必在每次运行播放器时都下载相同的内容。由于AB包可能会被更新,因此建立一种机制将本地缓存的AB包替换为较新版本也很重要。

        Unity 提供了基于磁盘的内置缓存,用于存储通过 UnityWebRequestAssetBundle 下载的 AssetBundle。要启用缓存,必须在调用 UnityWebRequestAssetBundle.GetAssetBundle 时指定版本整数或版本哈希参数。默认情况下,添加到磁盘缓存的任何AB包都将转换为 LZ4 压缩。因此,最初下载和加载 LZMA 压缩格式的AB包需要较长的时间,因为要进行重新压缩,但随后的加载会使用缓存版本(LZ4压缩格式)并快速运行。如果 Caching.compressionEnabled 为 false,Unity 会以未压缩格式将AB包写入磁盘缓存。

        通过互联网下载AB包时,必须采取措施确保缓存不接受被损坏或篡改的文件内容。在调用 UnityWebRequestAssetBundle.GetAssetBundle 时,您应指定预期的 CRC,这样 Unity 在将文件添加到缓存时就能将该值与下载的内容进行比较。在将 LZMA AssetBundle 转换为 LZ4 时,可以低成本执行 CRC 校验。一旦经过验证的文件到达缓存,就不需要再次重复 CRC 校验。另请参阅 AssetBundle Download Integrity and Security

        Caching 类可用于管理内置的 AssetBundle 缓存,例如清除其内容或检查 AssetBundle 是否已被缓存。

2.11.3 AB包内存使用

        为了提高性能,Unity 会在加载基于块或未压缩的AB包时在内存中保留一些未压缩的数据。但无论底层AB包文件有多大,这种缓存的大小都是固定的。

        要加载一个AB包, Unity需要随机访问它的内容,可以通过磁盘上的文件,内存中的文件或c# FileStream。它还要求 AssetBundle 文件未压缩或使用基于块的压缩(LZ4)。为了建立一个可加载的AB包,Unity 有时需要创建一个临时的内存AB包。这并不总是坏事,因为 AB包内容一旦进入内存,就可以快速加载。但在很多情况下,最好还是将文件以本地AB包或缓存下载的形式存在磁盘上,这样可以最大限度地减少内存使用量,而且即时压缩转换也不会拖慢加载速度。

        在以下情况下,将创建临时内存中的AB包:

  • 调用AssetBundle.LoadFromFile和AssetBundle.LoadFromFileAsync时,当AB包使用LZMA压缩时。
  • 所有AssetBundle.LoadFromMemory和AssetBundle.LoadFromMemoryAsync。
  • 调用AssetBundle.LoadFromStream和AssetBundle.LoadFromStreamAsync时,当AB包使用LZMA压缩时。
  • 调用UnityWebRequestAssetBundle.GetAssetBundle而不提供版本整数或散列,或者选择不支持基于磁盘的AB包缓存的平台(例如WebGL)。
  • (PS:LZMA压缩的AB包,在Load的时候就需要全解压。LZ4压缩的AB包,在LoadFromFile/Async、LoadFromStream/Async时只加载AB包Header,在使用具体资源时分块解压。)

        临时文件使用的内存在所有读取完成后,调用Unload释放。

        注意:在支持基于磁盘的AB包缓存的平台上, Caching.compressionEnabled 设置将影响用于临时内存中的AB包的格式。默认情况下,它是true,内存中的AssetBundles使用LZ4。当 Caching.compressionEnabled 为false时,这些内存中的文件将被解压缩,因此可能会占用更多的RAM。在不支持缓存的平台上,内存格式总是LZ4。如果输入是不同的格式,则执行动态转换,这可能会增加加载时间。

        注意:在调用AssetBundle.LoadFromFile、AssetBundle.LoadFromFileAsync、AssetBundle.LoadFromStream或AssetBundle.LoadFromStreamAsync做CRC检查。对于基于块的文件,将强制对文件的每个块进行完全读取和解压缩。这个计算是逐块进行的,而不是将整个文件加载到RAM中,所以它没有内存问题,但它会减慢加载时间。对于LZMA格式的AB包,执行CRC检查没有显著的额外成本,因为加载它总是读取并解压缩所有内容。

        内存分析器,诸如Memory Profiler包之类的工具可以用来检查加载的AssetBundles使用了多少内存。

2.12 AB包打补丁

        (这部分基本是官方文档翻译,然后加了些自己的补充)

        AB包打补丁很简单,只需要下载新的AB包并替换现有的AB包。如果使用UnityWebRequestAssetBundle.GetAssetBundle管理应用程序缓存的AB包,则传递不同的版本或哈希参数将触发新AB包的下载。

        在修补系统中要解决的更难的问题是检测要替换的 AssetBundle。修补系统需要两个信息列表:

  • 当前已下载的AB包及其版本控制信息的列表
  • 服务器上的AB包及其版本控制信息的列表

修补程序应下载服务器端AB包列表并比较这些AB包列表。应重新下载缺少的AB包或已更改版本控制信息的AB包。

        Unity不提供任何内置的差异补丁机制,UnityWebRequestAssetBundle.GetAssetBundle在使用内置缓存系统时不执行差异补丁。相反,它总是完整地下载新的AB包。如果需要不同的补丁,那么必须编写自定义下载程序。Unity以确定的方式构建数据排序的AB包,因此重建AB包补丁可能比完整文件小得多。未压缩或基于块的压缩(LZ4)将比默认的完整文件压缩(LZMA)更好地修补。大多数编写自己系统的开发人员选择为他们的AB包列表使用行业标准的数据格式,例如JSON,并使用标准的c#类来计算文件内容散列,例如MD5。文件内容散列可以作为AB包版本,或者您可以使用由构建系统生成的更传统的版本号。

2.13 AB包分组策略(参考)

  1. 将经常更新的资源与不经常更新的资源区分开。防止更新资源时频繁更新那些不需要更新的资源,造成下载资源浪费。
  2. 把需要同时加载的资源放在一个包里面。这样加载时只需加载一个包就行了,提高加载效率。
  3. 将相关的共享资源放在一个包里。防止产生资源副本,造成内存、加载效率问题。
  4. 对于同一资源的不同版本AB包,可以考虑通过后缀来区分。

3 结束语

        后面翻译官方文档那块以后有时间再调整调整,自己写着都感觉有点小乱。
        不得不说,官方文档写的真一言难尽,有些真不如直接看英文,而且最新版的文档有些部分也没有中文,所以还是得看英文的。话是这么说,不过文章里翻译那部分基本还是靠翻译软件翻译的,个别地方我则是自己去看然后修改,毕竟自己全看太费劲了,既然有科技就要活用嘛。

猜你喜欢

转载自blog.csdn.net/Davidorzs/article/details/134923656