You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在加载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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 12:36:20