当前位置:   article > 正文

Unity AssetsBundle 详解_unity assetbundle

unity assetbundle

1.AssetBundle 概念

AssetBundle又称AB包,是Unity提供的一种用于存储资源的资源压缩包。

Unity中的AssetBundle系统是对资源管理的一种扩展,通过将资源分布在不同的AB包中可以最大程度地减少运行时的内存压力,可以动态地加载和卸载AB包,继而有选择地加载内容。

2.AssetBundle 优势

  1. AB包存储位置自定义,继而可放入可读可写的路径下便于实现热更新
  2. AB包自定义压缩方式,可以选择不压缩或选择LZMA和LZ4等压缩方式,减小包的大小,更快的进行网络传输。
  3. 资源可分布在不同的AB包中,最大程度减少运行时的内存压力, 可做到即用即加载,有选择的加载需要的内容。
  4. AB包支持后期进行动态更新,显著减小初始安装包的大小,非核心资源以AB包形式上传服务器,后期运行时动态加载,提高用户体验。

AssetBundle和Resources的比较

AssetBundleResources
资源可分布在多个包中所有资源打包成一个大包
存储位置自定义灵活必须存放在Resources目录下
压缩方式灵活(LZMA,LZ4)资源全部会压缩成二进制
支持后期进行动态更新打包后资源只读无法动态更改

3.AssetBundle 特性

  1. AB包可以存储绝大部分Unity资源但无法直接存储C#脚本,所以代码的热更新需要使用Lua或者存储编译后的DLL文件。
  2. AB包不能重复进行加载,当AB包已经加载进内存后必须卸载后才能重新加载。
  3. 多个资源分布在不同的AB包可能会出现一个预制体的贴图等部分资源不在同一个包下,直接加载会出现部分资源丢失的情况,即AB包之间是存在依赖关系的,在加载当前AB包时需要一并加载其所依赖的包。
  4. 打包完成后,会自动生成一个主包(主包名称随平台不同而不同),主包的manifest下会存储有版本号、校验码(CRC)、所有其它包的相关信息(名称、依赖关系)

4.AssetBundle 使用流程

AssetBundle的使用流程比较繁琐,大致上分为五个阶段:分组、打包、加载包、加载资源、卸载包

4.1 分组

在这里插入图片描述
分组策略:

  1. 按逻辑实体分组
    • 一个UI界面或者所有UI界面一个包(这个界面里面的贴图和布局信息一个包)
    • 一个角色或者所有角色一个包(这个角色里面的模型和动画一个包)
    • 所有的场景所共享的部分一个包(包括贴图和模型)
      2.按照类型分组
      所有声音资源打成一个包,shader打成一个包,模型打成一个包,材质打成一个包
      3.按照使用分组
      把在某一时间内使用的所有资源打成一个包。可以按照关卡分,一个关卡所需要的所有资源包括角色、贴图、声音等打成一个包。也可以按照场景分,一个场景所需要的资源一个包
  • 注意事项:
  1. 经常更新的资源放在一个单独的包里面,跟不经常更新的包分离
  2. 把需要同时加载的资源放在一个包里面
  3. 可以把其他包共享的资源放在一个单独的包里面
  4. 把一些需要同时加载的小资源打包成一个包
  5. 如果对于一个同一个资源有两个版本,可以考虑通过后缀来区分,例如v1、v2、v3

4.2 打包

1、代码打包

Using Unity.Editor;

[MenuItem("Assets/Build AssetBundles")]
static void BuildAllAssetBundles()
{
   string assetBundleDirectory = "Assets/AssetBundles";
   if (!Directory.Exists(assetBundleDirectory))
   {
      Directory.CreateDirectory(assetBundleDirectory);
   }
   //BuildTarget -- 打包平台,打包的资源只能在该平台下使用
   BuildPipeline.BuildAssetBundles(assetBundleDirectory,
                                        BuildAssetBundleOptions.None,
                                        BuildTarget.StandaloneWindows);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

  此脚本将在 Assets 菜单底部创建一个名为 Build AssetBundles 的菜单项,该菜单项将执行与该标签关联的函数中的代码。单击 Build AssetBundles 时,将随构建对话框一起显示一个进度条。此过程将会获取带有 AssetBundle 名称标签的所有资源,并将它们放在 assetBundleDirectory 定义的路径中的文件夹中。
BuildAssetBundleOptions:

  • None: 默认构建AssetBundle的方式。使用LZMA算法压缩,此算法压缩包小,但是加载时间长,而且使用之前必须要整体解压。解压以后,这个包又会使用LZ4算法重新压缩,这样这种包就不要对其整体解压了。(也就是第一次解压很慢,之后就变快了。
  • UncompressedAssetBundle: 不压缩数据,包大,但是加载很快。
  • ChunkBaseCompression: 使用LZ4算法压缩,压缩率没有LZMA高,但是加载资源不必整体解压。这种方法中规中矩,比较常用。
  • 其它的参数请自行百度

2、AssetBundleBrowse 工具打包
AssetBundleBrowser插件的获取
Unity 2019版本可以直接在Windows —> PackageManager里面找到此插件并直接安装
或在https://github.com/Unity-Technologies/AssetBundles-Browser下载
在这里插入图片描述
2、AssetBundleBrowser面板的使用
通过 Windows --> AssetBundle Browser下打开AB包管理面板 一共有三个面板
Configure面板
查看当前AB包及其内部资源的基本情况(大小,资源,依赖情况等)
在这里插入图片描述
Build面板
负责AssetBundle打包的相关设置 按Build即可进行打包
在这里插入图片描述
三种压缩方式:

  • NoCompression:不压缩,解压快,包较大,不建议使用。
  • LZMA: 压缩最小,解压慢,用一个资源要解压包下所有资源。
  • LZ4: 压缩稍大,解压快,用什么解压什么,内存占用低,更建议使用。
    一般需要进行更改的设置即为图中勾选的相关选项设置。

Inspect面板
主要用来查看已经打包后的AB包文件的一些详细情况(大小,资源路径等)
在这里插入图片描述
打包后的 manifest 文件
打包完成后,会自动生成一个主包(主包名称随平台不同而不同),主包的manifest下会存储有版本号、校验码(CRC)、所有其它包的相关信息(名称、依赖关系)在这里插入图片描述
在这里插入图片描述

4.3 加载包

// 从文件中加载
AssetBundle.LoadFromFile(abPath);
AssetBundle.LoadFromFileAsync();
// 从内存中加载
AssetBundle.LoadFromMemory();
// 从网络下载
UnityWebRequestAssetBundle.GetAssetBundle();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

① AssetBundle.LoadFromFile(Async)

最常用也是最推荐使用的方法。
这个方法可以高性能的加载没有压缩或者通过LZ4方法压缩的AssetBundle。
使用这个方法的时候,大部分平台只加载AssetBundle的包头,剩下的数据仍然在硬盘上。AssetBundles的对象会按需要进行加载,或者被间接引用到的时候加载。在这个情境下不会有额外的内存被消耗。

② AssetBundle.LoadFromMemory(Async)

不建议使用的API。常用于通过加密过的ab资源的加载。
如果你通过下载或者其他方法已经把Assetbundle的字节保存到了内存中的一个数组中,就能通过此函数进行加载。
使用这个方法的时候,底层会把内存中已经加载好的assetbundle数组byte[]拷贝到一个新分配的、相邻的地址上,如果使用了LZMA压缩方法,拷贝的时候还会顺便解压。造成的问题是内存使用的峰值至少是assetbundle的两倍。

③ UnityWebRequestAssetBundle.GetAssetBundle()

从远端服务器加载ab资源时推荐的方法。
UnityWebRequest类允许开发者定义unity应该怎么处理下载的数据,避免不必要的内存消耗。
使用DownloadHandlerAssetBundle 类可以对下载进行配置,使用一个worker线程流式下载数据,并存放在一个可变大小的buffer,或者某个临时的存储空间中。DownloadHandler 不会复制已下载的bytes。
对于LZMA压缩的Assetbundles,下载的时候会顺便解压,存放的时候通过LZ4再次压缩,可以通过Caching.CompressionEnabled 设置。
下载完成后,可以通过DownloadHandler的属性对assetbundle进行访问,这个行为类似于AssetBundle.LoadFromFile。

 UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(assetBundlePath, 0);
 var opt = request.SendWebRequest();
 opt.completed += (_) =>
 {
     AssetBundle = DownloadHandlerAssetBundle.GetContent(request);
 };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

④ WWW.LoadFromCacheOrDownload
旧版API,因为性能问题已被废弃,建议使用UnityWebRequest。

4.4 加载资源

// 泛型加载
T obj = ab.LoadAsset<T>(ResourceName); 
// 参数指明资源类型 防止重名
Object obj = ab.LoadAsset(ResourceName, Type); 

// 异步加载,加载较大资源的时候使用
AssetBundle.LoadAssetAsync<GameObject>(assetName)  
// 参数指明资源类型 防止重名
Object obj = ab.LoadAssetAsync(resName,Type); 

// 加载AB包中所有的对象,不包含依赖的包
AssetBundle.LoadAllAssets();
// 异步加载全部资源
AssetBundle.LoadAllAssetsAsync();
// 加载资源及其子资源
AssetBundle.LoadAssetWithSubAssets();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

示例:

// 加载AB包
AssetBundle loadedAssetBundle = AssetBundle.LoadFromFile(path);

//加载单个游戏对象:
GameObject gameObject = loadedAssetBundle.LoadAsset<GameObject>(assetName);
//加载所有资源:
Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();
//异步加载资源:
AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName);
yield return request;
var loadedAsset = request.asset;
//异步加载所有资源:
AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync();
yield return request;
var loadedAssets = request.allAssets;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

4.5 卸载资源

为什么要卸载AssetBundle?

每次加载AB包都会有内存的占用,如果一直不卸载,随着游戏的运行不停加载使用到的新AB包,内存占用会越来越大。
哪些AssetBundle可以卸载?
当前运行时没有使用到的Assetbundle可以卸载。没有使用到的Assetbundle指的是内部的资源当前未被引用。
想要卸载AssetBundle,首先需要了解它在内存中的占用组成。AssetBundle的内存占用查看 5.AssetBundle的内存占用

// 会把 Bundle 卸载,但是已经从 Bundle 里加载出来的 资源 是不会被卸载的。
ab.Unload(false)

// 不但会卸载 Bundle,也会卸载已经从 Bundle 里加载出来的 所有资源,哪怕这些 资源 还被引用着。
ab.Unload(true)
  • 1
  • 2
  • 3
  • 4
  • 5

常用的卸载策略

需要通过引用计数管理ab包。
推荐的卸载方式:
加载ab包中资源时,引用计数+1,释放已加载的资源时,引用计数-1;
当引用计数为0时,调用AssetBundle.Unload(true) 彻底卸载ab包资源和从ab包中加载出来的资源;

5.AssetBundleManifest

AssetBundleManifest 文件内容:
主包的manifest下会存储有版本号、校验码(CRC)、所有其它包的相关信息(名称、依赖关系)
在这里插入图片描述
获取AssetBundleManifest:

// 编辑器下:
AssetBundleManifest manifest = BuildPipeline.BuildAssetBundles(outputPath, options, buildTarget);

// 正式环境下:
AssetBundle ab = AssetBundle.LoadFromFile(abPath);
AssetBundleManifest assetBundleManifest = ab.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

AssetBundleManifest API:

方法说明
string[] GetAllAssetBundles ()获取清单中的所有 AssetBundle。
string[] GetAllDependencies (string assetBundleName)获取给定 AssetBundle 的所有依赖 AssetBundle。
Hash128 GetAssetBundleHash (string assetBundleName)获取给定 AssetBundle 的哈希值。
string[] GetDirectDependencies (string assetBundleName)获取给定 AssetBundle 的直接依赖 AssetBundle。
string[] GetAllAssetBundlesWithVariant ()返回所有使用给定扩展名的assetbundlenames

6.AssetBundle的内存占用

在这里插入图片描述
黑色部分:www类本身占用内存,通过www接口加载AssetBundle才会有这部分内存,www对象保留了一份对WebStream数据(粉色部分)的引用。使用www =null 或者 www.dispose()释放。
其中www.dispose()会立即释放,而www = null会等待垃圾回收。释放www后WebStream的引用计数会相应减一。

橙色部分:官方称为WebStream数据,是数据真正的存储区域。当AssetBundle被加载进来后,这部分内存就被分配了。
它包含3个内容:压缩后的AssetBundle本身、解压后的资源以及一个解压缓冲区(图绿)。无论www(黑色部分)还是后面会提到的AssetBundle对象(粉色部分),都只是有一个结构指向了WebStream数据,从而能对外部提供操作真正资源数据的方法。而当www对象和AssetBundle对象释放时,WebStream数据的引用计数也会相应减1。当WebStream数据引用计数为0时,系统会自动释放。
但为了不频繁地开辟和销毁解压Buffer,其中绿色Decompression解压缓冲区Unity会至少保留一份。例如同时加载3个AssetBundle时,系统会生成3个Decompression Buffer,当解压完成后,系统会销毁两个。

粉色部分:AssetBundle对象,引用了橙色WebStream数据部分,并提供了从WebStream数据中加载资源的接口。通过AssetBundle.Unload(bool unloadAllLoadedObjects)释放。
如果调用AssetBundle.Unload(false),将释放AssetBundle对象本身,其对WebStream引用也将减少,从而可能引起WebStream释放,我们也就无法再通过接口或依赖关系从该AssetBundle加载资源。但已加载的资源还可以正常使用。
如果调用的是AssetBundle.Unload(true),不仅会释放WebStream部分,所有被加载出来的资源将被释放。

无论true或false,AssetBundle.Unload()都将销毁AssetBundle,销毁后调用该AssetBundle对象的任何方法都不会生效或产生报错,也就是说这个接口只能被调用一次,不能先调用unload(false)再调用unload(true)。

7.AB包资源加密

能被AssetStudio等解密软件解密到的资源

  • Resources文件夹下的资源文件
  • 被场景直接引用的资源文件
  • StreamingAssets下的资源文件(包含未加密的ab资源)

加密思路:

  1. 在构建完AB包后,可以将AB包中的内容以byte[]形式读取。
  2. 之后选用任意加密方式对该byte[]加密。
  3. 加密完后重新写入AB包中。
  4. AB包加密完成。

这样对AB包加密之后,如果使用AssetBundle.LoadFromFile()来加载加密的AB包是会报错的,因为Unity已经无法识别加密过后的内容了,这样也就防止了别人随意对AB包进行的读取和加载,保证了资源的安全性。

解密思路:

  1. 先以byte[]形式读取AB包中的内容。
  2. 之后使用对应的解密算法对该byte[]进行解密。
  3. 解密过后的byte[]通过AssetBundle.LoadFromMemory()来进行加载。
  4. AB包加载完成。
using System.IO;
using System.Security.Cryptography;
using UnityEngine;

/// <summary>
/// AB资源加解密
/// </summary>
public static class AssetBundleEncryption
{
    #region 基于偏移量加密
    private const int OffsetSize = 32; // 插入的字节数

    // 加密AssetBundle
    public static void EncryptAssetBundle(string inputPath, string outputPath)
    {
        byte[] originalBytes = File.ReadAllBytes(inputPath);
        byte[] offsetBytes = new byte[OffsetSize];

        // 随机生成插入的二进制数据(或使用特定标记)
        System.Random random = new System.Random();
        random.NextBytes(offsetBytes);

        byte[] encryptedBytes = new byte[originalBytes.Length + OffsetSize];
        System.Buffer.BlockCopy(offsetBytes, 0, encryptedBytes, 0, OffsetSize);
        System.Buffer.BlockCopy(originalBytes, 0, encryptedBytes, OffsetSize, originalBytes.Length);

        File.WriteAllBytes(outputPath, encryptedBytes);
        Debug.Log("加密完成: " + outputPath);
    }

    // 解密AssetBundle
    public static AssetBundle DecryptAssetBundle(string encryptedPath)
    {
        return AssetBundle.LoadFromFile(encryptedPath, 0, OffsetSize);
    }
    #endregion

    #region 基于AES加密
    private static readonly byte[] Key = new byte[32]; // 256位密钥
    private static readonly byte[] IV = new byte[16];  // 128位初始化向量

    static void AssetBundleAesEncryption()
    {
        // 生成随机密钥和初始化向量
        using (var rng = new RNGCryptoServiceProvider())
        {
            rng.GetBytes(Key);
            rng.GetBytes(IV);
        }
    }

    // 使用AES加密AssetBundle
    public static void AESEncryptAssetBundle(string inputPath, string outputPath)
    {
        byte[] originalBytes = File.ReadAllBytes(inputPath);
        byte[] encryptedBytes;

        using (Aes aes = Aes.Create())
        {
            aes.Key = Key;
            aes.IV = IV;

            ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

            using (MemoryStream ms = new MemoryStream())
            {
                using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                {
                    cs.Write(originalBytes, 0, originalBytes.Length);
                }
                encryptedBytes = ms.ToArray();
            }
        }

        using (FileStream fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
        {
            fs.Write(encryptedBytes, 0, encryptedBytes.Length);
        }

        Debug.Log("AES加密完成: " + outputPath);
    }

    // 解密并加载AES加密的AssetBundle
    public static AssetBundle AesEncryptedAssetBundle(string encryptedPath)
    {
        byte[] encryptedBytes = File.ReadAllBytes(encryptedPath);
        byte[] decryptedBytes;

        using (Aes aes = Aes.Create())
        {
            aes.Key = Key;
            aes.IV = IV;

            ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

            using (MemoryStream ms = new MemoryStream(encryptedBytes))
            {
                using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                {
                    using (MemoryStream decryptedStream = new MemoryStream())
                    {
                        cs.CopyTo(decryptedStream);
                        decryptedBytes = decryptedStream.ToArray();
                    }
                }
            }
        }

        string tempFilePath = Path.GetTempFileName();
        File.WriteAllBytes(tempFilePath, decryptedBytes);

        AssetBundle bundle = AssetBundle.LoadFromFile(tempFilePath);
        File.Delete(tempFilePath);

        // AssetBundle bundle = AssetBundle.LoadFromMemory(decryptedBytes);
        Debug.Log("AES解密并加载完成: " + encryptedPath);
        return bundle;
    }
    #endregion
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/975333
推荐阅读
相关标签
  

闽ICP备14008679号