Unity打包机制:如何决定资源进包
一、核心问题Unity 怎么决定什么进包打包(Build)时Unity面临一个根本问题 项目里成千上万个资源哪些要打进最终包 答案不是全部而是 从根(Root)出发顺着引用链能到达的才进包 ↓ 这叫【可达性分析】(Reachability) 类似垃圾回收(GC)的标记-清除思路核心思想Unity 把打包看成一次可达性遍历——从几个固定入口出发凡是被引用链连到的资源就标记为需要连不到的就不打包。二、三个根打包入口一切引用追溯都从这三个入口开始┌──────────────────────────────────────────┐ │ 根1Build Settings 中启用的场景 │ │ → 场景本身 场景引用的所有资源(递归) │ ├──────────────────────────────────────────┤ │ 根2Resources 文件夹 │ │ → ⚠️全部内容无条件进包(不看引用!) │ ├──────────────────────────────────────────┤ │ 根3AssetBundle / Addressables 标记的资源 │ │ → 被标记的资源 其依赖 │ └──────────────────────────────────────────┘ 还有一些次要入口 ├── Graphics Settings 里的 Always Included Shaders ├── Preloaded Assets (Player Settings) ├── Project Settings 各处引用的资源 └── Editor Default Resources 等三个根的本质区别┌────────────┬──────────────┬─────────────┐ │ 根 │ 进包规则 │ 特点 │ ├────────────┼──────────────┼─────────────┤ │ 场景 │ 按引用链 │ 精确,只进用到的│ │ Resources │ 全量无条件 │ ⚠️粗暴,易浪费 │ │ AB/Addr │ 按标记依赖 │ 可控,按需 │ └────────────┴──────────────┴─────────────┘三、引用链是怎么追溯的从场景根出发的递归场景(根) │ ├── GameObject │ ├── MeshRenderer ──→ Material(材质) │ │ ├──→ Shader │ │ ├──→ Texture(贴图) │ │ └──→ 其他贴图... │ ├── MeshFilter ──→ Mesh(网格) │ └── MonoBehaviour ──→ 序列化字段引用的资源 │ (拖到Inspector上的) │ └── ...递归所有对象和组件... 每个引用都是一条边Unity顺着边遍历 能走到的所有资源 → 进包引用的载体序列化字段引用是怎么连起来的靠【序列化】 组件/资源里的字段如果是资源引用类型 public Texture2D icon; ← 拖了张图 public Material mat; ← 引用了材质 public GameObject prefab; ← 引用了预制体 ↓ 这些引用被序列化保存(存的是GUID) ↓ 打包器读取序列化数据 → 发现引用 → 追溯关键Unity 的引用追溯基于序列化数据里保存的资源引用GUID。只有被序列化保存下来的引用才能被追溯到。四、GUID 与引用的底层理解引用的存储形式才懂追溯原理。.meta 文件与 GUID每个资源都有一个 .meta 文件里面有唯一GUID MyTexture.png MyTexture.png.meta ← 内含: guid: a1b2c3d4... 引用其他资源时存的不是路径而是GUID 材质文件引用贴图 m_Texture: {fileID: 2800000, guid: a1b2c3d4...} ↑ 指向贴图的GUID为什么用GUID而非路径 ├── 移动/重命名资源 → 路径变GUID不变 → 引用不断 └── 打包器靠GUID建立资源间的引用关系图引用关系图依赖图Unity内部维护一张依赖图 材质M ──guid──→ 贴图T 预制体P ──guid──→ 材质M 场景S ──guid──→ 预制体P ↓ 从场景S出发沿GUID指针能走到 P→M→T → 全部进包五、GetDependencies查询引用链的API// 这就是打包器判定引用用的核心API(编辑器下)// 查一个资源依赖了谁(它引用的所有资源)string[]depsAssetDatabase.GetDependencies(Assets/MyPrefab.prefab,recursive:true// true递归查完整链);// 返回: 该预制体用到的所有材质、贴图、网格、shader...recursive参数 ├── true递归查整条链(A→B→C全返回) └── false只查直接引用(只返回A→B) 打包判定用的就是递归的完整依赖⚠️重要GetDependencies只能查到静态序列化引用查不到代码里字符串动态加载的引用下面详述。六、⚠️ 静态引用 vs 动态引用核心盲区这是理解打包判定机制最关键的一点。静态引用能被追溯通过序列化字段建立的引用 public Sprite icon; // Inspector里拖了图 ↓ 序列化保存GUID → 打包器能追溯 → icon进包 ✅动态引用追溯不到代码里用字符串加载的 Resources.LoadGameObject(Enemy); // 字符串路径 Addressables.LoadAssetAsyncSprite(key); // 字符串key AssetBundle.LoadAsset(weapon); // 字符串名 ↓ 没有序列化的GUID引用连线 ↓ 打包器的依赖图里【没有这条边】 ↓ 无法通过引用链判定 → 靠其他机制保证进包那动态加载的资源怎么进包的每种动态加载有各自的进包保证 ┌────────────────┬──────────────────────┐ │ 动态加载方式 │ 靠什么进包 │ ├────────────────┼──────────────────────┤ │ Resources.Load │ Resources文件夹全量进包│ │ │ (根2保证,不靠引用链) │ ├────────────────┼──────────────────────┤ │ Addressables │ 被标记Addressable │ │ │ (根3保证) │ ├────────────────┼──────────────────────┤ │ AssetBundle │ 显式加进AB打包 │ │ │ (根3保证) │ └────────────────┴──────────────────────┘ 关键动态加载不靠引用链追溯进包 而靠资源本身在某个根的范围内核心洞察Unity 有两套进包机制——① 引用链追溯静态引用② 位置/标记规则Resources全量、AB/Addressables标记。动态加载的资源靠第二套保证进包不靠引用追溯。七、常见意外不进包问题问题代码动态加载资源运行时报找不到/丢失 原因链 资源没在Resources里 没标记Addressable/加进AB 又没有静态引用连着它 ↓ 三套机制都没覆盖到 → 不进包 → 运行时缺失 解决 ├── 放进Resources(但全量进包,慎用) ├── 标记为Addressable ├── 加进AssetBundle └── 或做个静态引用(如在某ScriptableObject里引用它)Shader 变体的特殊情况Shader进包了,但某个变体没进包(被剔除): → 运行时用到该变体 → 效果异常 → (回顾前面shader_feature剔除机制) Shader也可用 Always Included Shaders 强制进包八、Prefab / ScriptableObject 的引用作用利用静态引用拉住动态资源的技巧让扫描不到的资源被引用进包的常用手法 方法建一个资源清单ScriptableObject public ListGameObject allEnemies; ← 拖入所有敌人预制体 ↓ 这个SO被场景/其他进包资源引用 ↓ 它引用的所有敌人 → 通过引用链进包 ✅ ↓ 代码再从这个SO里取用(而非字符串Load)好处 ├── 避免Resources全量进包的浪费 ├── 引用可被追溯(不怕误删/漏打包) ├── 重命名资源不断引用(GUID) └── 是Addressables之外的一种轻量方案九、编辑器专用资源如何被排除不该进包的资源如何自动排除 1. Editor 文件夹 Assets/.../Editor/ 里的脚本和资源 → 只在编辑器用,不进包 2. 不被任何根引用 自然不进包(除非在Resources) 3. 平台相关 用平台宏/条件排除 4. StreamingAssets 原样拷贝进包(特殊,不参与引用判定)十、验证进包结果如何确认到底什么进了包 1. Editor Log(编辑器日志): 打包后,日志列出所有进包资源大小 路径: Console右上角菜单 → Open Editor Log 搜索 Used Assets 2. Build Report(2020): 构建报告,可视化各资源占用 3. GetDependencies手动核对: 代码查某资源依赖链 4. 解包分析: 第三方工具分析最终包内容Editor Log 输出示例 Used Assets and files from the Resources folder, sorted by uncompressed size: 5.2 mb 4.1% Assets/Textures/BigTex.png 2.1 mb 1.6% Assets/Models/Character.fbx ... → 意外发现大资源/不该进的资源 → 定位优化十一、StreamingAssets 的特殊性StreamingAssets 文件夹很特殊 ├── 不参与引用判定 ├── 原封不动整个拷贝进包 ├── 用文件路径IO访问(非资源加载) └── 常放:视频、原始数据、需外部读取的文件 ⚠️ 里面放什么就打包什么,无剔除 → 别乱放,和Resources一样是黑洞十二、常见误区误区真相“项目里的资源都会进包”只有被根引用链追溯到的才进包“没引用的一定不进包”Resources/StreamingAssets全量进包“动态加载靠引用链保证进包”靠Resources/AB/Addr的位置规则“引用存的是文件路径”存的是GUID,路径变引用不断“打包会追溯代码里的Load”追溯不到字符串动态加载“删了meta没关系”meta含GUID,删了引用全断十三、核心要点总结Unity打包引用判定机制 1. 本质:可达性分析(类似GC标记-清除) 从根出发,引用链能到达的进包 2. 三个根(入口): ├── Build场景(按引用链,精确) ├── Resources(⚠️全量无条件进包) └── AB/Addressables(按标记依赖) 次要:Always Included Shaders/Preloaded等 3. 引用如何建立: 靠序列化字段保存的GUID .meta文件存GUID,引用存目标GUID → 打包器沿GUID建依赖图遍历 4. 核心API: AssetDatabase.GetDependencies(path, recursive) → 查完整依赖链(但只查静态引用) 5. ★两套进包机制: ①引用链追溯(静态序列化引用) ②位置/标记规则(Resources全量/AB/Addr标记) 动态加载(字符串Load)靠②,不靠① 6. 动态加载盲区: Resources.Load/Addressables用字符串key 引用图里无此边 → 追溯不到 → 靠资源在Resources/被标记来保证进包 → 否则运行时缺失 7. 技巧: 用ScriptableObject清单静态引用动态资源 → 既能追溯进包,又避免Resources全量浪费 8. 特殊: ├── StreamingAssets:原样拷贝,不判引用 └── Editor文件夹:不进包 9. 验证: Editor Log / Build Report 查实际进包内容 一句话本质: 打包从三个根做引用链可达性遍历, 静态引用靠GUID追溯,动态加载靠位置/标记规则, 两套机制共同决定什么进包。一句话Unity 打包本质是一次可达性分析从三个根Build场景、Resources、AssetBundle/Addressables标记出发沿着序列化字段保存的GUID 引用链递归遍历能到达的资源就打包。但存在两套机制——静态引用靠引用链追溯精确而 Resources 全量进包、AB/Addressables 按标记进包不靠引用链。代码里字符串动态加载Resources.Load 等是引用追溯的盲区只能靠资源恰好在 Resources 或被标记来保证进包否则会运行时缺失。