从打包到加载的全流程详解,包含常见问题与最佳实践
AssetBundle(简称AB包)是Unity提供的一种资源打包格式,它可以将游戏资源(如预制体、贴图、音频、场景等)打包成独立的文件,用于实现:
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 普通AB包 | 包含预制体、材质、脚本引用等资源 | 大部分游戏资源 |
| 场景AB包 | 专门打包Unity场景文件 | 需要动态加载的场景 |
| StreamedSceneAssetBundle | 流式场景包,边下载边加载 | 大型开放世界场景 |
在打包前,需要为资源设置AssetBundle名称,有两种方式:
选中资源,在Inspector面板底部的AssetBundle下拉框中设置名称和后缀。
using UnityEditor;
using System.IO;
public class ABNameSetter
{
[MenuItem("Tools/Set AB Names")]
public static void SetABNames()
{
// 获取指定文件夹下的所有资源
string folderPath = "Assets/Resources/Prefabs";
string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { folderPath });
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
AssetImporter importer = AssetImporter.GetAtPath(assetPath);
// 设置AB包名称(小写,不含扩展名)
string abName = Path.GetFileNameWithoutExtension(assetPath).ToLower();
importer.assetBundleName = abName;
importer.assetBundleVariant = "ab"; // 可选后缀
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
using UnityEditor;
using System.IO;
public class ABBuilder
{
[MenuItem("Tools/Build AssetBundles")]
public static void BuildAllAB()
{
string outputPath = "Assets/StreamingAssets/AB";
// 确保输出目录存在
if (!Directory.Exists(outputPath))
Directory.CreateDirectory(outputPath);
// 核心打包API
BuildPipeline.BuildAssetBundles(
outputPath,
BuildAssetBundleOptions.ChunkBasedCompression, // LZ4压缩
BuildTarget.StandaloneWindows64
);
AssetDatabase.Refresh();
Debug.Log("AB包打包完成!");
}
}
| BuildAssetBundleOptions | 说明 | 使用场景 |
|---|---|---|
| None | 使用LZMA压缩,压缩率最高 | 对包体大小敏感,加载速度要求不高 |
| UncompressedAssetBundle | 不压缩 | 开发调试阶段 |
| ChunkBasedCompression | LZ4压缩,支持随机访问 | ⭐ 推荐用于生产环境 |
| ForceRebuildAssetBundle | 强制重新打包所有AB | 需要完全重建时使用 |
| DisableWriteTypeTree | 不写入TypeTree信息 | 减小包体,但降低兼容性 |
| DeterministicAssetBundle | 使用哈希ID而非随机ID | 增量更新必须开启 |
打包完成后会生成与AB包同名的.manifest文件,包含重要的依赖信息:
ManifestFileVersion: 0
CRC: 2422504565
Hashes:
AssetFileHash:
serializedVersion: 2
Hash: 8b1a9953c4611296a827abf8c47804d7
TypeTreeHash:
serializedVersion: 2
Hash: 7c2eb1e930d19e8c2d9e8d70cf7e8a2d
HashAppended: 0
ClassTypes:
- Class: 1
Script: {instanceID: 0}
- Class: 114
Script: {fileID: 11500000, guid: abc123, type: 3}
Assets:
- Assets/Prefabs/Player.prefab
Dependencies:
- materials.ab
- textures.ab
现象:多个AB包引用同一个资源,导致资源被重复打进多个包中,增大总包体。
原因:被引用的公共资源没有单独设置AB名称。
// 分析依赖,将公共资源单独打包
public static void AnalyzeAndSetDependencies()
{
string[] allBundles = AssetDatabase.GetAllAssetBundleNames();
Dictionary<string, List<string>> assetRefCount = new();
foreach (string bundle in allBundles)
{
string[] assets = AssetDatabase.GetAssetPathsFromAssetBundle(bundle);
foreach (string asset in assets)
{
string[] deps = AssetDatabase.GetDependencies(asset, true);
foreach (string dep in deps)
{
if (!assetRefCount.ContainsKey(dep))
assetRefCount[dep] = new List<string>();
if (!assetRefCount[dep].Contains(bundle))
assetRefCount[dep].Add(bundle);
}
}
}
// 被多个包引用的资源,单独打成公共包
foreach (var kvp in assetRefCount)
{
if (kvp.Value.Count > 1)
{
AssetImporter importer = AssetImporter.GetAtPath(kvp.Key);
if (importer != null && string.IsNullOrEmpty(importer.assetBundleName))
{
importer.assetBundleName = "common/shared_assets";
}
}
}
}
现象:加载AB包后,预制体上的脚本组件显示"Missing"。
原因:AB包只存储脚本引用(GUID),实际脚本代码在主工程中。脚本修改后GUID变化,或脚本不在构建中。
<!-- link.xml 示例 -->
<linker>
<assembly fullname="Assembly-CSharp">
<type fullname="YourNamespace.YourScript" preserve="all"/>
</assembly>
</linker>
现象:AB包中的材质显示为粉红色或Shader报错。
原因:Shader没有打进AB包,或Shader变体被裁剪。
// 预热Shader变体
ShaderVariantCollection svc = Resources.Load<ShaderVariantCollection>("ShaderVariants");
svc.WarmUp();
现象:Sprite图集被重复打包,或图集引用错误。
原因:SpriteAtlas和其中的Sprite被分别设置了AB名称。
现象:编辑器中正常,打包后找不到资源。
原因:路径大小写敏感、中文路径、特殊字符等问题。
现象:每次打包都是全量打包,非常慢。
原因:没有正确使用增量打包选项,或manifest文件被删除。
| 加载方式 | 同步/异步 | 来源 | 适用场景 |
|---|---|---|---|
| AssetBundle.LoadFromFile | 同步 | 本地文件 | ⭐ 本地资源首选 |
| AssetBundle.LoadFromFileAsync | 异步 | 本地文件 | 大资源加载 |
| AssetBundle.LoadFromMemory | 同步 | 内存字节 | 加密AB包 |
| AssetBundle.LoadFromStream | 同步 | 文件流 | 自定义读取逻辑 |
| UnityWebRequestAssetBundle | 异步 | 网络/本地 | ⭐ 远程下载首选 |
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class ABLoader : MonoBehaviour
{
private AssetBundleManifest manifest;
private Dictionary<string, AssetBundle> loadedBundles = new();
private string basePath;
void Start()
{
// 根据平台设置基础路径
string platformFolder = GetPlatformFolder();
basePath = Application.streamingAssetsPath + "/AB/" + platformFolder;
// 首先加载主Manifest
StartCoroutine(LoadManifest());
}
IEnumerator LoadManifest()
{
string manifestPath = basePath + "/" + GetPlatformFolder();
AssetBundle manifestBundle = AssetBundle.LoadFromFile(manifestPath);
if (manifestBundle != null)
{
manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
manifestBundle.Unload(false);
Debug.Log("Manifest加载成功!");
}
yield return null;
}
/// <summary>
/// 加载AB包及其所有依赖
/// </summary>
public IEnumerator LoadAssetBundleWithDependencies(string bundleName)
{
if (manifest == null)
{
Debug.LogError("Manifest未加载!");
yield break;
}
// 获取所有依赖
string[] dependencies = manifest.GetAllDependencies(bundleName);
// 先加载所有依赖
foreach (string dep in dependencies)
{
yield return LoadSingleBundle(dep);
}
// 再加载目标AB包
yield return LoadSingleBundle(bundleName);
}
private IEnumerator LoadSingleBundle(string bundleName)
{
if (loadedBundles.ContainsKey(bundleName))
{
Debug.Log($"AB包已加载: {bundleName}");
yield break;
}
string fullPath = basePath + "/" + bundleName;
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(fullPath);
yield return request;
if (request.assetBundle != null)
{
loadedBundles[bundleName] = request.assetBundle;
Debug.Log($"AB包加载成功: {bundleName}");
}
else
{
Debug.LogError($"AB包加载失败: {bundleName}");
}
}
/// <summary>
/// 从AB包加载资源
/// </summary>
public T LoadAsset<T>(string bundleName, string assetName) where T : Object
{
if (!loadedBundles.TryGetValue(bundleName, out AssetBundle bundle))
{
Debug.LogError($"AB包未加载: {bundleName}");
return null;
}
return bundle.LoadAsset<T>(assetName);
}
private string GetPlatformFolder()
{
#if UNITY_ANDROID
return "Android";
#elif UNITY_IOS
return "iOS";
#elif UNITY_WEBGL
return "WebGL";
#else
return "Windows";
#endif
}
}
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
public class WebABLoader : MonoBehaviour
{
public IEnumerator LoadFromWeb(string url, uint crc = 0)
{
// 使用缓存版本(推荐)
UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(
url,
1, // version - 用于缓存管理
crc // CRC校验(可选,0表示不校验)
);
request.SendWebRequest();
// 显示下载进度
while (!request.isDone)
{
Debug.Log($"下载进度: {request.downloadProgress * 100:F1}%");
yield return null;
}
if (request.result == UnityWebRequest.Result.Success)
{
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
Debug.Log("下载并加载成功!");
// 使用bundle...
}
else
{
Debug.LogError($"下载失败: {request.error}");
}
}
}
现象:预制体加载后材质丢失、贴图是紫色/白色。
原因:没有先加载AB包的依赖项。
必须先加载Manifest,获取依赖列表,按顺序加载所有依赖后再加载目标AB包。
// 正确的加载顺序
string[] deps = manifest.GetAllDependencies(targetBundle);
foreach (var dep in deps)
{
AssetBundle.LoadFromFile(path + dep); // 先加载依赖
}
AssetBundle.LoadFromFile(path + targetBundle); // 再加载目标
现象:报错"The AssetBundle 'xxx' can't be loaded because another AssetBundle with the same files is already loaded."
原因:试图重复加载已加载的AB包。
使用字典缓存已加载的AB包,加载前先检查是否已存在。
private Dictionary<string, AssetBundle> loadedBundles = new();
public AssetBundle LoadBundle(string name)
{
if (loadedBundles.TryGetValue(name, out var bundle))
return bundle; // 已加载,直接返回
bundle = AssetBundle.LoadFromFile(path + name);
loadedBundles[name] = bundle;
return bundle;
}
现象:PC上正常,Android上无法加载StreamingAssets中的AB包。
原因:Android的StreamingAssets在APK压缩包内,不能直接用File API访问。
// Android必须使用UnityWebRequest读取StreamingAssets
public IEnumerator LoadOnAndroid(string bundleName)
{
string path;
#if UNITY_ANDROID && !UNITY_EDITOR
// Android上StreamingAssets的正确路径
path = Application.streamingAssetsPath + "/" + bundleName;
// 或者使用 "jar:file://" + Application.dataPath + "!/assets/" + bundleName
using (UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(path))
{
yield return request.SendWebRequest();
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
}
#else
// 其他平台可以直接LoadFromFile
path = Application.streamingAssetsPath + "/" + bundleName;
AssetBundle bundle = AssetBundle.LoadFromFile(path);
yield return null;
#endif
}
现象:WebGL平台AB包加载失败或卡死。
原因:WebGL不支持同步加载,且必须使用UnityWebRequest。
现象:Instantiate后,克隆体的材质/贴图变成空白。
原因:AB包被Unload(true)后,所有从该包加载的资源都会被卸载。
// Unload参数说明:
bundle.Unload(false); // 只卸载AB包,保留已加载的资源(可能导致内存泄漏)
bundle.Unload(true); // 卸载AB包和所有从中加载的资源(谨慎使用)
// 最佳实践:使用引用计数管理
public class BundleRef
{
public AssetBundle Bundle;
public int RefCount = 0;
public void Retain() => RefCount++;
public void Release()
{
RefCount--;
if (RefCount <= 0)
{
Bundle.Unload(true);
}
}
}
现象:异步加载资源后使用时为null,或报空引用。
原因:没有正确等待异步加载完成就使用了资源。
// 方式1:协程等待
IEnumerator LoadAsync()
{
AssetBundleCreateRequest bundleReq = AssetBundle.LoadFromFileAsync(path);
yield return bundleReq; // 必须等待!
AssetBundleRequest assetReq = bundleReq.assetBundle.LoadAssetAsync<GameObject>(name);
yield return assetReq; // 必须等待!
GameObject prefab = assetReq.asset as GameObject;
Instantiate(prefab);
}
// 方式2:回调方式
IEnumerator LoadWithCallback(System.Action<GameObject> onLoaded)
{
// ... 加载代码 ...
onLoaded?.Invoke(loadedPrefab);
}
// 方式3:async/await(需要UniTask等库)
async UniTask<GameObject> LoadAsyncTask()
{
var bundle = await AssetBundle.LoadFromFileAsync(path);
var asset = await bundle.LoadAssetAsync<GameObject>(name);
return asset as GameObject;
}
当资源A引用了资源B,且A和B被打进了不同的AB包中,就会产生依赖关系。
prefab_player.ab
└── 依赖 → materials_common.ab
└── 依赖 → textures_shared.ab
| 策略 | 优点 | 缺点 |
|---|---|---|
| 按文件夹打包 | 简单,直观 | 可能产生冗余依赖 |
| 按功能模块打包 | 逻辑清晰,易于管理 | 需要良好的项目结构 |
| 自动分析依赖打包 | 最优化,无冗余 | 实现复杂 |
// 获取依赖的几种方式
// 1. 直接依赖(一级依赖)
string[] directDeps = manifest.GetDirectDependencies("mybundle.ab");
// 2. 所有依赖(包含依赖的依赖)- 推荐使用
string[] allDeps = manifest.GetAllDependencies("mybundle.ab");
// 3. 编辑器中分析资源依赖
string[] deps = AssetDatabase.GetDependencies(assetPath, recursive: true);
┌─────────────────────────────────────────────┐
│ Native Memory │
│ ├── AssetBundle对象 (AB包本身) │
│ ├── 序列化数据缓存 │
│ └── 资源原始数据 (贴图、网格等) │
├─────────────────────────────────────────────┤
│ Managed Memory │
│ ├── C#包装对象 (Texture2D, Mesh等) │
│ └── 实例化的GameObject │
└─────────────────────────────────────────────┘
| 方法 | 效果 | 使用场景 |
|---|---|---|
| bundle.Unload(false) | 仅释放AB包对象,保留已加载资源 | 资源需要持续使用时 |
| bundle.Unload(true) | 释放AB包和所有加载的资源 | 场景切换,完全清理 |
| Resources.UnloadUnusedAssets() | 卸载所有未引用的资源 | 配合Unload(false)使用 |
| AssetBundle.UnloadAllAssetBundles() | 卸载所有AB包 | 重新开始,完全清理 |
public class ABMemoryManager
{
private Dictionary<string, BundleInfo> bundles = new();
class BundleInfo
{
public AssetBundle Bundle;
public int RefCount;
public float LastAccessTime;
}
/// <summary>
/// 基于LRU策略的自动卸载
/// </summary>
public void AutoUnloadUnused(float maxIdleTime = 300f)
{
float currentTime = Time.time;
List<string> toUnload = new();
foreach (var kvp in bundles)
{
if (kvp.Value.RefCount <= 0 &&
currentTime - kvp.Value.LastAccessTime > maxIdleTime)
{
toUnload.Add(kvp.Key);
}
}
foreach (string name in toUnload)
{
bundles[name].Bundle.Unload(true);
bundles.Remove(name);
Debug.Log($"自动卸载闲置AB包: {name}");
}
}
}
/*
* 推荐架构:
*
* ┌──────────────────────────────────────────┐
* │ ResourceManager │ ← 对外接口层
* │ LoadPrefab() / LoadTexture() / ... │
* ├──────────────────────────────────────────┤
* │ AssetBundleManager │ ← AB包管理层
* │ LoadBundle() / UnloadBundle() │
* │ 引用计数 / 依赖管理 / 缓存策略 │
* ├──────────────────────────────────────────┤
* │ ManifestManager │ ← 清单管理层
* │ GetDependencies() / GetHash() │
* ├──────────────────────────────────────────┤
* │ DownloadManager │ ← 下载管理层
* │ Download() / CheckUpdate() │
* │ 断点续传 / 并行下载 / 进度回调 │
* └──────────────────────────────────────────┘
*/
| API | 说明 |
|---|---|
| BuildPipeline.BuildAssetBundles() | 核心打包方法 |
| AssetImporter.assetBundleName | 设置资源的AB包名 |
| AssetDatabase.GetDependencies() | 获取资源依赖 |
| AssetDatabase.GetAllAssetBundleNames() | 获取所有AB包名 |
| API | 说明 |
|---|---|
| AssetBundle.LoadFromFile[Async]() | 从本地文件加载 |
| AssetBundle.LoadFromMemory[Async]() | 从内存加载 |
| UnityWebRequestAssetBundle.GetAssetBundle() | 从网络/本地加载 |
| bundle.LoadAsset[Async]<T>() | 从AB包加载资源 |
| bundle.LoadAllAssets[Async]() | 加载AB包中所有资源 |
| manifest.GetAllDependencies() | 获取AB包所有依赖 |
| API | 说明 |
|---|---|
| bundle.Unload(false) | 只卸载AB包对象 |
| bundle.Unload(true) | 卸载AB包和所有资源 |
| Resources.UnloadUnusedAssets() | 卸载未使用资源 |
| AssetBundle.UnloadAllAssetBundles() | 卸载所有AB包 |