文章目录
- 1.AssetBundle 概念
- 2.AssetBundle 优势
- 3.AssetBundle 特性
- 4.AssetBundle 使用流程
- 4.1 分组
- 4.2 打包
- 4.3 加载包
- 4.4 加载资源
- 4.5 卸载资源
- 5.AssetBundleManifest
- 6.AssetBundle的内存占用
- 7.AB包资源加密
1.AssetBundle 概念
AssetBundle又称AB包,是Unity提供的一种用于存储资源的资源压缩包。
Unity中的AssetBundle系统是对资源管理的一种扩展,通过将资源分布在不同的AB包中可以最大程度地减少运行时的内存压力,可以动态地加载和卸载AB包,继而有选择地加载内容。
2.AssetBundle 优势
- AB包存储位置自定义,继而可放入可读可写的路径下便于实现热更新
- AB包自定义压缩方式,可以选择不压缩或选择LZMA和LZ4等压缩方式,减小包的大小,更快的进行网络传输。
- 资源可分布在不同的AB包中,最大程度减少运行时的内存压力, 可做到即用即加载,有选择的加载需要的内容。
- AB包支持后期进行动态更新,显著减小初始安装包的大小,非核心资源以AB包形式上传服务器,后期运行时动态加载,提高用户体验。
AssetBundle和Resources的比较
AssetBundle | Resources |
---|---|
资源可分布在多个包中 | 所有资源打包成一个大包 |
存储位置自定义灵活 | 必须存放在Resources目录下 |
压缩方式灵活(LZMA,LZ4) | 资源全部会压缩成二进制 |
支持后期进行动态更新 | 打包后资源只读无法动态更改 |
3.AssetBundle 特性
- AB包可以存储绝大部分Unity资源但无法直接存储C#脚本,所以代码的热更新需要使用Lua或者存储编译后的DLL文件。
- AB包不能重复进行加载,当AB包已经加载进内存后必须卸载后才能重新加载。
- 多个资源分布在不同的AB包可能会出现一个预制体的贴图等部分资源不在同一个包下,直接加载会出现部分资源丢失的情况,即AB包之间是存在依赖关系的,在加载当前AB包时需要一并加载其所依赖的包。
- 打包完成后,会自动生成一个主包(主包名称随平台不同而不同),主包的manifest下会存储有版本号、校验码(CRC)、所有其它包的相关信息(名称、依赖关系)
4.AssetBundle 使用流程
AssetBundle的使用流程比较繁琐,大致上分为五个阶段:分组、打包、加载包、加载资源、卸载包
4.1 分组
分组策略:
- 按逻辑实体分组
- 一个UI界面或者所有UI界面一个包(这个界面里面的贴图和布局信息一个包)
- 一个角色或者所有角色一个包(这个角色里面的模型和动画一个包)
- 所有的场景所共享的部分一个包(包括贴图和模型)
2.按照类型分组
所有声音资源打成一个包,shader打成一个包,模型打成一个包,材质打成一个包
3.按照使用分组
把在某一时间内使用的所有资源打成一个包。可以按照关卡分,一个关卡所需要的所有资源包括角色、贴图、声音等打成一个包。也可以按照场景分,一个场景所需要的资源一个包
- 注意事项:
- 经常更新的资源放在一个单独的包里面,跟不经常更新的包分离
- 把需要同时加载的资源放在一个包里面
- 可以把其他包共享的资源放在一个单独的包里面
- 把一些需要同时加载的小资源打包成一个包
- 如果对于一个同一个资源有两个版本,可以考虑通过后缀来区分,例如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);
}
此脚本将在 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();
① 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);
};
④ 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();
示例:
// 加载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;
4.5 卸载资源
为什么要卸载AssetBundle?
每次加载AB包都会有内存的占用,如果一直不卸载,随着游戏的运行不停加载使用到的新AB包,内存占用会越来越大。
哪些AssetBundle可以卸载?
当前运行时没有使用到的Assetbundle可以卸载。没有使用到的Assetbundle指的是内部的资源当前未被引用。
想要卸载AssetBundle,首先需要了解它在内存中的占用组成。AssetBundle的内存占用查看 5.AssetBundle的内存占用
// 会把 Bundle 卸载,但是已经从 Bundle 里加载出来的 资源 是不会被卸载的。
ab.Unload(false)
// 不但会卸载 Bundle,也会卸载已经从 Bundle 里加载出来的 所有资源,哪怕这些 资源 还被引用着。
ab.Unload(true)
常用的卸载策略
需要通过引用计数管理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");
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资源)
加密思路:
- 在构建完AB包后,可以将AB包中的内容以byte[]形式读取。
- 之后选用任意加密方式对该byte[]加密。
- 加密完后重新写入AB包中。
- AB包加密完成。
这样对AB包加密之后,如果使用AssetBundle.LoadFromFile()来加载加密的AB包是会报错的,因为Unity已经无法识别加密过后的内容了,这样也就防止了别人随意对AB包进行的读取和加载,保证了资源的安全性。
解密思路:
- 先以byte[]形式读取AB包中的内容。
- 之后使用对应的解密算法对该byte[]进行解密。
- 解密过后的byte[]通过AssetBundle.LoadFromMemory()来进行加载。
- 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
}