🎮 Unity AssetBundle 完整指南

从打包到加载的全流程详解,包含常见问题与最佳实践

📦 AssetBundle 基础概念

什么是 AssetBundle?

AssetBundle(简称AB包)是Unity提供的一种资源打包格式,它可以将游戏资源(如预制体、贴图、音频、场景等)打包成独立的文件,用于实现:

AB包的组成结构

📁 AB包文件结构

  • Header:包含AB包的标识、版本、压缩方式等元信息
  • Data Segment:实际的序列化资源数据
  • Resource Files:原始资源文件(如纹理、音频的原始数据)

AB包的类型

类型 说明 适用场景
普通AB包 包含预制体、材质、脚本引用等资源 大部分游戏资源
场景AB包 专门打包Unity场景文件 需要动态加载的场景
StreamedSceneAssetBundle 流式场景包,边下载边加载 大型开放世界场景

🔨 打包流程详解

Step 1: 资源标记(设置AssetBundle名称)

在打包前,需要为资源设置AssetBundle名称,有两种方式:

方式一:Inspector面板手动设置

选中资源,在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();
    }
}

Step 2: 执行打包

1
收集资源
2
分析依赖
3
序列化
4
压缩
5
生成Manifest
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包打包完成!");
    }
}

Step 3: 打包选项详解

BuildAssetBundleOptions 说明 使用场景
None 使用LZMA压缩,压缩率最高 对包体大小敏感,加载速度要求不高
UncompressedAssetBundle 不压缩 开发调试阶段
ChunkBasedCompression LZ4压缩,支持随机访问 ⭐ 推荐用于生产环境
ForceRebuildAssetBundle 强制重新打包所有AB 需要完全重建时使用
DisableWriteTypeTree 不写入TypeTree信息 减小包体,但降低兼容性
DeterministicAssetBundle 使用哈希ID而非随机ID 增量更新必须开启

Step 4: Manifest文件解析

打包完成后会生成与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

⚠️ 打包常见坑与解决方案

🔥 坑1:资源重复打包

现象:多个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";
            }
        }
    }
}

🔥 坑2:脚本丢失(Missing Script)

现象:加载AB包后,预制体上的脚本组件显示"Missing"。

原因:AB包只存储脚本引用(GUID),实际脚本代码在主工程中。脚本修改后GUID变化,或脚本不在构建中。

✅ 解决方案

  • 确保脚本在正确的Assembly中,并包含在构建中
  • 使用link.xml防止脚本被IL2CPP裁剪
  • 不要随意修改脚本的namespace和类名
  • 使用HybridCLR等热更新方案时,确保元数据正确补充
<!-- link.xml 示例 -->
<linker>
    <assembly fullname="Assembly-CSharp">
        <type fullname="YourNamespace.YourScript" preserve="all"/>
    </assembly>
</linker>

🔥 坑3:Shader丢失/变粉色

现象:AB包中的材质显示为粉红色或Shader报错。

原因:Shader没有打进AB包,或Shader变体被裁剪。

✅ 解决方案

  1. 将Shader单独打成AB包,或添加到Always Included Shaders
  2. 使用ShaderVariantCollection收集并保留需要的Shader变体
  3. Graphics Settings中配置Shader剥离规则
// 预热Shader变体
ShaderVariantCollection svc = Resources.Load<ShaderVariantCollection>("ShaderVariants");
svc.WarmUp();

🔥 坑4:图集打包问题

现象:Sprite图集被重复打包,或图集引用错误。

原因:SpriteAtlas和其中的Sprite被分别设置了AB名称。

✅ 解决方案

  • 只给SpriteAtlas设置AB名称,不要给单独的Sprite设置
  • 或者反过来,只给Sprite设置,让图集自动包含
  • 使用Include in Build选项控制图集是否打入包

🔥 坑5:打包后资源路径变化

现象:编辑器中正常,打包后找不到资源。

原因:路径大小写敏感、中文路径、特殊字符等问题。

✅ 解决方案

  • AB包名称统一使用小写字母
  • 避免使用中文、空格、特殊字符
  • 使用Path.ToLower()统一处理路径

🔥 坑6:增量打包失效

现象:每次打包都是全量打包,非常慢。

原因:没有正确使用增量打包选项,或manifest文件被删除。

✅ 解决方案

  • 保留上次打包生成的所有文件(包括.manifest)
  • 使用DeterministicAssetBundle选项
  • 不要使用ForceRebuildAssetBundle(除非需要强制重建)

📥 加载流程详解

加载方式对比

加载方式 同步/异步 来源 适用场景
AssetBundle.LoadFromFile 同步 本地文件 ⭐ 本地资源首选
AssetBundle.LoadFromFileAsync 异步 本地文件 大资源加载
AssetBundle.LoadFromMemory 同步 内存字节 加密AB包
AssetBundle.LoadFromStream 同步 文件流 自定义读取逻辑
UnityWebRequestAssetBundle 异步 网络/本地 ⭐ 远程下载首选

完整加载流程

1
加载Manifest
2
解析依赖
3
加载依赖AB
4
加载目标AB
5
加载Asset
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
    }
}

使用UnityWebRequest加载(支持远程)

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}");
        }
    }
}

🚨 加载常见坑与解决方案

🔥 坑1:依赖未加载导致资源异常

现象:预制体加载后材质丢失、贴图是紫色/白色。

原因:没有先加载AB包的依赖项。

✅ 解决方案

必须先加载Manifest,获取依赖列表,按顺序加载所有依赖后再加载目标AB包。

// 正确的加载顺序
string[] deps = manifest.GetAllDependencies(targetBundle);
foreach (var dep in deps)
{
    AssetBundle.LoadFromFile(path + dep); // 先加载依赖
}
AssetBundle.LoadFromFile(path + targetBundle); // 再加载目标

🔥 坑2:重复加载同一AB包

现象:报错"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;
}

🔥 坑3:Android路径问题

现象: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
}

🔥 坑4:WebGL特殊限制

现象:WebGL平台AB包加载失败或卡死。

原因:WebGL不支持同步加载,且必须使用UnityWebRequest。

✅ 解决方案

  • WebGL必须使用异步加载(协程/async-await)
  • 使用UnityWebRequestAssetBundle
  • 注意跨域问题(CORS),服务器需配置正确的响应头
  • AB包建议使用LZ4压缩,不要用LZMA

🔥 坑5:实例化后资源引用丢失

现象: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);
        }
    }
}

🔥 坑6:异步加载时序问题

现象:异步加载资源后使用时为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);

🧠 内存管理与卸载

内存模型

AB包加载后的内存分布

┌─────────────────────────────────────────────┐
│  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}");
        }
    }
}

⚠️ 内存泄漏常见原因

  • 使用Unload(false)后没有手动清理资源引用
  • 事件监听没有正确移除
  • 静态变量持有资源引用
  • 协程没有正确停止

最佳实践总结

打包最佳实践

  1. 重要公共资源单独打包,避免重复
  2. 推荐使用LZ4压缩(ChunkBasedCompression)
  3. 推荐开启DeterministicAssetBundle支持增量更新
  4. 按功能/场景划分AB包粒度
  5. Shader单独打包或加入Always Included
  6. AB包命名使用小写,避免特殊字符
  7. 建立资源版本管理和对比系统

加载最佳实践

  1. 重要先加载Manifest,正确处理依赖
  2. 重要缓存已加载的AB包,避免重复加载
  3. 推荐大资源使用异步加载
  4. 根据平台选择正确的加载方式
  5. 实现加载失败的重试机制
  6. 添加CRC校验保证资源完整性

内存管理最佳实践

  1. 重要实现引用计数管理AB包生命周期
  2. 场景切换时统一清理资源
  3. 使用Profiler定期检查内存状态
  4. 避免频繁加载卸载同一资源
  5. 合理设置AB包的卸载时机

推荐的AB包管理器架构

/*
 * 推荐架构:
 * 
 * ┌──────────────────────────────────────────┐
 * │           ResourceManager               │  ← 对外接口层
 * │  LoadPrefab() / LoadTexture() / ...     │
 * ├──────────────────────────────────────────┤
 * │          AssetBundleManager             │  ← AB包管理层
 * │  LoadBundle() / UnloadBundle()          │
 * │  引用计数 / 依赖管理 / 缓存策略          │
 * ├──────────────────────────────────────────┤
 * │           ManifestManager               │  ← 清单管理层
 * │  GetDependencies() / GetHash()          │
 * ├──────────────────────────────────────────┤
 * │          DownloadManager                │  ← 下载管理层
 * │  Download() / CheckUpdate()             │
 * │  断点续传 / 并行下载 / 进度回调          │
 * └──────────────────────────────────────────┘
 */

📋 常用API速查表

打包相关(Editor)

API 说明
BuildPipeline.BuildAssetBundles() 核心打包方法
AssetImporter.assetBundleName 设置资源的AB包名
AssetDatabase.GetDependencies() 获取资源依赖
AssetDatabase.GetAllAssetBundleNames() 获取所有AB包名

加载相关(Runtime)

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包