Unity 构建序列化:level0、sharedassets、resources.assets 一、构建产物总览以 Windows Standalone 为例MyGame_Data/目录大致如下Android 位于assets/bin/Data/MyGame_Data/ ├── globalgamemanagers # 全局管理器PlayerSettings、TagManager、QualitySettings…… ├── globalgamemanagers.assets # 全局管理器引用的资源Always Included Shaders、Splash、MonoScript 等 ├── level0 # Build Settings 中第 0 个场景的场景对象 ├── level1 ├── ... ├── sharedassets0.assets # level0 首次引用的资源对象 ├── sharedassets0.assets.resS # 对应的大块二进制数据纹理像素、网格顶点等 ├── sharedassets0.resource # 音视频数据AudioClip、VideoClip ├── sharedassets1.assets ├── ... ├── resources.assets # Resources 文件夹中的资源及其依赖 ├── resources.assets.resS ├── resources.resource ├── Resources/ │ ├── unity default resources # 内置资源默认字体、内置 Mesh 等 │ └── unity_builtin_extra # 内置 Shader、Default-Material 等 └── Managed/ # 托管程序集Mono 后端或 il2cpp_data这些文件除.resS/.resource外都是同一种格式SerializedFile。区别只在于装了哪些对象。二、三类文件各装什么1. levelN —— 场景本身对应 Build Settings 中第 N 个场景只包含属于场景的对象场景中所有GameObject、Transform以及各类ComponentMonoBehaviour、MeshRenderer、Collider……场景级设置RenderSettings、LightmapSettings、NavMeshSettings、OcclusionCullingSettings场景实例化的 Prefab 会被展开成普通 GameObject 写入Player 中不存在Prefab 实例概念不包含Texture、Mesh、Material、AudioClip、AnimationClip 等资源对象——它们只以引用形式PPtr出现在 level 文件中。2. sharedassetsN.assets —— 场景依赖的资源规则可以概括为按场景构建顺序资源放进第一个引用它的场景所对应的 sharedassets 文件中后续场景复用不重复存储。Scene0 引用: TexA, MatA, MeshA → sharedassets0: TexA, MatA, MeshA Scene1 引用: TexA, TexB → sharedassets1: TexB (TexA 引用 sharedassets0) Scene2 引用: MatA, MeshC → sharedassets2: MeshC (MatA 引用 sharedassets0)因此加载level2时运行时需要同时访问sharedassets2和sharedassets0通过外部引用表。level0 的依赖越多sharedassets0 越大首场景加载越慢——这是用一个极简启动场景做 level0的直接原因。3. resources.assets —— Resources 文件夹所有位于任意Resources/目录下的资源无条件打入不管有没有被引用。这些资源的依赖如 Prefab 引用的 Material、Texture也会被收集进来若某依赖已在其他序列化文件中则可能以跨文件引用的方式指向它具体分配策略随 Unity 版本略有差异建议用下文的 BuildReport 实际核对。Resources 的路径 → 对象索引表保存在globalgamemanagers的ResourceManager中启动时会构建查找结构Resources 中对象越多启动开销越大。这也是官方不推荐大量使用 Resources 的主要原因之一。4. .resS 与 .resource —— 流式数据Unity 5.x 之后大块数据从 SerializedFile 中剥离文件内容说明.assets.resS纹理像素、网格顶点/索引等对象内只存StreamingInfo { offset, size, path }.resourceAudioClip、VideoClip 数据支持流式读取音频好处是SerializedFile 本体小、反序列化快大数据可异步/直接读取到 GPU 上传缓冲Texture Streaming 也基于此机制。三、SerializedFile 二进制结构┌───────────────────────────────┐ │ Header │ metadataSize, fileSize, version, │ │ dataOffset, endianness ├───────────────────────────────┤ │ Metadata │ │ ├─ Unity 版本字符串 │ 如 2022.3.20f1 │ ├─ 目标平台 (BuildTarget) │ │ ├─ enableTypeTree 标志 │ │ ├─ Types[] │ classID、是否被裁剪、脚本类型索引、 │ │ │ 类型哈希、(可选) TypeTree 节点 │ ├─ Objects[] │ PathID(int64)、byteStart、byteSize、typeIndex │ ├─ ScriptTypes[] │ 引用的脚本对象 │ ├─ Externals[] │ 外部文件表GUID、type、pathName │ └─ RefTypes[] / UserInfo │ (SerializeReference 等新版本) ├───────────────────────────────┤ │ Object Data │ 每个对象按 Transfer 顺序二进制写入 │ │ 对象间按 8/16 字节对齐 └───────────────────────────────┘关键点① 跨文件引用PPtr任何对UnityEngine.Object的引用在磁盘上都是PPtr { int m_FileID; int64 m_PathID; }m_FileID 0同一文件内的对象m_FileID nExternals[n-1]指向的文件如sharedassets0.assets、unity default resourcesm_PathID目标文件中对象的唯一标识Player 构建时通常会重新编号与编辑器 YAML 中的 fileID 不同所以 level1 里的 MeshRenderer 指向 sharedassets0 里的 Material就是一个FileID1, PathIDxx的 PPtr。② TypeTreeTypeTree 描述字段布局名字、类型、大小、对齐用于版本兼容的反序列化。Player 构建默认不写 TypeTree引擎与数据版本完全一致直接按编译期布局读取省空间、更快。AssetBundle 默认写入 TypeTree因为可能被不同版本的 Player 加载可用BuildAssetBundleOptions.DisableWriteTypeTree关闭。③ 对象数据写入引擎使用StreamedBinaryWrite调用每个类型的Transfer()基本类型按小端直接写入bool/byte后按 4 字节对齐数组/字符串先写int32长度再写元素结束后 4 字节对齐MonoBehaviourclassID 114先写引擎字段m_GameObject、m_Enabled、m_Script等再按 C# 序列化规则写用户字段四、构建流程中的序列化步骤逻辑上BuildPipeline.BuildPlayer的流程大致是1. 编译脚本Player 版本去掉 UNITY_EDITOR 代码 2. 按 Build Settings 顺序处理每个场景 a. 打开场景执行 IProcessSceneWithReport / OnProcessScene 回调 b. 剥离 EditorOnly 标签对象、HideFlags.DontSaveInBuild 对象 c. 计算依赖闭包 d. 场景对象 → levelN 未分配过的依赖资源 → sharedassetsN大数据 → .resS / .resource 3. 收集 Resources 文件夹 → resources.assets 4. 全局管理器 → globalgamemanagers其引用资源 → globalgamemanagers.assets 5. Shader 变体编译、纹理平台格式转换结果缓存于 Library 6. 可选LZ4 压缩打包为 data.unity3dStandalone 或合并为 .dataWebGL、放入 APK/AABAndroid注意资源在写入时已是平台格式如 ASTC、ETC2、BC7Material 只保留目标平台需要的 keyword 和 Shader 变体引用。五、如何查看谁进了哪个文件方法 1BuildReport API最权威usingUnityEditor;usingUnityEditor.Build.Reporting;usingUnityEngine;usingSystem.Linq;publicstaticclassBuildContentDumper{[MenuItem(Tools/Build And Dump Content)]publicstaticvoidBuildAndDump(){varoptionsnewBuildPlayerOptions{scenesEditorBuildSettings.scenes.Where(ss.enabled).Select(ss.path).ToArray(),locationPathNameBuild/MyGame.exe,targetBuildTarget.StandaloneWindows64,optionsBuildOptions.None};BuildReportreportBuildPipeline.BuildPlayer(options);foreach(PackedAssetspackedinreport.packedAssets){Debug.Log(${packed.shortPath}({packed.overhead}bytes overhead) );foreach(varginpacked.contents.GroupBy(cc.sourceAssetPath).OrderByDescending(gg.Sum(c(long)c.packedSize))){longsizeg.Sum(c(long)c.packedSize);Debug.Log(${size/1024f:F1}KB{g.Key});}}}}packedAssets会精确列出level0、sharedassets0.assets、resources.assets等每个文件中包含的源资源及大小。方法 2Editor.log构建完成后在 Editor.log 中搜索Build Report可看到按类别统计的大小和最大资源列表。方法 3解析构建产物AssetStudio / AssetRipper可视化浏览 SerializedFile 中的对象。UnityPyPythonimportUnityPy envUnityPy.load(MyGame_Data/sharedassets0.assets)forobjinenv.objects:print(obj.path_id,obj.type.name,obj.byte_size)注意由于 Player 构建不含 TypeTree第三方工具需要匹配对应 Unity 版本的类型结构才能完整解析。六、实践建议问题原因对策首场景加载慢level0 依赖多sharedassets0 庞大level0 用极简启动场景再异步加载主场景启动时间长Resources 索引在启动时构建减少 Resources 使用改用 Addressables / AssetBundle包体臃肿Resources 中的资源无条件打包清理 Resources 中无用资源资源重复同一资源在 Player 和 AssetBundle 中各存一份Player 与 Bundle 之间不会去重需规划好资源归属意外包含编辑器资源场景中残留调试对象使用 EditorOnly 标签或IProcessSceneWithReport剥离内置资源冗余引用 Default-Material、内置 Shader用自定义资源替代避免依赖 unity_builtin_extra七、小结levelN第 N 个场景的场景对象GameObject/Component/场景设置。sharedassetsN.assets按场景顺序首次引用即归属的资源跨场景通过 PPtr 共享不重复存储。resources.assetsResources 文件夹中的全部资源无条件打包并影响启动时间。.resS / .resource从对象中剥离的大块数据支持流式读取。所有文件共享 SerializedFile 格式通过Externals 表 PPtr(FileID, PathID)构成跨文件引用网络。理解这套机制后就能有针对性地优化首场景加载、启动时间和包体大小也更容易判断何时应迁移到 Addressables/AssetBundle 体系。