
1. 项目概述为什么我们需要自定义资源重复检查器在Unity项目开发的后期尤其是当项目体量膨胀到包含成千上万个资源文件时资源管理会变成一个令人头疼的“暗坑”。Addressables系统作为Unity官方推出的资源管理方案确实极大地提升了资源加载和依赖管理的灵活性。然而它更像是一个强大的“物流系统”负责把资源打包、分发、送到正确的地方但它并不擅长在打包前帮你检查仓库里是不是堆了太多一模一样的东西。这就是资源重复问题。想象一下你有一个精美的宝剑模型被用在主角、商店展示和过场动画三个不同的Prefab里。如果你没有使用Addressables或者使用不当这个模型可能会被分别打包进这三个Prefab所在的AssetBundle里。结果就是同一个模型在最终的发布包中出现了三次白白浪费了宝贵的包体和内存空间。Addressables通过共享依赖打包理论上能解决这个问题但前提是资源的引用和标记必须完全正确。在实际开发中由于预制体嵌套、脚本动态加载、资源迁移历史遗留问题等原因隐性的资源重复屡见不鲜。Unity Addressables自带的Analyze功能提供了一些基础规则比如“Check Duplicate Bundle Dependencies”但它更像一个通用扫描仪给出的报告可能不够直观或者无法满足项目特定的检查需求。例如你可能只想检查特定文件夹下的纹理是否重复或者想忽略某些作为模板故意重复使用的UI图集。这时自定义一个AnalyzeRule就显得至关重要。它允许你将团队的资源管理规范直接转化为一个自动化的检查工具集成到日常的CI/CD流程中在资源提交或打包前自动拦截问题把资源优化的主动权牢牢掌握在自己手中。2. AnalyzeRule核心机制深度解析要打造一把好用的“检查器”首先得吃透它的工作原理。AnalyzeRule并不是一个黑盒子它是Addressables Analyze框架下的一套可扩展契约。2.1 AnalyzeRule的生命周期与执行流程一个自定义的AnalyzeRule本质上是一个继承了UnityEditor.AddressableAssets.Build.AnalyzeRules.AnalyzeRule的类。它的执行流程可以概括为以下几个阶段规则注册与列表展示你的规则类需要通过[InitializeOnLoadMethod]特性在Unity编辑器初始化时调用AnalyzeSystem.RegisterNewRuleT()方法进行注册。之后它就会出现在Addressables Analyze窗口的下拉列表中。执行分析CanFix当用户在界面点击“Analyze Selected Rules”时系统会调用规则的CanFix属性。如果返回false则只进行分析如果返回true界面会显示一个“Fix Selected Rules”按钮。对于“检查重复”这类纯粹的分析型规则我们通常返回false。核心分析逻辑RefreshAnalysis这是规则的核心方法。系统会调用RefreshAnalysis并传入一个AddressableAssetSettings对象它包含了当前项目所有的Addressables配置数据。你需要在这个方法内遍历资源执行你的重复检查算法并将结果填充到Results属性中。结果展示与清理分析完成后Results列表中的每一个AnalyzeResult对象都会以树状或列表形式展示在Analyze窗口下方。每个结果包含描述信息、严重程度以及一个用于在Project窗口高亮定位具体资源的SelectionObject。当规则再次执行或窗口刷新时系统会调用ClearAnalysis()方法来清空旧结果。理解这个流程的关键在于AnalyzeRule运行在编辑时它分析的是项目AssetDatabase中的资源引用关系和Addressables的配置数据而非运行时的加载状态。因此它的分析是彻底且权威的。2.2 关键API与数据模型剖析编写规则时你会频繁与以下几个核心API打交道AddressableAssetSettings这是所有配置的入口。通过settings.groups可以遍历所有资源组进而访问组内的每一个AddressableAssetEntry资源条目。AddressableAssetEntry代表一个被标记为Addressable的资源。它的AssetPath是资源在项目中的绝对路径guid是唯一标识parentGroup指向所属组。最重要的是Dependencies属性它能获取该资源所依赖的所有其他资源的路径列表包括非Addressable资源。这是我们查找重复依赖的基石。AnalyzeResult分析结果的载体。创建它时需要提供描述性的resultName。通常我们会把有问题的资源路径作为名称。通过设置severity如MessageType.Warning或MessageType.Error来控制其在界面中的显示颜色。SelectionObject可以设置为一个UnityEngine.Object通常是AssetDatabase.LoadAssetAtPathObject(path)加载得到的对象这样用户点击结果时就能自动跳转。一个常见的误区是只检查AddressableAssetEntry之间的重复。实际上非Addressable资源如被多个Addressable资源引用的公共材质球、纹理的重复打包问题更隐蔽、危害更大。因此我们的检查器必须深入到资源的依赖链中去挖掘。3. 手把手构建自定义资源重复检查器接下来我们从零开始构建一个名为CheckDuplicateDependenciesRule的规则。这个规则的目标是找出所有被多个不同的Addressable资源条目所依赖的非Addressable资源并列出所有引用它的“父资源”帮助开发者定位冗余。3.1 创建规则类与基础框架首先在项目的Editor文件夹下确保是UnityEditor命名空间下创建一个新的C#脚本。using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.AddressableAssets.Build.AnalyzeRules; using UnityEditor.AddressableAssets.Settings; using UnityEngine; namespace MyAddressablesTools.AnalyzeRules { public class CheckDuplicateDependenciesRule : AnalyzeRule { // 规则在界面中显示的名称 public override string ruleName 检查非Addressable资源重复依赖; // 这是一个纯分析规则无法自动修复 public override bool CanFix false; // 核心分析方法 public override ListAnalyzeResult RefreshAnalysis(AddressableAssetSettings settings) { ClearAnalysis(); // 清空旧结果 Results.Clear(); // 核心逻辑将在这里实现 FindDuplicateNonAddressableDependencies(settings); return Results; } // 清理结果 public override void ClearAnalysis() { Results.Clear(); } // 核心查找逻辑 private void FindDuplicateNonAddressableDependencies(AddressableAssetSettings settings) { // 后续步骤将填充这里 } } }3.2 实现重复依赖检测算法算法的核心思路是建立一个“依赖倒排索引”。我们遍历所有Addressable资源收集每个被依赖的非Addressable资源并记录是哪些Addressable资源依赖了它。在FindDuplicateNonAddressableDependencies方法中实现private void FindDuplicateNonAddressableDependencies(AddressableAssetSettings settings) { // 键非Addressable资源的GUID 值依赖它的所有Addressable资源条目的列表 Dictionarystring, ListAddressableAssetEntry dependencyToParentsMap new Dictionarystring, ListAddressableAssetEntry(); // 1. 收集所有Addressable资源条目 ListAddressableAssetEntry allEntries new ListAddressableAssetEntry(); foreach (var group in settings.groups) { if (group null) continue; group.GatherAllAssets(allEntries, true, true, null); } // 2. 遍历每个条目分析其依赖 foreach (AddressableAssetEntry entry in allEntries) { // 获取该条目资源的所有依赖资源路径包括间接依赖 Liststring dependencyPaths new Liststring(); AssetDatabase.GetDependencies(entry.AssetPath, true, dependencyPaths); foreach (string depPath in dependencyPaths) { // 跳过自身 if (depPath entry.AssetPath) continue; // 获取依赖资源的GUID string depGuid AssetDatabase.AssetPathToGUID(depPath); // 关键检查这个依赖资源本身是否被标记为Addressable AddressableAssetEntry depEntry settings.FindAssetEntry(depGuid); if (depEntry ! null) { // 如果依赖资源本身也是Addressable那么Addressables系统会处理它们的打包关系。 // 这里我们只关心非Addressable资源的重复。 continue; } // 将这个非Addressable依赖记录到字典中 if (!dependencyToParentsMap.ContainsKey(depGuid)) { dependencyToParentsMap[depGuid] new ListAddressableAssetEntry(); } // 避免同一个父资源被重复添加虽然不常见 if (!dependencyToParentsMap[depGuid].Contains(entry)) { dependencyToParentsMap[depGuid].Add(entry); } } } // 3. 分析字典找出被多个Addressable资源依赖的非Addressable资源 foreach (var kvp in dependencyToParentsMap) { if (kvp.Value.Count 1) // 被超过一个Addressable资源依赖 { string depGuid kvp.Key; string depPath AssetDatabase.GUIDToAssetPath(depGuid); ListAddressableAssetEntry parentEntries kvp.Value; // 构建结果信息 string resultMsg $资源: {depPath}\n; resultMsg $被 {parentEntries.Count} 个Addressable资源引用\n; foreach (var parent in parentEntries) { resultMsg $ - {parent.address} ({parent.AssetPath})\n; } // 创建分析结果 var result new AnalyzeResult { resultName depPath, severity MessageType.Warning // 作为警告提示 }; // 尝试加载该资源对象以便点击结果时能够定位 UnityEngine.Object depObj AssetDatabase.LoadAssetAtPathUnityEngine.Object(depPath); if (depObj ! null) { result.selectionObject depObj; } Results.Add(result); } } }3.3 注册规则到Analyze系统规则写好了还需要让它出现在Addressables的界面上。在同一命名空间下创建一个静态的注册类。using UnityEditor.AddressableAssets.Build.AnalyzeRules; namespace MyAddressablesTools.AnalyzeRules { // 静态初始化器用于注册规则 [InitializeOnLoadMethod] public static class RegisterCustomRules { static RegisterCustomRules() { // 注册我们的自定义规则 AnalyzeSystem.RegisterNewRuleCheckDuplicateDependenciesRule(); } } }现在打开Unity编辑器进入Window - Asset Management - Addressables - Analyze。在下方的规则列表中你应该能看到“检查非Addressable资源重复依赖”这一项。选中它点击“Analyze Selected Rules”稍等片刻下方就会列出所有被发现重复依赖的非Addressable资源点击任意一条结果Project窗口会自动定位到该资源。4. 高级优化与定制化实践基础的检查器已经能工作但在实际项目中我们往往需要更精细的控制。下面介绍几个关键的优化方向。4.1 添加过滤器与配置参数基础的规则会检查所有资源这可能会产生大量“噪音”。例如我们可能想忽略某些类型的资源如.cs脚本文件或者只检查特定路径下的资源。我们可以为规则添加可配置的过滤器。首先创建一个可序列化的配置类使用ScriptableObjectusing UnityEngine; using System.Collections.Generic; namespace MyAddressablesTools.AnalyzeRules { public class DuplicateCheckConfig : ScriptableObject { [Header(路径过滤)] public Liststring includePaths new Liststring() { Assets/ }; public Liststring excludePaths new Liststring() { Assets/Plugins/, Assets/StreamingAssets/ }; [Header(资源类型过滤)] public Liststring ignoreFileExtensions new Liststring() { .cs, .js, .boo }; [Header(报告阈值)] [Tooltip(被引用次数超过此值才报告)] public int reportThreshold 2; } }然后在规则类中读取这个配置并在分析逻辑中应用过滤private DuplicateCheckConfig GetConfig() { // 从固定路径加载配置如果不存在则创建默认配置 string configPath Assets/Editor/AddressablesDuplicateCheckConfig.asset; var config AssetDatabase.LoadAssetAtPathDuplicateCheckConfig(configPath); if (config null) { config ScriptableObject.CreateInstanceDuplicateCheckConfig(); AssetDatabase.CreateAsset(config, configPath); AssetDatabase.SaveAssets(); Debug.Log($创建默认配置文件于: {configPath}); } return config; } private void FindDuplicateNonAddressableDependencies(AddressableAssetSettings settings) { DuplicateCheckConfig config GetConfig(); // ... 之前的字典初始化和条目收集代码 ... foreach (AddressableAssetEntry entry in allEntries) { Liststring dependencyPaths new Liststring(); AssetDatabase.GetDependencies(entry.AssetPath, true, dependencyPaths); foreach (string depPath in dependencyPaths) { // ... 跳过自身和Addressable资源的检查 ... // 应用路径排除 if (IsPathExcluded(depPath, config)) continue; // 应用文件类型排除 if (IsExtensionExcluded(depPath, config)) continue; // ... 记录到字典的代码 ... } } // 在生成结果时应用报告阈值 foreach (var kvp in dependencyToParentsMap) { if (kvp.Value.Count config.reportThreshold) // 使用配置的阈值 { // ... 创建结果的代码 ... } } } private bool IsPathExcluded(string path, DuplicateCheckConfig config) { // 检查是否在排除路径中 foreach (var excludePath in config.excludePaths) { if (path.StartsWith(excludePath)) return true; } // 检查是否不在包含路径中如果includePaths不为空 if (config.includePaths.Count 0) { bool isIncluded false; foreach (var includePath in config.includePaths) { if (path.StartsWith(includePath)) { isIncluded true; break; } } return !isIncluded; } return false; } private bool IsExtensionExcluded(string path, DuplicateCheckConfig config) { string ext System.IO.Path.GetExtension(path).ToLower(); return config.ignoreFileExtensions.Contains(ext); }提示通过ScriptableObject配置我们可以让策划或美术同学也能轻松修改检查规则而无需接触代码。将配置文件放在Assets/Editor下可以确保它不会被构建到游戏包中。4.2 性能优化策略当项目资源极多时遍历所有依赖可能会比较耗时。我们可以从以下几点优化缓存AssetDatabase调用AssetDatabase.AssetPathToGUID和AssetDatabase.LoadAssetAtPath都是相对较慢的IO操作。对于在循环中频繁使用的路径-GUID转换可以预先建立缓存字典。增量分析与缓存结果AnalyzeRule的RefreshAnalysis每次都是全量分析。对于大型项目可以设计增量分析逻辑只分析自上次检查以来有变化的资源组或资源。可以将上次分析的结果如资源哈希值存储起来本次只比较哈希值发生变化的资源。异步与进度显示在RefreshAnalysis中可以通过EditorUtility.DisplayProgressBar来显示分析进度避免编辑器假死。对于极其复杂的分析可以考虑将部分计算放到后台线程但需要注意Unity API的线程安全性大部分Editor API只能在主线程调用。选择性分析在规则界面中可以结合AddressableAssetGroup让用户选择只分析特定的资源组而不是整个项目。4.3 生成可视化报告除了在Analyze窗口查看我们还可以将结果导出为更友好的格式如HTML、Markdown或CSV方便在团队内部分享和跟踪。在规则类中添加一个方法public void ExportResultsToCSV(string filePath) { if (Results null || Results.Count 0) { Debug.LogWarning(没有可导出的分析结果。); return; } System.Text.StringBuilder csv new System.Text.StringBuilder(); // 写入表头 csv.AppendLine(资源路径,引用次数,引用者列表); foreach (var analyzeResult in Results) { // 这里需要从resultName或自定义数据结构中提取信息 // 假设我们的resultName格式是“路径 [被N个资源引用]” string line $\{analyzeResult.resultName}\,; // 解析引用次数和列表实际项目中可能需要更结构化的数据存储 // 此处仅为示例 csv.AppendLine(line); } System.IO.File.WriteAllText(filePath, csv.ToString()); Debug.Log($分析结果已导出至: {filePath}); EditorUtility.RevealInFinder(filePath); // 在文件管理器中显示 }然后你可以通过一个自定义的EditorWindow来触发这个导出功能或者直接在规则类的上下文菜单中添加选项。5. 实战中的常见问题与排查技巧在实际集成和使用自定义AnalyzeRule的过程中你可能会遇到一些典型问题。5.1 规则未在Analyze窗口中显示检查点1脚本位置与编译时机。确保你的规则类脚本放在Assets下的任意Editor文件夹中如Assets/Editor或Assets/Plugins/SomePlugin/Editor。只有放在Editor文件夹下的脚本才会在Unity编辑器环境下编译。检查Unity编辑器控制台是否有编译错误。检查点2注册代码是否执行。确保[InitializeOnLoadMethod]静态构造函数被正确调用。你可以在其中加一句Debug.Log(正在注册自定义Analyze规则...);来验证。有时如果脚本编译在Addressables系统初始化之后可能需要手动触发一次注册。重启Unity编辑器通常是最直接的方法。检查点3规则名称冲突。确保ruleName是唯一的不与内置或其他第三方规则重名。5.2 分析结果为空或不准检查点1资源是否已正确标记为Addressable。我们的规则检查的是非Addressable资源的重复依赖。确保你测试的资源如图片、预制体已经通过Addressables窗口被分配到了某个Group中。检查点2依赖获取是否完整。AssetDatabase.GetDependencies(assetPath, recursive)的第二个参数recursive设置为true以确保获取所有嵌套依赖。有时通过脚本动态加载的资源如Resources.Load或AssetBundle API管理的资源其依赖关系可能无法通过静态分析完全获取这是此类工具的局限性。检查点3过滤条件是否过严。检查你的配置如果有中的includePaths、excludePaths和reportThreshold是否可能过滤掉了所有结果。5.3 分析过程编辑器卡顿或无响应应对策略1分帧处理。对于超大型项目在RefreshAnalysis的循环中可以每处理一定数量的资源如100个就执行一次EditorUtility.DisplayProgressBar并yield return null;如果方法改为协程。但这需要修改规则基类的接口实现起来更复杂通常更简单的做法是显示一个进度条让用户知道程序在运行。应对策略2性能剖析。使用Unity Profiler分析RefreshAnalysis方法的性能瓶颈。通常是AssetDatabase.GetDependencies或频繁的路径/GUID转换操作。应用前面提到的缓存策略能显著提升性能。应对策略3提供范围选择。如前所述允许用户只分析选中的Group或特定文件夹能立即减少分析量。5.4 如何集成到CI/CD流程自动化检查是发挥其最大价值的关键。你可以通过命令行来触发Addressables的Analyze。编写编辑器脚本创建一个静态方法专门用于执行你的自定义规则并输出结果。public static void RunDuplicateCheckInBatchMode() { var settings AddressableAssetSettingsDefaultObject.Settings; if (settings null) { /* 处理错误 */ return; } var rule new CheckDuplicateDependenciesRule(); var results rule.RefreshAnalysis(settings); if (results.Count 0) { // 将结果写入日志文件或CI系统报告 foreach (var r in results) { System.Console.WriteLine($[DUPLICATE_WARNING] {r.resultName}); } // 如果发现重复可以让CI流程失败 EditorApplication.Exit(1); } else { EditorApplication.Exit(0); } }命令行调用在CI服务器上使用类似以下的命令来执行Unity并运行这个方法。Unity.exe -batchmode -quit -projectPath [你的项目路径] -executeMethod MyAddressablesTools.AnalyzeRules.BatchModeRunner.RunDuplicateCheckInBatchMode处理结果CI脚本根据Unity的退出代码EditorApplication.Exit的参数来判断检查是否通过。如果失败可以将生成的结果文件作为构建产物保存供开发者查看。注意在批处理模式下所有对编辑器UI的调用如EditorUtility.DisplayDialog都会失败确保你的规则逻辑在无头模式下能稳定运行。通过以上步骤你不仅得到了一个功能强大的自定义资源重复检查器更掌握了一套扩展Addressables分析能力的方法论。这套工具能随着项目成长而不断进化成为保障项目资源健康、优化包体大小的忠实守卫。