Unity中AssetBundle使用整理(一)
一、AssetBundle 概述
AssetBundle 是 Unity 用于存储和加载游戏资源(如模型、纹理、预制体、音频等)的一种文件格式。它允许开发者将游戏资源打包成独立的文件,在运行时动态加载,从而实现资源的按需加载、更新以及减小初始安装包大小等功能。
用途:
减小初始安装包大小:将部分资源(如非首发场景资源、后期更新资源)以 AssetBundle 形式打包,不在初始安装包中包含,降低安装包体积。
资源动态更新:在游戏发布后,可以通过下载新的 AssetBundle 文件来更新游戏资源,无需发布新的游戏版本,实现快速迭代和内容更新。
资源复用:多个场景或项目可以共享相同的 AssetBundle 资源,提高资源利用效率。
二、AssetBundle 的创建
1. 资源标记
1.在 Unity 编辑器中,通过 Inspector 面板为需要打包进 AssetBundle 的资源设置 AssetBundle 名称和变体(Variant)。变体可用于区分不同平台、画质等级等版本的资源。
2.通过脚本方式设置。
代码:使用AssetImporter设置ab包名和变体名,可以设置没有提前配置的包名和变体名
public static class AssetBundleSetter{ [MenuItem(\"Tools/Set AssetBundle Name\")] static void SetAssetBundleName() { // 获取资源路径 string assetPath = \"Assets/Res/buffIcon_Speed.png\"; // 获取资源的 AssetImporter AssetImporter importer = AssetImporter.GetAtPath(assetPath); if (importer != null) { // 设置 AssetBundle 名称和变体 importer.assetBundleName = \"obj1\"; // 主名称 importer.assetBundleVariant = \"unity3d\"; // 变体后缀 // 保存设置 importer.SaveAndReimport(); Debug.Log($\"已设置: {assetPath} -> {importer.assetBundleName}.{importer.assetBundleVariant}\"); } else { Debug.LogError($\"资源不存在: {assetPath}\"); } AssetDatabase.Refresh(); // 刷新数据库 }}
注:变体(Variant)是什么?
变体是与其一起存储的 AssetBundle 的选项或子类,允许同一组资源的不同变体(如高清/标清材质、多语言文本)共享相同的加载逻辑,运行时动态选择合适版本,同一Bundle名称下,要么全部资源使用变体,要么全部不使用变体,否则会导致资源冲突或加载错误。
2.构建 AssetBundle
使用BuildPipeline.BuildAssetBundles方法来构建 AssetBundle。此方法需要指定输出路径、构建选项和目标平台。
BuildAssetBundleOptions的主要参数信息如下:
此选项使用称为 LZ4 的压缩方法,因此压缩文件大小比 LZMA 更大,但不像 LZMA 那样需要解压缩整个包才能使用捆绑包。LZ4 使用基于块的算法,允许按段或“块”加载 AssetBundle。解压缩单个块即可使用包含的资源,即使 AssetBundle 的其他块未解压缩也不影响。
更多选项信息:BuildAssetBundleOptions - Unity 脚本 API
注:
LZMA 压缩(BuildAssetBundleOptions.None)
特点:文件体积最小,但使用资源前需解压整个包,适用于首次远程下载,首次解压完成后,将使用 LZ4 压缩技术在磁盘上重新压缩ab包,并存入本地缓存目录中,后续再加载,都会加载LZ4压缩的ab包。
使用场景:资源包高度关联且需整体加载(如完整场景、角色资源)。
LZ4 压缩(缓存与本地优化)
特点:按需加载资源,无需全包解压,性能更快。
自动转换:通过 UnityWebRequestAssetBundle 下载的 LZMA 包会转为 LZ4 并本地缓存。
手动转换:其他方式下载的 LZMA 包需调用 AssetBundle.RecompressAssetBundleAsync 转 LZ4。
核心原则:远程用 LZMA(省带宽),本地用 LZ4(提性能)。
BuildTarget的参数信息如下:
更多选项信息:BuildTarget - Unity 脚本 API
注:EditorUserBuildSettings.activeBuildTarget是获取当前的在BuildSetting中设置的平台,利用此API可以在打包时直接打此设置平台的ab包。
代码:
public class BuildAssetBundle : MonoBehaviour{ [MenuItem(\"Tools/Build AssetBundles\")] static void BuildAllAssetBundles() { string assetBundleDirectory = \"Assets/AssetBundles\"; if (!Directory.Exists(assetBundleDirectory)) { Directory.CreateDirectory(assetBundleDirectory); } BuildPipeline.BuildAssetBundles(assetBundleDirectory, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows); }}
注:以上Assetbundle操作的脚本需放在Editor文件夹,否则打包时会报错。
准备资源并标记包名和变体名(可选)如下:
执行打包,打包完成后,除了定义的每个 AssetBundle 文件(如 scene.bundle)及其对应的 .manifest 文件外,还会生成两个全局管理文件,分别是主清单文件(.bundle) 文件和一个 元数据文件(.manifest) 文件,(清单ab包以其所在的目录(构建 AssetBundle 的目录)命名)。
注:
主清单文件(.bundle):
是 Unity 引擎能直接识别的二进制格式,包含 AssetBundleManifest 对象,存储所有 AssetBundle 的依赖关系和哈希值。
必须通过 AssetBundle.LoadFromFile 加载,用于运行时依赖解析。
元数据文件(.manifest):
是纯文本文件,记录所有 AssetBundle 的名称、依赖关系和哈希值,方便开发者手动查看内容。
不参与运行时逻辑,仅用于调试或热更新时的版本对比。
3.资源分组
1.逻辑实体分组
逻辑实体分组是指根据资源所代表的项目功能部分将资源分配给 AssetBundle。这包括各种不同部分,比如用户界面、角色、环境以及在应用程序整个生命周期中可能经常出现的任何其他内容。非常适合于可下载内容 (DLC),因为通过这种方式将所有内容隔离后,可以对单个实体进行更改,而无需下载其他未更改的资源。
逻辑实体分组例如:
1.用户界面屏幕的所有纹理和布局数据。
2.一个/一组角色的所有模型和动画。
3.在多个关卡之间共享的景物的纹理和模型。
2.类型分组
将相似类型的资源(例如音频轨道或语言本地化文件)分配到单个 AssetBundle。
3.并发内容分组
将需要同时加载和使用的资源放到一个AssetBundle。可以将这些类型的捆绑包用于基于关卡的游戏(其中每个关卡包含完全独特的角色、纹理、音乐等)。最常见的用例是针对基于场景的AssetBundle包。在此分配策略中,每个场景AssetBundle包应包含大部分或全部场景依赖项。
注:
按更新频率分组
频繁更新资源与稳定资源拆分为独立ab包,减少冗余下载。
按加载关联性分组
同时加载的资源(如模型+纹理+动画)合并到同一个ab包。
依赖管理
多ab包共享的依赖项,独立为共享ab包,避免重复加载。
按需分包
不可能同时加载的资源(如标清/高清资源)拆分到不同ab包。
拆分冗余内容
若 AssetBundle 中超过 50% 的资源不常同时使用,拆分成更小包。
合并小型高频包
将多个小型(5~10 个资源以内)且常共用的ab包合并,减少加载次数。
变体处理多版本
同一资源的不同版本(如多语言、设备适配)使用 AssetBundle 变体 管理。
核心原则:
减少冗余(依赖、版本、低频资源)
提升加载效率(按需加载、合并高频包)
简化维护(变体、分组策略)
三、AssetBundle 的加载与卸载
准备资源
创建一个Cube、一个材质球、一张纹理贴图
场景中显示如下,使用这些资源来测试加载卸载的API
1.加载本地ab包
1.同步加载:
1.LoadFromFile:从磁盘上的文件同步加载 AssetBundle,是加载 AssetBundle 的最快方法, 在lzma压缩的情况下,数据将被解压缩到内存中,可以直接从磁盘读取未压缩和块压缩的ab包。
此API有三个重载,
参数的含义:
代码:
void ABLoadFromFile() { var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, \"AssetBundles/obj1\")); if (myLoadedAssetBundle == null) { Debug.Log(\"加载AB包失败!\"); } else { Debug.Log(\"加载AB包成功!\"); } }
结果:
注:
1.crc参数如何使用?
如果校验失败,抛出的错误会带有计算的crc32的值,如下:
将此数值在加载时传入,
var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, \"AssetBundles/obj1\"), 0x6789); myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, \"AssetBundles/obj1\"), 0xa56b8935); if (myLoadedAssetBundle == null) { Debug.Log(\"加载AB包失败!\"); } else { Debug.Log(\"加载AB包成功!\"); }
结果:
2.offset参数如何使用?
offset 是 从文件起始位置跳过的字节数,用于指定 AssetBundle 数据在文件中的起始位置。它的核心作用是处理 非标准文件结构,即当 AssetBundle 数据并不位于文件开头时,通过 offset 跳过无关数据,直接定位到 AssetBundle 的真实数据位置。使用场景如下:
1.文件包含自定义头信息。
2.需要从复合文件中提取特定 AssetBundle。
3.处理加密或分段存储的 AssetBundle。
例:读取加密AB包
[加密头(64字节)][加密的AssetBundle数据]
string path = \"combined.bundle\";ulong offset = 1024; // Bundle1 的结束位置AssetBundle bundle = AssetBundle.LoadFromFile(path, 0, offset);
2.LoadFromMemory:
此函数有两个重载
参数含义:
代码:
void ABLoadFromMemory() { // 读取本地文件到字节数组 string path = Path.Combine(Application.dataPath, \"AssetBundles/obj1\"); byte[] data = File.ReadAllBytes(path); // 加载 AssetBundle var myLoadedAssetBundle = AssetBundle.LoadFromMemory(data); if (myLoadedAssetBundle == null) { Debug.Log(\"加载AB包失败!\"); } else { Debug.Log(\"加载AB包成功!\"); } }
结果:
3.LoadFromStream:从托管 Stream 同步加载 AssetBundle,lzma 压缩数据被解压缩到内存中,而未压缩和块压缩的ab包则直接从 Stream 中读取。
此API有三个重载
参数含义:
代码:
void LoadFromStream() { var fileStream = new FileStream(Path.Combine(Application.dataPath, \"AssetBundles/obj1\"), FileMode.Open, FileAccess.Read); var myLoadedAssetBundle = AssetBundle.LoadFromStream(fileStream); if (myLoadedAssetBundle == null) { Debug.Log(\"加载AB包失败!\"); } else { Debug.Log(\"加载AB包成功!\"); } }}
结果:
注:
1.managedReadBufferSize怎么设置?
代码:
// 设置缓冲区为 64KB(通常默认值即可) using (FileStream fs = new FileStream(Path.Combine(Application.dataPath, \"AssetBundles/obj1\"), FileMode.Open,FileAccess.Read)) { AssetBundle myLoadedAssetBundle = AssetBundle.LoadFromStream(fs, 0, 65536); // 64KB 缓冲区 if (myLoadedAssetBundle == null) { Debug.Log(\"加载AB包失败!\"); } else { Debug.Log(\"加载AB包成功!\"); } }
结果:
2.优化 AssetBundle 数据加载,Stream 对象强制要求如下:
1.数据起始位置:AssetBundle 数据必须从Stream的 Position = 0 开始,Unity 在加载 AssetBundle 数据之前会自动将Stream位置重置为 0。
2.基础能力:Unity 假定Stream中的读取位置不会被任何其他进程更改。这允许 Unity 进程从Stream中读取数据,而无需在每次读取之前调用 Seek()。Stream必须支持 CanRead(可读)和 CanSeek(可定位)。
3.线程安全:流的 Seek() 和 Read() 需支持多线程调用(Unity 可能从非主线程访问)。
4.越界处理:读取超过 AssetBundle 数据末尾时,必须返回实际读取字节数(不抛出异常)。
从数据末尾读取时返回 0 字节且不报错。
3.为了减少从本机代码到托管代码的调用次数,使用缓冲区大小为 managedReadBufferSize 的缓冲读取器从 Stream 中读取数据。
4.缓冲区(managedReadBufferSize)优化:
1.性能影响:缓冲区大小直接影响加载效率(尤其移动端),需根据项目实测调整。
2.推荐值范围:测试 8KB/16KB/32KB/64KB/128KB,观察性能变化。
3.大缓冲区适用场景:
压缩的 AssetBundle、Bundle 含大型资源(如场景、高清纹理)、资源按顺序加载。
4.小缓冲区适用场景:
未压缩的 AssetBundle、Bundle 含大量小资源(如图标、配置表)、资源随机加载(需频繁定位)。
2.异步加载:
1.LoadFromFileAsnyc:与LoadFromFile类似,但此函数是异步的。
代码:
IEnumerator ABLoadFromFileAsync() { var bundleLoadRequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.dataPath, \"AssetBundles/obj1\")); yield return bundleLoadRequest; var myLoadedAssetBundle = bundleLoadRequest.assetBundle; if (myLoadedAssetBundle == null) { Debug.Log(\"加载AB包失败!\"); } else { Debug.Log(\"加载AB包成功!\"); } yield break; }
结果:
2.LoadFromMemoryAsnyc:与LoadFromMemory类似,但此函数是异步的。
代码:
IEnumerator ABLoadFromMemoryAsync() { // 读取本地文件到字节数组 string path = Path.Combine(Application.dataPath, \"AssetBundles/obj1\"); byte[] data = File.ReadAllBytes(path); var bundleLoadRequest = AssetBundle.LoadFromMemoryAsync(data); yield return bundleLoadRequest; var myLoadedAssetBundle = bundleLoadRequest.assetBundle; if (myLoadedAssetBundle == null) { Debug.Log(\"加载AB包失败!\"); } else { Debug.Log(\"加载AB包成功!\"); } yield break; }
结果:
3.LoadFromStreamAsnyc:与LoadFromStream类似,但此函数是异步的。
代码:
IEnumerator LoadFromStreamAsync() { // 创建可读可定位的流 using (FileStream fs = new FileStream(Path.Combine(Application.dataPath, \"AssetBundles/obj1\"), FileMode.Open, FileAccess.Read)) { // 异步加载 AssetBundleCreateRequest bundleLoadRequest = AssetBundle.LoadFromStreamAsync(fs); // 等待加载完成 yield return bundleLoadRequest; var myLoadedAssetBundle = bundleLoadRequest.assetBundle; if (myLoadedAssetBundle == null) { Debug.Log(\"加载AB包失败!\"); } else { Debug.Log(\"加载AB包成功!\"); } } }
结果:
2.加载Web端ab包
1.基础
获取 AssetBundle:
方法1:GetAssetBundle(string url, int version)
通过 url(本地路径或远程 URL)和版本号下载指定 AssetBundle。
方法2:UnityWebRequestAssetBundle + DownloadHandlerAssetBundle
使用专用处理器异步下载并解析 AssetBundle。
核心流程:URL 请求 → 下载 → 解析为 AssetBundle
2.使用
首先搭建一个用于测试的web服务器,步骤如下:
使用PhpStudy搭建Web测试服务器-CSDN博客
然后将ab包放到web服务器的文件夹中,
最后将ab包下载到本地并加载,代码如下:
IEnumerator ABLoadUnityRequest() { string url = \"http://localhost:88/Test/obj1\"; var request = UnityEngine.Networking.UnityWebRequestAssetBundle.GetAssetBundle(url, 0); yield return request.SendWebRequest(); AssetBundle bundle = UnityEngine.Networking.DownloadHandlerAssetBundle.GetContent(request); if (bundle == null) { Debug.Log(\"加载AB包失败!\"); } else { Debug.Log(\"加载AB包成功!\"); } }
结果:
3.从AssetBundle加载资源
1.加载资源:
操作:从已加载的 AssetBundle 对象调用 LoadAsset(string assetName)。
T:资源类型(如 GameObject、Texture)。
assetName:资源在包内的名称(区分大小写)。
(1).LoadAsset:加载单个游戏对象
GameObject heroPrefab = myLoadedAssetBundle.LoadAsset(\"obj1\");if (heroPrefab == null){ Debug.Log(\"加载资产失败!\");}else{ Debug.Log(\"加载资产成功!\");}
结果:
(2).LoadAllAssets:加载所有资源
代码:
// 加载全部资源 GameObject[] heroPrefab = myLoadedAssetBundle.LoadAllAssets(); if (heroPrefab == null) { Debug.Log(\"加载资产失败!\"); } else { Debug.Log(\"加载资产成功!\"); }
结果:
(3).LoadAssetAsync
代码:
IEnumerator AssetLoadAssetAsync() { var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, \"AssetBundles/obj1\")); if (myLoadedAssetBundle != null) { AssetBundleRequest request = myLoadedAssetBundle.LoadAssetAsync(\"obj1\"); yield return request; var loadedAsset = request.asset; if (loadedAsset == null) { Debug.Log(\"加载资产失败!\"); } else { Debug.Log(\"加载资产成功!\"); } } }
结果:
(4).LoadAllAssetsAsync
代码:
IEnumerator AssetLoadAllAssetsAsync() { var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, \"AssetBundles/obj1\")); if (myLoadedAssetBundle != null) { // 异步加载全部资源 AssetBundleRequest request = myLoadedAssetBundle.LoadAllAssetsAsync(); yield return request; var loadedAssets = request.allAssets; if (loadedAssets == null) { Debug.Log(\"加载资产失败!\"); } else { Debug.Log(\"加载资产成功!\"); } } }
结果:
2.使用资源:
加载后的对象与 Unity 常规资源一致,可直接操作。
如:使用Instantiate ( gameObjectFromAssetBundle) 实例化到场景。
代码:
// 加载资源(如名为 \"obj1\" 的预制体) GameObject heroPrefab = myLoadedAssetBundle.LoadAsset(\"obj1\"); Instantiate(heroPrefab);
结果:
4.加载AssetBundle资源清单
1.加载清单
代码:
void LoadManifest() { string manifestFilePath = Path.Combine(Application.dataPath, \"AssetBundles/AssetBundles\"); AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath); AssetBundleManifest manifest = assetBundle.LoadAsset(\"AssetBundleManifest\"); if (manifest == null) { Debug.Log(\"加载清单失败!\"); } else { Debug.Log(\"加载清单成功!\"); } }
结果:
2.通过清单加载依赖
将obj1包中的材质,命名为obj2
这样打包之后,清单文件里就有依赖了
代码:
string manifestFilePath = Path.Combine(Application.dataPath, \"AssetBundles/AssetBundles\"); AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath); AssetBundleManifest manifest = assetBundle.LoadAsset(\"AssetBundleManifest\"); string[] dependencies = manifest.GetAllDependencies(\"obj1\"); //传递想要依赖项的捆绑包的名称。 foreach (string dependency in dependencies) { AssetBundle assetBundle2 = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, \"AssetBundles/\" + dependency)); if (assetBundle2 == null) { Debug.Log(\"加载资源失败!\"); } else { Debug.Log(\"加载清单成功!\"); } }
结果:
5.管理已加载的 AssetBundle
1.核心问题
资源不会自动卸载:Unity 不会主动清理已加载的 AssetBundle 资源,需手动管理卸载时机。
错误卸载后果:内存泄漏(残留未卸载资源)、资源丢失(如纹理变粉红或消失)。
2.AssetBundle.Unload(bool) 参数区别
优先使用 Unload(true):
1.明确卸载时机:在关卡切换、加载界面时统一卸载。
2.引用计数:记录资源引用次数,仅当所有依赖对象不再使用时卸载。
避免 Unload(false) 的陷阱:
1.手动清理残留引用:
//清理GameAsset内存,包括材质、贴图、模型、声音、已实例化的Prefab等, Resources.UnloadAsset(gameAssetName); //清理GameObject内存,包括未实例化的Prefab,Object.Destroy可以删除层级视图面板上的游戏对象, //在托管堆中将其置未null,在GC时只会回收托管堆中的对象,并没有释放实际的Native中的内存, Resources.UnloadUnusedAssets(prefabName);
2.非附加式加载场景:
// 销毁旧场景并清理资源,Unity会在每次切换场景时在底层进行Native无用内存回收SceneManager.LoadScene(\"NewScene\", LoadSceneMode.Single);
四、AssetBundle 的依赖管理
AssetBundle的依赖管理是确保资源正确加载和避免冗余的关键机制。
1. 依赖关系的自动记录
当资源 A(如场景)引用了资源 B(如材质或贴图),且 A 和 B 分别被打包到不同的 AssetBundle 中,Unity 会自动记录这种依赖关系。
示例:
scene.bundle(场景)引用了 materials.bundle(材质)。
Unity 会记录 scene.bundle 依赖 materials.bundle。
2.自定义配置文件记录依赖关系
使用json格式定义配置文件
[ \"charactersbundle\": { \"md5\": \"e10adc3949ba59abbe56e057f20f883e\", \"dependencies\": [\"materials.bundle\"] }]
3.运行时加载依赖的流程
(1)加载自定义配置文件
public CustomManifest LoadCustomManifest(string jsonPath){ string json = File.ReadAllText(jsonPath); return JsonUtility.FromJson(json);}
(2)递归加载依赖项
string[] dependencies = LoadCustomManifest(\"characters.bundle\").charactersbundle.dependencies;foreach (string dep in dependencies) { AssetBundle.LoadFromFile(Path.Combine(path, dep));}
(3)加载目标ab包
AssetBundle sceneBundle = AssetBundle.LoadFromFile(\"scene.bundle\");GameObject scene = sceneBundle.LoadAsset(\"MainScene\");
4. 依赖管理的优化策略
共享依赖包:
将高频引用的资源(如通用材质、Shader)打包到独立 AssetBundle(如 common.bundle),供多个主 AssetBundle 共享。优点:避免重复加载,减少内存占用。
变体(Variants):
同一资源的不同版本(如高清/标清贴图)使用变体后缀(如 textures.hd.bundle 和 textures.sd.bundle),通过代码动态切换。
// 当前激活的变体类型private static string activeVariant = \"hd\";// 单个Bundle加载private static IEnumerator LoadBundle(string bundleName){ string path = GetBundlePath(bundleName); using (UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(path)) { yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request); loadedBundles[bundleName] = bundle; } }}// 获取带变体的完整路径private static string GetBundlePath(string bundleName){ #if UNITY_EDITOR return $\"file://{Application.dataPath}/../AssetBundles/{bundleName}.{activeVariant}\"; #else return $\"{Application.streamingAssetsPath}/{bundleName}.{activeVariant}\"; #endif}
按需分块:
将大型资源(如场景)拆分为多个 AssetBundle,按需加载。
五、AssetBundle 的版本控制
版本控制是确保资源热更新和客户端同步的关键机制。
1. MD5校验
生成机制:Unity 在打包 AssetBundle 时,为每个包生成唯一的 MD5码,保存在自定义配置文件中。
远程校验:将本地MD5码与服务器上的最新MD5码对比,判断是否需要更新。
2. 版本号管理
版本标识:使用本地版本号与服务器版本号对比,确定是否更新。
增量更新:仅下载MD5码变化的 AssetBundle,减少流量消耗。
3.依赖链版本控制
递归更新:若依赖的ab包(如 textures.bundle)更新,需同时更新此包依赖及其依赖链。
4.本地版本缓存
校验更新:启动时加载本地缓存,与服务器版本对比。
5.错误处理与回退
下载失败:
记录失败次数,达到阈值后回退到旧版本或提示用户重试。
版本冲突:
若更新后依赖关系不兼容(如父包未更新),回滚到上一稳定版本。
参考:
《Unity3D游戏开发第三版》
AssetBundle - Unity 手册
AssetBundle 简介 - 2019.4 - Unity Learn
资源、资源和 AssetBundle - Unity Learn
AssetBundle 简介 - 2019.4 - Unity Learn