# AssetBundle 创建与加载

  • AssetBundle 只能用代码创建和加载
  • 在创建前需要先为资源分配资源包,如图:img

# AssetBundle 创建

  • 区分平台(Windows、OSX、Linux 等)
  • 区分创建方式(是否压缩、压缩方式等)
  • lua 文件无法被打包,本文使用的方法是:生成 .lua 文件对应的 .txt 文件,将 .txt 文件打包,然后删除生成的 .txt 文件
  • 代码需要放到 Editor 文件夹中
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace Common
{
    /// <summary>
    /// CreateAssetBundles
    /// </summary>
    public class CreateAssetBundles
    {
        /// <summary>
        /// BuildAllAssetBundles
        /// </summary>
        [MenuItem("Build/Build AssetBundles")]
        private static void BuildAllAssetBundles()
        {
            // 自定义路径
            string assetBundleDirectory = EditorUtility.OpenFolderPanel("选择导出路径", Application.dataPath, "");
            if (string.IsNullOrEmpty(assetBundleDirectory))
            {
                return;
            }
            // 固定路径
            //string assetBundleDirectory = Application.streamingAssetsPath + "/AssetBundles";
            BuildAssetBundleOptions assetBundleOptions;
            BuildTarget targetPlatform;
#if UNITY_STANDALONE_WIN //Windows 独立平台应用程序
            assetBundleOptions = BuildAssetBundleOptions.UncompressedAssetBundle;
            targetPlatform = BuildTarget.StandaloneWindows64;
#elif UNITY_WSA //UWP
            assetBundleOptions = BuildAssetBundleOptions.UncompressedAssetBundle;
            targetPlatform = BuildTarget.WSAPlayer;
#elif UNITY_STANDALONE_OSX //Mac OS X(包括 Universal、PPC 和 Intel 架构)
            assetBundleOptions = BuildAssetBundleOptions.UncompressedAssetBundle;
            targetPlatform = BuildTarget.StandaloneOSX;
#elif UNITY_STANDALONE_LINUX //Linux
            assetBundleOptions = BuildAssetBundleOptions.UncompressedAssetBundle;
            targetPlatform = BuildTarget.StandaloneLinux;
#elif UNITY_ANDROID
            assetBundleOptions = BuildAssetBundleOptions.ChunkBasedCompression;
            targetPlatform = BuildTarget.Android;
#elif UNITY_IOS
            assetBundleOptions = BuildAssetBundleOptions.ChunkBasedCompression;
            targetPlatform = BuildTarget.iOS;
#elif UNITY_WEBGL
            assetBundleOptions = BuildAssetBundleOptions.ChunkBasedCompression;
            targetPlatform = BuildTarget.WebGL;
#endif
            if (!Directory.Exists(assetBundleDirectory))
            {
                Directory.CreateDirectory(assetBundleDirectory);
            }
            else
            {
                Directory.Delete(assetBundleDirectory, true);
                Directory.CreateDirectory(assetBundleDirectory);
            }
            string[] allTxtPaths = CopyLuaToTxt();
            GetFilesInfo();
            BuildPipeline.BuildAssetBundles(assetBundleDirectory, assetBundleOptions, targetPlatform);
            DeleteTxtFiles(allTxtPaths);
            AssetDatabase.Refresh();
        }
        /// <summary>
        /// 获取文件信息,用于调试
        /// </summary>
        private static void GetFilesInfo()
        {
            string[] bundleNames = AssetDatabase.GetAllAssetBundleNames();
            List<string> allAssetPaths = new List<string>();
            foreach (string bundleName in bundleNames)
            {
                string[] assetPaths = AssetDatabase.GetAssetPathsFromAssetBundle(bundleName);
                allAssetPaths.AddRange(assetPaths);
            }
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("All AB paths:");
            foreach (string path in allAssetPaths)
            {
                stringBuilder.AppendLine(path);
            }
            Debug.Log(stringBuilder.ToString());
        }
        /// <summary>
        /// 将 .lua 文件转换为 .txt 文件
        /// </summary>
        /// <returns>.txt 文件路径 & lt;/returns>
        private static string[] CopyLuaToTxt()
        {
            string[] bundleNames = AssetDatabase.GetAllAssetBundleNames();
            List<string> allAssetPaths = new List<string>();
            List<string> allTxtPaths = new List<string>();
            foreach (string bundleName in bundleNames)
            {
                string[] assetPaths = AssetDatabase.GetAssetPathsFromAssetBundle(bundleName);
                allAssetPaths.AddRange(assetPaths);
            }
            foreach (string path in allAssetPaths)
            {
                if (path.EndsWith(".lua"))
                {
                    // 读取.lua 文件内容
                    var utf8 = new System.Text.UTF8Encoding(false);
                    string content = File.ReadAllText(path, utf8);
                    // 构造对应的.txt 文件路径
                    string txtPath = Path.ChangeExtension(path, "txt");
                    File.WriteAllText(txtPath, content, utf8);
                    allTxtPaths.Add(txtPath);
                }
            }
            AssetDatabase.Refresh();
            return allTxtPaths.ToArray();
        }
        /// <summary>
        /// 删除 .txt 文件
        /// </summary>
        /// <param name="dir"></param>
        private static void DeleteTxtFiles(string[] allTxtPaths)
        {
            foreach (string path in allTxtPaths)
            {
                AssetDatabase.DeleteAsset(path);
            }
        }
    }
}

# 缓存记录和加载状态记录

  • 加载 AB 时记录引用计数,在计数为 0 时卸载
  • 记录加载状态防止协程互相等待卡死或请求同一个资源
using UnityEngine;
namespace Common
{
    /// <summary>
    /// AssetBundle 缓存记录
    /// </summary>
    public sealed class BundleRecord
    {
        /// <summary>
        /// AssetBundle 对象
        /// </summary>
        public AssetBundle bundle;
        /// <summary>
        /// 当前有多少个加载请求持有这个 Bundle
        /// </summary>
        public int refCount;
        public BundleRecord() { }
        public BundleRecord(AssetBundle bundle, int refCount)
        {
            this.bundle = bundle;
            this.refCount = refCount;
        }
    }
}
using UnityEngine;
namespace Common
{
    /// <summary>
    /// Manifest 加载记录
    /// </summary>
    public class LoadingManifest
    {
        /// <summary>
        /// 加载的 manifest
        /// </summary>
        public AssetBundleManifest manifest;
        /// <summary>
        /// 是否操作结束
        /// </summary>
        public bool isDone;
    }
}
using UnityEngine;
namespace Common
{
    /// <summary>
    /// AssetBundle 加载记录
    /// </summary>
    public class LoadingAssetBundle
    {
        /// <summary>
        /// 加载的 bundle
        /// </summary>
        public AssetBundle bundle;
        /// <summary>
        /// 是否操作结束
        /// </summary>
        public bool isDone;
        /// <summary>
        /// 加载的引用计数
        /// </summary>
        public int acquireCount;
    }
}

# 生命周期

  • 通过 Handle 管理一次加载的生命周期
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Common
{
    /// <summary>
    /// AssetBundle 加载结果的句柄,包含加载完成后的 AssetBundle 和引用的所有 Bundle Key
    /// </summary>
    public sealed class AssetBundleHandle : IDisposable
    {
        /// <summary>
        /// 加载完成前是否已经请求释放
        /// </summary>
        internal bool releaseRequested;
        /// <summary>
        /// 获取引用的所有 Bundle Key,包括依赖 Bundle 和目标 Bundle
        /// </summary>
        internal List<string> acquiredBundles;
        /// <summary>
        /// 请求路径
        /// </summary>
        public string Path { get; }
        /// <summary>
        /// 加载完成后的 AssetBundle
        /// </summary>
        public AssetBundle Bundle { get; internal set; }
        /// <summary>
        /// 是否完成加载
        /// </summary>
        public bool IsDone { get; internal set; }
        /// <summary>
        /// 是否已经释放
        /// </summary>
        public bool IsReleased { get; internal set; }
        /// <summary>
        /// 是否持有一个成功加载的结果
        /// </summary>
        public bool IsSuccess => IsDone && !IsReleased && Bundle != null;
        /// <summary>
        /// 加载完成事件
        /// </summary>
        public event Action<AssetBundleHandle> Completed;
        internal AssetBundleHandle(string path)
        {
            this.Path = path;
        }
        /// <summary>
        /// 触发加载完成事件
        /// </summary>
        internal void InvokeCompleted()
        {
            Action<AssetBundleHandle> completed = Completed;
            Completed = null;
            completed?.Invoke(this);
        }
        /// <summary>
        /// 释放这一次 Load 获得的所有引用
        /// </summary>
        public void Dispose()
        {
            Release();
        }
        /// <summary>
        /// 释放这一次 Load 获得的所有引用
        /// </summary>
        private void Release()
        {
            AssetBundleManager.Release(this);
        }
    }
}

# 加载器

  • 协程需要在 MonoBehaviour 上执行,所以这里需要一个单例类
namespace Common
{
    /// <summary>
    /// AB 包加载器
    /// </summary>
    public class AssetBundleLoader : MonoSingleton<AssetBundleLoader>
    {
        protected override void Init()
        {
            base.Init();
            DontDestroyOnLoad(this.gameObject);
        }
    }
}

# AssetBundle 加载

  • 两种加载方式:通过网络请求加载(UnityWebRequest)、通过本地存储加载(AssetBundle.LoadFromFile ())。这里使用网络请求加载方式,目的是为了适配更多的设备
  • 同一个 ab 包中如果有多个同名文件,则只会加载第一个匹配的文件
  • 加载目标 ab 包前,需要先加载它的依赖 ab 包(如果存在)
  • 加载目标 ab 包后,调用委托处理其他逻辑
  • 缓存 ab 包,在短时间内大量调用(例如初始化)时只需要加载一次
  • 加载的 ab 包在使用完后需要卸载,防止占用内存
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
namespace Common
{
    /// <summary>
    /// AB 包管理器
    /// </summary>
    public static class AssetBundleManager
    {
        /// <summary>
        ///manifest 字典,key 为 AB 包的根目录名称,value 为对应的 manifest 文件
        /// </summary>
        private static Dictionary<string, AssetBundleManifest> manifestDic = new();
        /// <summary>
        /// 正在加载 manifest 的字典,key 为 AB 包的根目录名称,value 为对应的 LoadingManifest 对象
        /// </summary>
        private static Dictionary<string, LoadingManifest> loadingManifest = new();
        /// <summary>
        /// 缓存的 bundle 字典,key 为 AB 包在 StreamingAssets 内的相对路径,value 为对应的 BundleRecord 对象
        /// </summary>
        private static Dictionary<string, BundleRecord> bundles = new();
        /// <summary>
        /// 正在加载的 bundle 字典,key 为 AB 包在 StreamingAssets 内的相对路径,value 为对应的 LoadingRecord 对象
        /// </summary>
        private static Dictionary<string, LoadingAssetBundle> loadingBundles = new();
        /// <summary>
        /// AssetBundleLoader 实例
        /// </summary>
        private static AssetBundleLoader loader = AssetBundleLoader.Instance;
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
        private static void Reset()
        {
            foreach (BundleRecord record in bundles.Values)
            {
                if (record.bundle != null)
                {
                    record.bundle.Unload(false);
                }
            }
            manifestDic.Clear();
            loadingManifest.Clear();
            bundles.Clear();
            loadingBundles.Clear();
        }
        /// <summary>
        /// 加载资源包
        /// </summary>
        /// <param name="path">AB 包在 StreamingAssets 内的相对路径 & lt;/param>
        /// <param name="callback"> 委托 & lt;/param>
        /// <returns></returns>
        public static AssetBundleHandle LoadAssetBundle(string path, Action<AssetBundleHandle> callback = null)
        {
            AssetBundleHandle handle = new(path);
            if (callback != null)
            {
                handle.Completed += callback;
            }
            loader.StartCoroutine(ToLoadAssetBundle(handle));
            return handle;
        }
        /// <summary>
        /// 释放资源包
        /// </summary>
        public static void Release(AssetBundleHandle handle)
        {
            if (handle == null)
            {
                return;
            }
            // 已经释放过
            if (handle.IsReleased)
            {
                return;
            }
            // Bundle 还在加载
            if (!handle.IsDone)
            {
                handle.releaseRequested = true;
                return;
            }
            // 加载失败,没有资源需要释放
            if (handle.acquiredBundles == null)
            {
                handle.IsReleased = true;
                handle.Bundle = null;
                return;
            }
            // 释放这张 Handle 持有的全部引用
            for (int i = handle.acquiredBundles.Count - 1; i >= 0; --i)
            {
                ReleaseBundle(handle.acquiredBundles[i]);
            }
            handle.acquiredBundles = null;
            handle.Bundle = null;
            handle.IsReleased = true;
        }
        /// <summary>
        /// 加载资源包
        /// </summary>
        /// <param name="handle"></param>
        /// <returns></returns>
        private static IEnumerator ToLoadAssetBundle(AssetBundleHandle handle)
        {
            string path = handle.Path;
            if (!TryResolveBundlePath(path, out string startName, out string bundleName, out string startPath))
            {
                Debug.LogError("Invalid path: " + path);
                LoadFailed(handle);
                yield break;
            }
            // 加载 manifest 文件
            AssetBundleManifest targetManifest = null;
            yield return AcquireManifest(startPath, startName,
                manifest =>
                {
                    targetManifest = manifest;
                });
            if (targetManifest == null)
            {
                LoadFailed(handle);
                yield break;
            }
            // 获取依赖文件
            string[] dependencies = targetManifest.GetAllDependencies(bundleName);
            List<string> acquired = new();
            foreach (string dependency in dependencies)
            {
                AssetBundle dependencyBundle = null;
                yield return AcquireBundle(startName, dependency, startPath,
                    bundle =>
                    {
                        dependencyBundle = bundle;
                    });
                if (dependencyBundle == null)
                {
                    for (int i = acquired.Count - 1; i >= 0; --i)
                    {
                        ReleaseBundle(acquired[i]);
                    }
                    LoadFailed(handle);
                    yield break;
                }
                acquired.Add(CombinePath(startName, dependency));
            }
            // 加载目标 Bundle
            AssetBundle targetBundle = null;
            yield return AcquireBundle(startName, bundleName, startPath,
                bundle =>
                {
                    targetBundle = bundle;
                });
            if (targetBundle == null)
            {
                for (int i = acquired.Count - 1; i >= 0; --i)
                {
                    ReleaseBundle(acquired[i]);
                }
                LoadFailed(handle);
                yield break;
            }
            acquired.Add(CombinePath(startName, bundleName));
            handle.Bundle = targetBundle;
            handle.acquiredBundles = acquired;
            handle.IsDone = true;
            // 有可能用户在加载过程中已经 Release ()
            if (handle.releaseRequested)
            {
                Release(handle);
                yield break;
            }
            handle.InvokeCompleted();
        }
        /// <summary>
        /// 加载 manifest
        /// </summary>
        /// <param name="startPath">AB 包根目录路径 & lt;/param>
        /// <param name="startName">AB 包的根目录名称 & lt;/param>
        /// <param name="callback"> 委托 & lt;/param>
        /// <returns></returns>
        private static IEnumerator AcquireManifest(string startPath, string startName, Action<AssetBundleManifest> callback)
        {
            // 已经加载完成
            if (manifestDic.TryGetValue(startName, out AssetBundleManifest manifest))
            {
                callback?.Invoke(manifest);
                yield break;
            }
            // 已经有人正在加载
            if (loadingManifest.TryGetValue(startName, out LoadingManifest loading))
            {
                while (!loading.isDone)
                {
                    yield return null;
                }
                if (loading.manifest == null)
                {
                    callback?.Invoke(null);
                    yield break;
                }
                // 加载成功后直接返回结果
                if (manifestDic.TryGetValue(startName, out manifest))
                {
                    callback?.Invoke(manifest);
                }
                else
                {
                    Debug.LogError($"Bundle load state error: {startName}");
                    callback?.Invoke(null);
                }
                yield break;
            }
            // 没加载,也没人正在加载
            loading = new LoadingManifest();
            loadingManifest.Add(startName, loading);
            using UnityWebRequest request_ab = UnityWebRequestAssetBundle.GetAssetBundle(CombinePath(startPath, startName), 0);
            yield return request_ab.SendWebRequest();
            if (request_ab.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError("Failed to load AssetBundle: " + request_ab.error);
                // 通知所有等待者
                CompleteLoading(startName, loading, null);
                callback?.Invoke(null);
                yield break;
            }
            AssetBundle bundle_ab = DownloadHandlerAssetBundle.GetContent(request_ab);
            if (bundle_ab == null)
            {
                Debug.LogError($"Failed to get manifest bundle content: {startName}");
                CompleteLoading(startName, loading, null);
                callback?.Invoke(null);
                yield break;
            }
            manifest = bundle_ab.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
            bundle_ab.Unload(false);
            if (manifest == null)
            {
                Debug.LogError($"AssetBundleManifest was not found in manifest bundle: {startName}");
                CompleteLoading(startName, loading, null);
                callback?.Invoke(null);
                yield break;
            }
            manifestDic.Add(startName, manifest);
            // 通知所有等待者
            CompleteLoading(startName, loading, manifest);
            callback?.Invoke(manifest);
        }
        /// <summary>
        /// 加载 bundle
        /// </summary>
        /// <param name="startName">AB 包的根目录名称 & lt;/param>
        /// <param name="bundleName">AB 包名称 & lt;/param>
        /// <param name="startPath">AB 包根目录路径 & lt;/param>
        /// <param name="callback"> 委托 & lt;/param>
        /// <returns></returns>
        private static IEnumerator AcquireBundle(string startName, string bundleName, string startPath, Action<AssetBundle> callback)
        {
            string key = CombinePath(startName, bundleName);
            // 已经加载完成
            if (bundles.TryGetValue(key, out BundleRecord record))
            {
                record.refCount++;
                callback?.Invoke(record.bundle);
                yield break;
            }
            // 已经有人正在加载
            if (loadingBundles.TryGetValue(key, out LoadingAssetBundle loadingRecord))
            {
                loadingRecord.acquireCount++;
                // 等待那个加载任务完成
                while (!loadingRecord.isDone)
                {
                    yield return null;
                }
                // 加载失败
                if (loadingRecord.bundle == null)
                {
                    callback?.Invoke(null);
                    yield break;
                }
                // 加载成功
                if (bundles.TryGetValue(key, out record))
                {
                    callback?.Invoke(record.bundle);
                }
                else
                {
                    Debug.LogError($"Bundle load state error: {key}");
                    callback?.Invoke(null);
                }
                yield break;
            }
            // 没加载,也没人正在加载。第一个请求拥有第一个引用
            loadingRecord = new LoadingAssetBundle
            {
                acquireCount = 1
            };
            loadingBundles.Add(key, loadingRecord);
            using UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(CombinePath(startPath, bundleName), 0);
            yield return request.SendWebRequest();
            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"Failed to load AssetBundle: {bundleName}\n{request.error}");
                // 通知所有等待者
                CompleteLoading(key, loadingRecord, null);
                callback?.Invoke(null);
                yield break;
            }
            AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
            if (bundle == null)
            {
                Debug.LogError($"Failed to get AssetBundle content: {bundleName}");
                CompleteLoading(key, loadingRecord, null);
                callback?.Invoke(null);
                yield break;
            }
            // 第一个请求完成记录
            record = new BundleRecord(bundle, loadingRecord.acquireCount);
            bundles.Add(key, record);
            // 通知所有等待者
            CompleteLoading(key, loadingRecord, bundle);
            callback?.Invoke(bundle);
        }
        /// <summary>
        /// 加载完成,通知所有等待者
        /// </summary>
        private static void CompleteLoading(string key, LoadingAssetBundle loading, AssetBundle bundle)
        {
            loading.bundle = bundle;
            loading.isDone = true;
            loadingBundles.Remove(key);
        }
        /// <summary>
        /// 加载完成,通知所有等待者
        /// </summary>
        private static void CompleteLoading(string key, LoadingManifest loading, AssetBundleManifest manifest)
        {
            loading.manifest = manifest;
            loading.isDone = true;
            loadingManifest.Remove(key);
        }
        /// <summary>
        /// 加载失败,结束 Handle
        /// </summary>
        private static void LoadFailed(AssetBundleHandle handle)
        {
            handle.Bundle = null;
            handle.acquiredBundles = null;
            handle.IsDone = true;
            if (handle.releaseRequested)
            {
                handle.IsReleased = true;
                return;
            }
            handle.InvokeCompleted();
        }
        /// <summary>
        /// 释放 bundle 的引用计数,如果引用计数小于等于 0,则卸载 bundle
        /// </summary>
        private static bool ReleaseBundle(string key)
        {
            if (!bundles.TryGetValue(key, out BundleRecord record))
            {
                return false;
            }
            record.refCount--;
            if (record.refCount <= 0)
            {
                record.bundle.Unload(false);
                bundles.Remove(key);
            }
            return true;
        }
        /// <summary>
        /// 处理路径
        /// </summary>
        /// <param name="path"></param>
        /// <param name="startName">AB 包的根目录名称 & lt;/param>
        /// <param name="bundleName">AB 包名称 & lt;/param>
        /// <param name="startPath">AB 包根目录路径 & lt;/param>
        /// <returns></returns>
        private static bool TryResolveBundlePath(string path, out string startName, out string bundleName, out string startPath)
        {
            startName = null;
            bundleName = null;
            startPath = null;
            if (string.IsNullOrEmpty(path))
            {
                return false;
            }
            string normalizedPath = path.Replace('\\', '/').Trim().TrimStart('/');
            string[] segments = normalizedPath.Split('/');
            if (segments.Length < 2)
            {
                return false;
            }
            for (int i = 0; i < segments.Length; ++i)
            {
                segments[i] = segments[i].Trim();
                if (string.IsNullOrEmpty(segments[i])
                    || segments[i] == "."
                    || segments[i] == ".."
                    || segments[i].Contains(':'))
                {
                    return false;
                }
            }
            startName = segments[0];
            bundleName = string.Join("/", segments, 1, segments.Length - 1);
            if (string.IsNullOrEmpty(startName) || string.IsNullOrEmpty(bundleName))
            {
                return false;
            }
            startPath = PathHelper.GetPath(CombinePath(null, startName));
            return true;
        }
        /// <summary>
        /// 组合路径
        /// </summary>
        private static string CombinePath(string left, string right)
        {
            if (string.IsNullOrEmpty(right))
            {
                return null;
            }
            if (left == null)
            {
                return $"/{right}";
            }
            return $"{left}/{right}";
        }
    }
}

# 使用方法

创建
点击按钮创建 ab 包,如图:img

加载

using Common;
using UnityEngine;
namespace Default
{
    /// <summary>
    /// TestAB
    /// </summary>
    public class TestAB : MonoBehaviour
    {
        private string path = "/AssetBundles/gas/player";
        private bool isDone = false;
        private AssetBundleHandle handle;
        private void Start()
        {
            handle = AssetBundleManager.LoadAssetBundle(path, OnTest);
        }
        private void Update()
        {
            if (handle != null && !isDone)
            {
                isDone = handle.IsDone;
                Debug.Log($"handle.IsDone:{handle.IsDone}, handle.IsSuccess:{handle.IsSuccess}");
            }
        }
        private void OnTest(AssetBundleHandle completedHandle)
        {
            if (!completedHandle.IsSuccess)
            {
                Debug.LogError($"AssetBundle 加载失败:{completedHandle.Path}", this);
                return;
            }
            Debug.Log(completedHandle.Bundle);
            Debug.Log($"handle == completedHandle: {completedHandle == handle}");
            //completedHandle.Dispose();
            //handle = null;
        }
        private void OnDestroy()
        {
            handle?.Dispose();
            handle = null;
        }
    }
}