源码-构建映射与依赖分析(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 的生成流程如下:
markdown
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 的核心数据结构:
csharp
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 依赖分析的源码实现
依赖分析的核心代码:
csharp
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 循环依赖检测
循环依赖检测的源码实现:
csharp
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 共享资源识别
共享资源识别的源码实现:
csharp
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 分配的核心代码:
csharp
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 验证的实现
csharp
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 缓存的实现
csharp
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) 下一篇 :构建管线分析