04-03-源码-构建映射与依赖分析(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 的生成流程如下:

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) 下一篇 :构建管线分析

相关推荐
甲维斯13 小时前
我把“赶 Deadline”和AI做成了一款游戏
人工智能·游戏开发
SmalBox1 天前
04-02-源码-资源收集器(AssetBundleCollector)
unity3d·游戏开发
字节暗面1 天前
客户端上报不可信:游戏服务端数据校验与对局重演设计
c#·游戏开发·服务端校验·反作弊
字节暗面1 天前
游戏服务端如何校验客户端上报数据?四层校验思路详解
游戏开发
甲维斯2 天前
我用AI做了一款「三国吃鸡」,开局孔明灯,落地青龙刀 !
人工智能·游戏·游戏开发
SmalBox2 天前
04-01-源码-编辑器总览
unity3d·游戏开发
Behavior2 天前
Unity 7.0 Alpha 拆解:编辑器干掉 Mono 换 CoreCLR
c#·unity3d·游戏开发
SmalBox3 天前
03-06-架构篇-资源依赖与分组架构
unity3d·游戏开发
鑫鑫哥adam3 天前
一局游戏的时长,凭什么信客户端说的?
unity3d·游戏开发