04-03-YooAsset源码-Unity构建映射与依赖分析(BuildMap)
源码-构建映射与依赖分析(BuildMap)
篇章:04-源码深度-编辑器模块
阅读时间:约 30 分钟
前置知识:了解 AssetBundleCollector 模块
一、引言
本章将深入解析 YooAsset 构建映射与依赖分析(BuildMap)的源码实现。BuildMap 是 YooAsset 编辑器端的核心数据结构,记录了资源到 Bundle 的映射关系以及 Bundle 之间的依赖关系。深入理解 BuildMap 的实现,是掌握 YooAsset 资源打包机制的关键。
BuildMap 整合了所有 Collector 的收集结果,进行了依赖分析、冗余检测和打包优化,是资源打包的蓝图。本章将从源码层面深入解析 BuildMap 的实现。
二、BuildMap 概述
2.1 BuildMap 的作用
BuildMap 在 YooAsset 中起到核心作用:
打包依据:
BuildMap 是 Builder 打包的依据。Builder 根据 BuildMap 执行实际的 AssetBundle 打包操作。
依赖管理:
BuildMap 记录了 Bundle 之间的依赖关系,避免重复打包共享资源。
优化分析:
BuildMap 可以分析资源冗余、循环依赖等问题,提供优化建议。
数据桥梁:
BuildMap 是 Collector 和 Builder 之间的数据桥梁。Collector 输出 BuildMap,Builder 消费 BuildMap。
2.2 BuildMap 的生成流程
BuildMap 的生成流程如下:
BuildMap 生成流程 1. 收集所有 Collector 输出 2. 合并所有资源信息 3. 分析资源依赖关系 4. 检测循环依赖 5. 计算共享资源 6. 分配 Bundle 7. 验证 BuildMap 8. 输出最终 BuildMap收集所有 Collector 输出详解:
从所有 Collector 收集资源信息。每个 Collector 输出一个 CollectAssetInfo 列表。
合并所有资源信息详解:
合并所有 Collector 的输出,去除重复资源,生成统一的资源列表。
分析资源依赖关系详解:
使用 Unity 的 AssetDatabase.GetDependencies API 分析资源之间的依赖关系。
检测循环依赖详解:
使用图算法检测依赖图中是否存在循环依赖。如果存在循环依赖,抛出错误。
计算共享资源详解:
识别被多个 Bundle 共享的资源,决定是否提取为共享 Bundle。
分配 Bundle 详解:
根据 Pack Rule 决定每个资源属于哪个 Bundle。
验证 BuildMap 详解:
验证 BuildMap 的完整性,包括所有资源都有 Bundle 分配、所有依赖关系都正确等。
2.3 BuildMap 的数据结构
BuildMap 的核心数据结构:
public class BuildMap { // 包名 public string PackageName; // 包版本 public string PackageVersion; // Bundle 列表 public List<BuildBundleInfo> Bundles = new List<BuildBundleInfo>(); // 资源到 Bundle 的映射 public Dictionary<string, string> AssetToBundle = new Dictionary<string, string>(); // 资源依赖关系 public Dictionary<string, List<string>> AssetDependencies = new Dictionary<string, List<string>>(); } public class BuildBundleInfo { public string BundleName; public string FileName; public List<string> Assets = new List<string>(); public List<string> Dependencies = new List<string>(); public int MainAssetCount; public long FileSize; public string Hash; public EBundleType BundleType; }BuildBundleInfo 详解:
BuildBundleInfo 描述了一个 Bundle 的所有信息,包括 Bundle 名称、包含的资源、依赖的 Bundle、文件大小、Hash 等。
AssetToBundle 详解:
AssetToBundle 记录了资源到 Bundle 的映射关系,便于快速查找资源所属的 Bundle。
AssetDependencies 详解:
AssetDependencies 记录了资源之间的依赖关系,用于分析资源依赖图。
三、依赖分析实现
3.1 依赖分析的作用
依赖分析是 BuildMap 的核心功能:
构建依赖图:
分析所有资源之间的依赖关系,构建完整的依赖图。
检测循环依赖:
检测依赖图中是否存在循环依赖。如果存在,需要警告开发者。
识别共享资源:
识别被多个 Bundle 共享的资源,决定是否提取为共享 Bundle。
优化打包:
根据依赖关系优化打包策略,避免重复打包。
3.2 依赖分析的源码实现
依赖分析的核心代码:
public class DependencyAnalyzer { public Dictionary<string, List<string>> AnalyzeDependencies(List<string> assets) { Dictionary<string, List<string>> dependencies = new Dictionary<string, List<string>>(); foreach (var asset in assets) { // 使用 Unity API 获取资源依赖 string[] deps = AssetDatabase.GetDependencies(asset, true); dependencies[asset] = new List<string>(deps); } return dependencies; } }AssetDatabase.GetDependencies 详解:
AssetDatabase.GetDependencies 是 Unity 提供的 API,用于获取资源的所有依赖(包括直接依赖和间接依赖)。
递归依赖详解:
GetDependencies(asset, true) 会递归获取资源的所有依赖,包括间接依赖。
3.3 循环依赖检测
循环依赖检测的源码实现:
public class CycleDetector { public List<List<string>> DetectCycles(Dictionary<string, List<string>> graph) { List<List<string>> cycles = new List<List<string>>(); HashSet<string> visited = new HashSet<string>(); HashSet<string> inStack = new HashSet<string>(); Stack<string> path = new Stack<string>(); foreach (var node in graph.Keys) { DFS(node, graph, visited, inStack, path, cycles); } return cycles; } private void DFS(string node, Dictionary<string, List<string>> graph, HashSet<string> visited, HashSet<string> inStack, Stack<string> path, List<List<string>> cycles) { if (inStack.Contains(node)) { // 发现循环 var cyclePath = path.ToList(); int index = cyclePath.IndexOf(node); cycles.Add(cyclePath.GetRange(index, cyclePath.Count - index)); return; } if (visited.Contains(node)) return; visited.Add(node); inStack.Add(node); path.Push(node); if (graph.ContainsKey(node)) { foreach (var neighbor in graph[node]) { DFS(neighbor, graph, visited, inStack, path, cycles); } } path.Pop(); inStack.Remove(node); } }DFS 详解:
使用深度优先搜索(DFS)检测循环依赖。
循环依赖的处理:
如果发现循环依赖,需要警告开发者并提示修复。
3.4 共享资源识别
共享资源识别的源码实现:
public class SharedAssetExtractor { public List<string> FindSharedAssets(BuildMap buildMap) { Dictionary<string, int> assetRefCount = new Dictionary<string, int>(); // 统计每个资源的引用次数 foreach (var bundle in buildMap.Bundles) { foreach (var asset in bundle.Assets) { if (!assetRefCount.ContainsKey(asset)) assetRefCount[asset] = 0; assetRefCount[asset]++; } } // 找出被多个 Bundle 引用的资源 List<string> sharedAssets = new List<string>(); foreach (var kvp in assetRefCount) { if (kvp.Value > 1) { sharedAssets.Add(kvp.Key); } } return sharedAssets; } }共享资源的处理:
共享资源通常会被提取到独立的 Bundle 中,避免重复打包。
四、Bundle 分配策略
4.1 打包规则类型
YooAsset 支持多种打包规则:
Collector 级别打包:
每个 Collector 单独打包为一个 Bundle。适用于独立模块。
Group 级别打包:
Group 内所有 Collector 合并打包为一个 Bundle。适用于整体模块。
目录级别打包:
按顶级目录打包,每个目录一个 Bundle。适用于按目录组织的资源。
自定义打包:
开发者可以自定义打包规则,根据自己的需求打包。
4.2 Bundle 分配的源码实现
Bundle 分配的核心代码:
public class BundleAllocator { public List<BuildBundleInfo> AllocateBundles(List<CollectAssetInfo> assets, AssetBundlePackage package) { List<BuildBundleInfo> bundles = new List<BuildBundleInfo>(); foreach (var group in package.Groups) { switch (group.PackRuleType) { case EGroupPackRuleType.CollectorPack: // 每个 Collector 单独打包 foreach (var collector in group.Collectors) { BuildBundleInfo bundle = CreateBundleFromCollector(collector); bundles.Add(bundle); } break; case EGroupPackRuleType.GroupPack: // Group 合并打包 BuildBundleInfo groupBundle = CreateBundleFromGroup(group); bundles.Add(groupBundle); break; case EGroupPackRuleType.TopDirectoryPack: // 按顶级目录打包 var topDirBundles = CreateBundlesByTopDirectory(group); bundles.AddRange(topDirBundles); break; } } return bundles; } }Collector 级别打包详解:
每个 Collector 的所有资源打包为一个 Bundle。Bundle 名称默认为 Collector 名称。
Group 级别打包详解:
Group 内所有 Collector 的资源合并打包为一个 Bundle。Bundle 名称默认为 Group 名称。
目录级别打包详解:
按顶级目录打包,每个顶级目录的所有资源打包为一个 Bundle。Bundle 名称默认为目录名。
4.3 Bundle 命名规则
Bundle 的命名规则需要考虑:
唯一性:
Bundle 名称必须唯一,不能重复。
可读性:
Bundle 名称应该易读,便于调试和优化。
长度限制:
Bundle 名称长度有限制,不能过长。
特殊字符:
Bundle 名称不能包含特殊字符。
YooAsset 的命名规则:
YooAsset 默认使用group/collector作为 Bundle 名称,便于阅读和理解。
五、BuildMap 验证
5.1 验证的作用
BuildMap 验证用于确保 BuildMap 的正确性:
- 检查所有资源都有 Bundle 分配
- 检查所有依赖关系都正确
- 检查 Bundle 名称是否唯一
- 检查 Bundle 大小是否合理
- 检查是否存在循环依赖
5.2 验证的实现
public class BuildMapValidator { public ValidationResult Validate(BuildMap buildMap) { ValidationResult result = new ValidationResult(); // 检查 Bundle 名称唯一性 HashSet<string> bundleNames = new HashSet<string>(); foreach (var bundle in buildMap.Bundles) { if (!bundleNames.Add(bundle.BundleName)) { result.AddError($"Duplicate bundle name: {bundle.BundleName}"); } } // 检查资源完整性 foreach (var asset in buildMap.AssetToBundle.Keys) { if (!File.Exists(asset)) { result.AddError($"Asset not found: {asset}"); } } // 检查循环依赖 var cycles = new CycleDetector().DetectCycles(buildMap.AssetDependencies); foreach (var cycle in cycles) { result.AddError($"Cycle detected: {string.Join(" -> ", cycle)}"); } return result; } }Bundle 名称唯一性检查详解:
使用 HashSet 检查 Bundle 名称是否唯一。
资源完整性检查详解:
使用 File.Exists 检查所有资源是否存在。
循环依赖检查详解:
使用 CycleDetector 检查依赖图中是否存在循环依赖。
5.3 错误处理
BuildMap 验证发现错误时,需要妥善处理:
严重错误:
- 循环依赖
- 资源不存在
- Bundle 名称重复
警告:
- Bundle 过大
- 资源过多
- 依赖过深
错误处理策略:
- 严重错误:阻止构建,要求修复
- 警告:提示开发者,但允许构建继续
六、BuildMap 缓存
6.1 缓存的作用
BuildMap 缓存用于提高构建效率:
避免重复计算:
对于未变化的部分,不需要重新计算 BuildMap。
支持增量构建:
通过对比前后两次 BuildMap,识别变化的 Bundle。
提高构建速度:
减少构建时间,特别是对于大型项目。
6.2 缓存的实现
public class BuildMapCache { public void Save(BuildMap buildMap, string cachePath) { string json = JsonUtility.ToJson(buildMap, true); File.WriteAllText(cachePath, json); } public BuildMap Load(string cachePath) { if (!File.Exists(cachePath)) return null; string json = File.ReadAllText(cachePath); return JsonUtility.FromJson<BuildMap>(json); } public BuildMap Diff(BuildMap oldMap, BuildMap newMap) { // 对比两个 BuildMap,找出变化的 Bundle var changedBundles = new List<BuildBundleInfo>(); foreach (var newBundle in newMap.Bundles) { var oldBundle = oldMap.Bundles.FirstOrDefault(b => b.BundleName == newBundle.BundleName); if (oldBundle == null || !IsBundleEqual(oldBundle, newBundle)) { changedBundles.Add(newBundle); } } // 创建新的 BuildMap,只包含变化的 Bundle BuildMap diffMap = new BuildMap(); diffMap.Bundles = changedBundles; return diffMap; } }缓存保存详解:
将 BuildMap 序列化为 JSON 并保存到文件。
缓存加载详解:
从文件读取 JSON 并反序列化为 BuildMap 对象。
差异对比详解:
对比两个 BuildMap,找出变化的 Bundle。
6.3 缓存的清理
BuildMap 缓存需要定期清理:
- 清理过期的缓存
- 清理无效的缓存
- 限制缓存大小
清理策略:
- 按时间清理:清理超过指定时间的缓存
- 按大小清理:限制缓存总大小
- 按版本清理:清理旧版本的缓存
七、总结
本章深入解析了 YooAsset 构建映射与依赖分析(BuildMap)的源码实现,包括:
- BuildMap 概述:BuildMap 的作用、生成流程、数据结构
- 依赖分析实现:依赖分析的源码、循环依赖检测、共享资源识别
- Bundle 分配策略:打包规则类型、Bundle 分配实现、命名规则
- BuildMap 验证:验证的作用、实现、错误处理
- BuildMap 缓存:缓存的作用、实现、清理
通过深入理解 BuildMap 的源码实现,开发者可以更好地使用 YooAsset 的资源打包功能,并根据自己的需求进行扩展和优化。
上一篇:资源收集模块(AssetBundleCollector)
下一篇:构建管线分析
