如何在加载AssetBundle前不加载包体获取其内部真实名称
AssetBundle 重名冲突预校验方案
核心实现思路:直接解析AB文件头读取真实名称
不需要完整加载AssetBundle就能拿到内部真实名称的最可靠方式,是直接解析AssetBundle的公开文件头部结构,整个过程仅读取文件开头几十到几百字节,不会触发Unity的AB加载逻辑,完全不会引发重名加载报错,性能开销可以忽略。
AssetBundle文件头部有固定格式规范:
- 开头7字节为固定魔数,用来识别文件是否为合法AssetBundle,常见值包括
UnityFS(当前主流版本格式)、UnityWeb、UnityRaw(旧版本格式) - 魔数后为固定偏移的版本号、文件大小、压缩配置等元数据字段
- 头部元数据块中存储的第一个字符串字段,就是AssetBundle的真实内部名称,和文件名没有关联
参考实现代码
using System; using System.IO; using System.Collections.Generic; using System.Text; using UnityEngine; public static class AssetBundleHelper { // 全局已加载AB缓存,Key为AB真实名称 private static readonly Dictionary<string, AssetBundle> _loadedBundles = new Dictionary<string, AssetBundle>(); /// <summary> /// 不加载AB,直接从文件头读取真实AB名称 /// </summary> public static string ReadRealBundleName(string filePath) { if (!File.Exists(filePath)) return null; try { using (var fs = File.OpenRead(filePath)) using (var br = new BinaryReader(fs)) { // 校验文件魔数 char[] sigChars = br.ReadChars(7); string signature = new string(sigChars); if (signature != "UnityFS" && signature != "UnityWe" && signature != "UnityRa") { return null; } // 跳过固定偏移字段,可根据项目所用Unity版本微调偏移值 br.ReadUInt32(); // 格式版本 br.ReadBytes(10); // Unity版本字符串固定长度段,不同版本可校准 br.ReadUInt32(); // 生成器版本 br.ReadInt64(); // 文件总大小 uint compressedInfoSize = br.ReadUInt32(); uint uncompressedInfoSize = br.ReadUInt32(); uint flags = br.ReadUInt32(); // 根据flags判断头部压缩类型,解压元数据块后读取第一个字符串即为真实AB名 // 压缩类型判断逻辑:(flags & 0x3F) 0=未压缩 1=LZMA 2=LZ4/LZ4HC byte[] infoBytes; int compressionType = (int)(flags & 0x3F); if (compressionType == 0) { infoBytes = br.ReadBytes((int)uncompressedInfoSize); } else if (compressionType == 2) { infoBytes = br.ReadBytes((int)compressedInfoSize); // 调用Unity内置LZ4解压接口即可,无需引入第三方库 infoBytes = UncompressLZ4(infoBytes, (int)uncompressedInfoSize); } else { // LZMA压缩仅在极旧版本AB中作为头部压缩格式出现,按需添加解压逻辑即可 return null; } // 从解压后的元数据块中读取第一个字符串即为真实AB名称 using (var ms = new MemoryStream(infoBytes)) using (var infoBr = new BinaryReader(ms)) { // 跳过元数据块前的固定字段 infoBr.ReadInt64(); // 数据块偏移 infoBr.ReadInt64(); // 数据块大小 infoBr.ReadInt32(); // 节点数量 infoBr.ReadBytes(16); // 缓冲区相关字段 // 读取真实名称 int nameLen = infoBr.ReadInt32(); return Encoding.UTF8.GetString(infoBr.ReadBytes(nameLen)); } } } catch (Exception e) { Debug.LogWarning($"读取AB真实名称失败,文件路径:{filePath},错误:{e.Message}"); return null; } } /// <summary> /// 安全加载AB,自动规避重名冲突 /// </summary> public static AssetBundle SafeLoadBundle(string filePath) { string realName = ReadRealBundleName(filePath); if (string.IsNullOrEmpty(realName)) { Debug.LogError($"非法AB文件:{filePath}"); return null; } // 缓存中已存在直接返回 if (_loadedBundles.TryGetValue(realName, out var loadedBundle) && loadedBundle != null) { return loadedBundle; } // 缓存不存在再执行加载 var newBundle = AssetBundle.LoadFromFile(filePath); if (newBundle != null) { _loadedBundles[realName] = newBundle; } return newBundle; } /// <summary> /// 卸载AB时同步清理缓存 /// </summary> public static void UnloadBundle(AssetBundle bundle, bool unloadAllLoadedObjects = false) { if (bundle == null) return; string targetKey = null; foreach (var pair in _loadedBundles) { if (pair.Value == bundle) { targetKey = pair.Key; break; } } if (!string.IsNullOrEmpty(targetKey)) { _loadedBundles.Remove(targetKey); } bundle.Unload(unloadAllLoadedObjects); } private static byte[] UncompressLZ4(byte[] compressedData, int uncompressedSize) { // 这里替换为项目中使用的LZ4解压实现,Unity可直接调用内置的UnsafeUtility相关接口 return null; } }
注意事项
- 不要用
try-catch包裹AssetBundle.LoadFromFile的方式做重名判断:触发重名报错时Unity底层已经输出错误日志,部分版本还会残留未释放的文件句柄,长时间运行会导致资源泄漏。 - 上述代码中的偏移量可以根据你项目实际使用的Unity版本做微调,所有正式发布的Unity版本的AB格式都是固定的,不存在结构随机变动的情况。
- 如果需要兼容用户用不同Unity版本打包的AB,只需要加一层版本识别逻辑,对应调整读取偏移即可,所有版本的AB真实名称都存储在头部元数据块中,不存在读取不到的情况。
- 单文件读取真实名称的耗时在0.1毫秒级别,哪怕批量扫描上百个AB文件也不会造成卡顿。
内容的提问来源于stack exchange,提问作者pixlhero
相关产品推荐
相关产品推荐

