AssetStudio 资源加载全解:一个 Bundle 变成可导出资源的完整流程

发布时间:2026/8/27 1:06:56
AssetStudio 资源加载全解:一个 Bundle 变成可导出资源的完整流程 AssetStudio 资源加载全解一个 Bundle 变成可导出资源的完整流程【免费下载链接】AssetStudioAssetStudio is a tool for exploring, extracting and exporting assets and assetbundles.项目地址: https://gitcode.com/gh_mirrors/as/AssetStudioAssetStudio 是一个用于探索、提取和导出 Unity 资源与 AssetBundle 的工具它的资源加载模块干的核心事情就一件把一堆格式、布局、压缩方式各不相同的文件变成一张可浏览、可导出的对象图。这里的痛点很实在——签名五花八门、压缩层层嵌套、引用散落在多个文件之间、单个 Bundle 动辄几个 GB。这篇文章按要解决什么问题的顺序把整条链路讲清楚文件怎么认出来、壳怎么剥、引用怎么解、内存怎么控制。格式五花八门怎么准确认出它是谁魔数检测如何工作前 20 字节里读一个字符串文件进入 AssetStudio 时第一步不是按扩展名决定怎么读而是FileReader构造函数里立刻执行CheckFileType()源码位于AssetStudio/FileReader.cs。这个函数先把文件开头最多 20 字节按 NUL 结尾读成一个字符串——Unity 的各种容器格式都会在文件头放一段可读签名命中即定身份文件头字符串识别为含义UnityFSBundleFile现代 AssetBundle线上最常见UnityWeb / UnityRawBundleFile旧版 web / raw 格式v6 起按 UnityFS 处理UnityArchiveBundleFile归档格式UnityWebData1.0WebFileweb 数据容器这里的关键点是它完全不信任文件扩展名一个.dat文件照样可能被认成 Bundle。签名没命中时走回退链Unity 并不是每个文件都有签名AssetStudio 准备了一条有序的回退链前 2 字节是1F 8B→ GZipFile偏移0x20处 6 字节等于62 72 6F 74 6C 69ASCII 的 brotli→ BrotliFile。这是 Unity 格式的怪癖——它把 brotli 这个词嵌在固定位置而不是标准 Brotli 流头尝试序列化文件结构校验见下节→ AssetsFile前 4 字节是50 4B 03 04或50 4B 07 08ZIP 本地头 / 分卷标记→ ZipFile全部落空 → ResourceFile当裸字节处理。一个容易踩的坑是这条链的顺序不能换GZip 先于序列化结构判断否则一个被 GZip 压过的 assets 文件就会被误读。序列化文件的结构自证不光读头还要验算任何魔数都没命中时最后一步是校验序列化文件头是否自洽读出元数据区大小、文件自报总大小、格式版本、数据区偏移——注意FileReader默认按大端序读恰好就是 Unity 序列化头的字节序。版本 ≥ 22 时这些字段升级为 64 位要重读约 48 字节的扩展头。随后只做两个判断自报大小必须等于磁盘上的实际长度数据区偏移不能超过文件长度。简单说就是两个独立字段互相验证——二进制文件的头部再随机只要长度字段对不上账就不是序列化文件。识别只是入场券很多文件此时还裹着一层压缩壳。压缩壳怎么剥递归地一层层拆GZip / Brotli先解压再回头识别在AssetsManager.LoadFile的分支里GZip 和 Brotli 的处理各只有一步把整个流解压进MemoryStreamGZip 直接用 BCL 的GZipStreamBrotli 用项目自带的BrotliInputStream代码在AssetStudio/Brotli/在其上重建FileReader再递归调用一次LoadFile。这里的关键点是识别逻辑被完整复用。剥掉壳之后里面可能是 Bundle、assets 文件甚至再套一层压缩——与其猜里面是什么不如把它重新放回识别链路的起点。整条链因此能收敛成一个 switch 加几次递归。Bundle 里套 Bundle递归不是问题WebFile 打开后内部每个条目都会拿到新的FileReader再按类型分派assets 直接进对象解析队列仍是 BundleFile 就重新走解包流程仍是 WebFile 就再次走 web 解包。实际跑起来你会发现多层嵌套的 AssetBundle 就是被这样一层层剥开的。真实场景嵌套层数很少递归深度不构成实际问题。动手之前先把分卷文件合并真正开始加载前LoadFiles/LoadFolder会先调用MergeSplitAssets把.split0、.split1… 这类碎片拼回完整文件ZIP 分支里还会再做一遍——归档里凡名字含.split的条目先在内存中合并成流再当作一个文件走LoadFile。目录里看到一组xxx.assets.splitN时说明正处在合并环节。壳剥掉之后轮到 Bundle 内部每个数据块该跑哪个解压算法Unity Bundle 解析流程的内层每个块怎么选对算法两级目录先读头再读块表BundleFile构造函数先读Header签名、格式版本、生成它的 Unity 版本字符串、总大小、块信息区的压缩/未压缩大小、标志位。随后分路旧版UnityWeb/UnityRawv6 统一并入 UnityFS把块和目录一次性读完主流的UnityFS先读头再读块列表 目录表。目录表是一组Node每条记录一个内部文件的偏移、大小和路径——可以把它理解成一份迷你目录清单。标志位低 6 位决定跑哪个算法这里的关键点是每个块自描述StorageBlockFlags的低 6 位头部标志里也有对应的压缩类型掩码就是压缩类型。压缩类型实际使用的解码器备注None直接拷贝定长CopyTo进块流Lzma项目内置的 7zip LZMA 解码SevenZipHelper源码在AssetStudio/7zip/支持流式直读直写Lz4 / Lz4HCLZ4Codec.Decode解码后校验输出长度不符直接抛异常Lzham不支持抛IOException明确拒绝LZ4 分支是后面内存池的最大受益者压缩、解压两个缓冲区都从池里租用完归还。WebFile简单的那条路WebFile 结构轻得多UnityWebData1.0之后是头部长度头部结束前依次排列(数据偏移, 数据长度, 路径)三元组取文件就是 seek 到偏移读固定长度——没有块、没有压缩。对比着记BundleFile 是归档 分块压缩WebFile 是原始拼接 索引表。对象全部装进来之后问题变成一个对象往往只是指向另一个文件的引用。PPtr 跨文件引用怎么解析外部引用表m_Externals 与 FileIdentifier序列化文件头部带一张外部引用表——FileIdentifier数组每条含 GUID、类型、原始路径外加 AssetStudio 自用的fileName。加载 assets 文件时AssetsManager遍历这张表先在同目录找同名文件找不到就在子目录递归搜不区分大小写找不到的记进noexistFiles集合避免反复空搜。找到的文件直接追加进加载队列——注意此时主加载循环还在跑队列边读边长所以Load的主循环是每轮重新检查长度的 for 循环而不是 foreach。PPtr 的解析就是三步PPtrPersistent Pointer是 Unity 的对象引用句柄内容只有两个数m_FileID和m_PathID。跨文件引用怎么解析三步// m_FileID 0引用就在当前文件直接查本文件 // m_FileID 0取 m_Externals[m_FileID - 1] 得到目标文件名 // 在全局 assetsFileList 中按名定位结果走缓存然后 if (sourceFile.ObjectsDic.TryGetValue(m_PathID, out var obj)) { if (obj is T variable) { result variable; return true; } }定位目标文件2. 在目标文件的ObjectsDic里用m_PathID直查对象3.is T确认具体类型。文件级查找结果缓存在assetsFileIndexCacheDictionarystring, int忽略大小写每个 PPtr 自身也记住缓存索引同一目标被引用千百次也只查一次。一个容易踩的坑是m_PathID的位宽旧序列化格式Unknown_14 之前它是 32 位整数之后才扩成 64 位——按错宽度读一个字节错位后面所有字段全跟着乱。依赖缺失时静默失败不炸全局三步中任何一步没走通TryGet只是返回 false、调用方拿到 null不抛异常。缺失的依赖在界面上表现为引用为空但整批资源照常处理完。对批量导出工具来说这种优雅降级比响亮地抛错重要得多。对象装好了剩下的账是内存解压后的 Bundle 轻松超过 2GB。超大 Bundle 的内存控制手段AssetStudio 内存优化策略BigArrayPool大缓冲池BigArrayPoolT的全部实现就是一行配置ArrayPoolT.Create(64 * 1024 * 1024, 3)——单块数组上限 64MB、3 个规格档。LZ4 解压路径里输入输出缓冲区都从这里租var compressedBytes BigArrayPoolbyte.Shared.Rent(compressedSize); var uncompressedBytes BigArrayPoolbyte.Shared.Rent(uncompressedSize); // 解码写入块流后两块内存全部归还池中同样大小的块反复解压时反复用的是同一批大缓冲GC 几乎看不到它们的影子。解压总量超 2GB 时降级到磁盘CreateBlocksStream按所有块解压后大小之和做决定不到int.MaxValue预分配MemoryStream达到或超过就落到源文件旁边的.temp临时文件并用FileOptions.DeleteOnClose保证关闭时自动消失。同一思路在按目录表切分内部文件阶段再来一次内部文件 ≥ 2GB 的写进源文件旁的xxx_unpacked目录其余进MemoryStream。用完即释放LoadBundleFile/LoadWebFile各分支都在 finally 里关 readerClear()统一关闭所有 assets 与资源文件的 reader、清空全部字典。不依赖终结器的最终会清理——流用完了就关。快速上手三步把一批 Bundle 跑起来 整目录加载别挑单文件用LoadFolder把整个目录丢进去.split分卷会自动合并。跨文件引用依赖同目录的其他文件挑单个文件手动开往往缺的就是这一环——把文件放对位置AssetStudio 会自己把依赖找齐。用日志关键字定位问题日志里三个词就能定位绝大多数问题Loading xxx表示开始处理Skipping xxx表示重名跳过Error while reading会带上文件名和异常。若看到 Unity version has been stripped 报错说明文件头版本被剥离、工具无法自行推断格式版本——在选项里设置SpecifyUnityVersionGUI 的版本选项即对应它再加载即可。留意中间产物.temp与*_unpacked是 ≥2GB 数据的落地中转跑的过程中别删跑完记得看一眼目录确认已清理。下次再遇到打不开的 Bundle先看前 20 字节的签名——后面的路 AssetStudio 已经铺好了。【免费下载链接】AssetStudioAssetStudio is a tool for exploring, extracting and exporting assets and assetbundles.项目地址: https://gitcode.com/gh_mirrors/as/AssetStudio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考