Files
5.24BuYu/Assets/MyGame/Framework/Editor/AssetDependencyAnalyzer.cs
T

960 lines
32 KiB
C#
Raw Normal View History

2026-07-28 22:56:35 +08:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
/// <summary>
/// 资源依赖分析器
/// 用于查找某个 Prefab / 资源 关联的所有依赖资源,并标记其在 Addressables 中的归属(Group、Address、Labels),
/// 供 AA 分包调查使用。
/// 菜单:Tools → 资源依赖分析器
/// </summary>
public class AssetDependencyAnalyzer : EditorWindow
{
2026-07-29 22:59:26 +08:00
// ============================ 日志开关 ============================
// 启用: Player Settings → Scripting Define Symbols 添加 ENABLE_EDITOR_LOG
// 关闭: 删除该符号。未定义时所有 Log/LogError 调用被编译器自动移除,零开销
[System.Diagnostics.Conditional("ENABLE_EDITOR_LOG")]
private static void Log(string msg) { Debug.Log(msg); }
[System.Diagnostics.Conditional("ENABLE_EDITOR_LOG")]
private static void LogError(string msg) { Debug.LogError(msg); }
2026-07-28 22:56:35 +08:00
#region 内部类
[Serializable]
public class DependencyInfo
{
public string assetPath;
public string guid;
public Type type;
public string typeName;
public bool isInAddressables;
public string aaAddress;
public string aaGroup;
public List<string> aaLabels = new List<string>();
public long fileSizeBytes;
public string fileSizeReadable;
}
#endregion
#region 字段
private Vector2 leftScroll;
private Vector2 rightScroll;
private Vector2 statsScroll;
// 输入:单个目标
private UnityEngine.Object targetAsset;
// 批量目标
private List<UnityEngine.Object> batchTargets = new List<UnityEngine.Object>();
private bool useBatchMode = false;
// 收集结果
private List<DependencyInfo> dependencies = new List<DependencyInfo>();
private Dictionary<string, DependencyInfo> dependencyMap = new Dictionary<string, DependencyInfo>();
// 反向查找(谁引用了这个资源)
private readonly Dictionary<string, List<string>> reverseRefMap = new Dictionary<string, List<string>>();
// 过滤
private enum FilterMode
{
All,
InAddressables,
NotInAddressables
}
private FilterMode filterMode = FilterMode.All;
private string typeFilter = "全部";
private string groupFilter = "全部";
private string labelFilter = "全部";
private string pathKeyword = "";
// 展示
private bool showStats = true;
private bool showFilter = true;
private bool showBatchList = true;
// 缓存
private AddressableAssetSettings aaSettings;
private List<string> allGroups = new List<string>();
private List<string> allLabels = new List<string>();
private List<string> allTypes = new List<string>();
private bool includeScripts = false;
private bool deepAnalyze = true;
#endregion
#region 菜单与初始化
[MenuItem("Tools/资源依赖分析器", false, 200)]
public static void OpenWindow()
{
var window = GetWindow<AssetDependencyAnalyzer>("资源依赖分析器");
window.minSize = new Vector2(1000, 600);
window.Show();
}
private void OnEnable()
{
RefreshAACache();
}
private void RefreshAACache()
{
aaSettings = AddressableAssetSettingsDefaultObject.Settings;
allGroups.Clear();
allLabels.Clear();
if (aaSettings != null)
{
foreach (var g in aaSettings.groups)
{
if (g != null) allGroups.Add(g.Name);
}
allLabels.AddRange(aaSettings.GetLabels());
}
}
#endregion
#region 主GUI
private void OnGUI()
{
DrawToolbar();
EditorGUILayout.Space(4);
DrawTargetSelector();
if (showFilter)
{
EditorGUILayout.Space(4);
DrawFilterBar();
}
if (showStats && dependencies.Count > 0)
{
EditorGUILayout.Space(4);
DrawStats();
}
EditorGUILayout.Space(4);
DrawDependencyList();
}
#endregion
#region 工具栏
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("分析依赖", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
AnalyzeAll();
}
if (GUILayout.Button("反向查找引用", EditorStyles.toolbarButton, GUILayout.Width(100)))
{
ReverseLookupSelected();
}
if (GUILayout.Button("导出报告", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
ExportReport();
}
if (GUILayout.Button("复制AA地址", EditorStyles.toolbarButton, GUILayout.Width(90)))
{
CopySelectedAddresses();
}
if (GUILayout.Button("加入AA(选中)", EditorStyles.toolbarButton, GUILayout.Width(110)))
{
AddSelectedToAddressables();
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("刷新AA缓存", EditorStyles.toolbarButton, GUILayout.Width(90)))
{
RefreshAACache();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
showFilter = GUILayout.Toggle(showFilter, "过滤", EditorStyles.toolbarButton, GUILayout.Width(50));
showStats = GUILayout.Toggle(showStats, "统计", EditorStyles.toolbarButton, GUILayout.Width(50));
showBatchList = GUILayout.Toggle(showBatchList, "批量列表", EditorStyles.toolbarButton, GUILayout.Width(70));
useBatchMode = GUILayout.Toggle(useBatchMode, "批量模式", EditorStyles.toolbarButton, GUILayout.Width(70));
GUILayout.FlexibleSpace();
includeScripts = GUILayout.Toggle(includeScripts, "包含脚本", GUILayout.Width(80));
deepAnalyze = GUILayout.Toggle(deepAnalyze, "递归依赖", GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
}
#endregion
#region 目标选择
private void DrawTargetSelector()
{
EditorGUILayout.BeginVertical("box");
if (!useBatchMode)
{
EditorGUILayout.LabelField("目标资源(Prefab / 贴图 / 音频 / 材质等,可拖拽)", EditorStyles.boldLabel);
var newObj = EditorGUILayout.ObjectField("目标", targetAsset, typeof(UnityEngine.Object), false);
if (newObj != targetAsset)
{
targetAsset = newObj;
}
EditorGUILayout.HelpBox(
"操作说明:\n" +
"1. 拖拽或选择目标资源(通常是 Prefab)。\n" +
"2. 点击「分析依赖」递归收集所有依赖资源。\n" +
"3. 通过过滤栏查看 AA 内 / 非 AA 资源,便于分包决策。\n" +
"4. 选中非 AA 资源后点击「加入AA」可直接添加到指定 Group。\n" +
"5. 「反向查找引用」会扫描所有 AA 条目,找出谁引用了当前选中资源。",
MessageType.Info);
}
else
{
EditorGUILayout.LabelField("批量目标列表(可拖拽多个资源到下方区域)", EditorStyles.boldLabel);
// 拖拽区域
Rect dropRect = GUILayoutUtility.GetRect(0, 80, GUILayout.ExpandWidth(true));
GUI.Box(dropRect, "拖拽资源到此处添加到批量列表");
HandleDragDrop(dropRect);
// 列表显示
if (showBatchList && batchTargets.Count > 0)
{
leftScroll = EditorGUILayout.BeginScrollView(leftScroll, GUILayout.Height(120));
for (int i = 0; i < batchTargets.Count; i++)
{
EditorGUILayout.BeginHorizontal();
var obj = EditorGUILayout.ObjectField(batchTargets[i], typeof(UnityEngine.Object), false);
if (obj != batchTargets[i]) batchTargets[i] = obj;
if (GUILayout.Button("×", GUILayout.Width(20)))
{
batchTargets.RemoveAt(i);
break;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("清空列表", GUILayout.Width(80)))
{
batchTargets.Clear();
}
EditorGUILayout.LabelField($"共 {batchTargets.Count} 个目标", EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
}
}
EditorGUILayout.EndVertical();
}
private void HandleDragDrop(Rect dropRect)
{
var evt = Event.current;
if (!dropRect.Contains(evt.mousePosition)) return;
if (evt.type == EventType.DragUpdated)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
evt.Use();
}
else if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
foreach (var path in DragAndDrop.paths)
{
var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
if (obj != null && !batchTargets.Contains(obj))
{
batchTargets.Add(obj);
}
}
evt.Use();
}
}
#endregion
#region 过滤栏
private void DrawFilterBar()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
filterMode = (FilterMode)EditorGUILayout.EnumPopup("归属", filterMode, GUILayout.Width(180));
// 类型过滤
var typeOptions = new List<string> { "全部" };
typeOptions.AddRange(allTypes);
int typeIdx = Mathf.Max(0, typeOptions.IndexOf(typeFilter));
typeIdx = EditorGUILayout.Popup("类型", typeIdx, typeOptions.ToArray(), GUILayout.Width(180));
typeFilter = typeOptions[typeIdx];
// Group过滤
var groupOptions = new List<string> { "全部", "(非AA)" };
groupOptions.AddRange(allGroups);
int gIdx = Mathf.Max(0, groupOptions.IndexOf(groupFilter));
gIdx = EditorGUILayout.Popup("Group", gIdx, groupOptions.ToArray(), GUILayout.Width(180));
groupFilter = groupOptions[gIdx];
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
var labelOptions = new List<string> { "全部", "(无标签)" };
labelOptions.AddRange(allLabels);
int lIdx = Mathf.Max(0, labelOptions.IndexOf(labelFilter));
lIdx = EditorGUILayout.Popup("Label", lIdx, labelOptions.ToArray(), GUILayout.Width(180));
labelFilter = labelOptions[lIdx];
pathKeyword = EditorGUILayout.TextField("路径关键字", pathKeyword, GUILayout.Width(220));
if (GUILayout.Button("清空过滤", GUILayout.Width(80)))
{
filterMode = FilterMode.All;
typeFilter = "全部";
groupFilter = "全部";
labelFilter = "全部";
pathKeyword = "";
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
#endregion
#region 统计
private void DrawStats()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("统计", EditorStyles.boldLabel);
int totalCount = dependencies.Count;
int aaCount = dependencies.Count(d => d.isInAddressables);
int nonAACount = totalCount - aaCount;
long totalSize = dependencies.Sum(d => d.fileSizeBytes);
EditorGUILayout.LabelField($"总依赖数: {totalCount} AA内: {aaCount} 非AA: {nonAACount} 总大小: {FormatSize(totalSize)}");
// 按类型统计
statsScroll = EditorGUILayout.BeginScrollView(statsScroll, GUILayout.Height(140));
var byType = dependencies.GroupBy(d => d.typeName).OrderByDescending(g => g.Count());
EditorGUILayout.LabelField("按类型统计:", EditorStyles.miniBoldLabel);
foreach (var grp in byType)
{
long s = grp.Sum(d => d.fileSizeBytes);
int aaInGroup = grp.Count(d => d.isInAddressables);
EditorGUILayout.LabelField($" {grp.Key}: {grp.Count()} 个 (AA内 {aaInGroup}) 大小 {FormatSize(s)}", EditorStyles.miniLabel);
}
// 按 Group 统计
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("按 AA Group 统计:", EditorStyles.miniBoldLabel);
var byGroup = dependencies.Where(d => d.isInAddressables).GroupBy(d => d.aaGroup).OrderByDescending(g => g.Count());
foreach (var grp in byGroup)
{
long s = grp.Sum(d => d.fileSizeBytes);
EditorGUILayout.LabelField($" {grp.Key}: {grp.Count()} 个 大小 {FormatSize(s)}", EditorStyles.miniLabel);
}
// 非 AA 资源按目录统计
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("非AA资源按目录统计:", EditorStyles.miniBoldLabel);
var nonAAByDir = dependencies
.Where(d => !d.isInAddressables)
.GroupBy(d => Path.GetDirectoryName(d.assetPath)?.Replace('\\', '/'))
.OrderByDescending(g => g.Count());
foreach (var grp in nonAAByDir)
{
long s = grp.Sum(d => d.fileSizeBytes);
EditorGUILayout.LabelField($" {grp.Key}: {grp.Count()} 个 大小 {FormatSize(s)}", EditorStyles.miniLabel);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
#endregion
#region 依赖列表
private void DrawDependencyList()
{
if (dependencies.Count == 0)
{
EditorGUILayout.HelpBox("尚未分析。请选择目标资源后点击「分析依赖」。", MessageType.Info);
return;
}
var filtered = ApplyFilter(dependencies);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"依赖列表 (过滤后: {filtered.Count} / {dependencies.Count})", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
if (GUILayout.Button("全选非AA", GUILayout.Width(80)))
{
SelectAllNonAA();
}
EditorGUILayout.EndHorizontal();
rightScroll = EditorGUILayout.BeginScrollView(rightScroll);
// 表头
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField("", GUILayout.Width(20));
EditorGUILayout.LabelField("资源路径", GUILayout.Width(300));
EditorGUILayout.LabelField("类型", GUILayout.Width(80));
EditorGUILayout.LabelField("大小", GUILayout.Width(70));
EditorGUILayout.LabelField("AA归属", GUILayout.Width(120));
EditorGUILayout.LabelField("Address", GUILayout.Width(150));
EditorGUILayout.LabelField("Labels", GUILayout.Width(150));
EditorGUILayout.EndHorizontal();
foreach (var info in filtered)
{
DrawDependencyRow(info);
}
EditorGUILayout.EndScrollView();
}
private List<DependencyInfo> GetFilteredCache()
{
return ApplyFilter(dependencies);
}
private void SelectAllNonAA()
{
var filtered = GetFilteredCache();
var nonAA = filtered.Where(d => !d.isInAddressables).Select(d => d.assetPath).ToList();
var objs = new List<UnityEngine.Object>();
foreach (var p in nonAA)
{
var o = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(p);
if (o != null) objs.Add(o);
}
Selection.objects = objs.ToArray();
2026-07-29 22:59:26 +08:00
Log($"[依赖分析器] 已选中 {objs.Count} 个非AA资源到 Project 视图");
2026-07-28 22:56:35 +08:00
}
private void DrawDependencyRow(DependencyInfo info)
{
EditorGUILayout.BeginHorizontal(GUIStyle.none);
// 复选框(仅展示,不维护勾选状态,使用 Selection.paths 作为统一选择机制)
bool selected = Selection.objects != null && Selection.objects.Any(o =>
o != null && AssetDatabase.GetAssetPath(o) == info.assetPath);
bool newSel = EditorGUILayout.Toggle(selected, GUILayout.Width(20));
if (newSel != selected)
{
var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(info.assetPath);
if (obj != null)
{
var list = Selection.objects != null ? Selection.objects.ToList() : new List<UnityEngine.Object>();
if (newSel && !list.Contains(obj)) list.Add(obj);
if (!newSel && list.Contains(obj)) list.Remove(obj);
Selection.objects = list.ToArray();
}
}
// 路径(可点击定位)
if (GUILayout.Button(info.assetPath, EditorStyles.label, GUILayout.Width(300)))
{
var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(info.assetPath);
if (obj != null)
{
EditorGUIUtility.PingObject(obj);
Selection.activeObject = obj;
}
}
EditorGUILayout.LabelField(info.typeName, GUILayout.Width(80));
EditorGUILayout.LabelField(info.fileSizeReadable, GUILayout.Width(70));
// AA 归属(彩色显示)
var oldColor = GUI.color;
GUI.color = info.isInAddressables ? new Color(0.6f, 1f, 0.6f) : new Color(1f, 0.6f, 0.6f);
EditorGUILayout.LabelField(info.isInAddressables ? info.aaGroup : "非AA", GUILayout.Width(120));
GUI.color = oldColor;
EditorGUILayout.LabelField(info.aaAddress ?? "-", GUILayout.Width(150));
EditorGUILayout.LabelField(info.aaLabels.Count > 0 ? string.Join(",", info.aaLabels) : "-", GUILayout.Width(150));
EditorGUILayout.EndHorizontal();
}
private List<DependencyInfo> ApplyFilter(List<DependencyInfo> source)
{
var result = source;
switch (filterMode)
{
case FilterMode.InAddressables:
result = result.Where(d => d.isInAddressables).ToList();
break;
case FilterMode.NotInAddressables:
result = result.Where(d => !d.isInAddressables).ToList();
break;
}
if (typeFilter != "全部")
result = result.Where(d => d.typeName == typeFilter).ToList();
if (groupFilter == "(非AA)")
result = result.Where(d => !d.isInAddressables).ToList();
else if (groupFilter != "全部")
result = result.Where(d => d.isInAddressables && d.aaGroup == groupFilter).ToList();
if (labelFilter == "(无标签)")
result = result.Where(d => d.isInAddressables && d.aaLabels.Count == 0).ToList();
else if (labelFilter != "全部")
result = result.Where(d => d.isInAddressables && d.aaLabels.Contains(labelFilter)).ToList();
if (!string.IsNullOrEmpty(pathKeyword))
result = result.Where(d => d.assetPath.IndexOf(pathKeyword, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
return result;
}
#endregion
#region 分析逻辑
private void AnalyzeAll()
{
dependencies.Clear();
dependencyMap.Clear();
allTypes.Clear();
RefreshAACache();
var targetPaths = new HashSet<string>();
if (useBatchMode)
{
foreach (var obj in batchTargets)
{
if (obj == null) continue;
string p = AssetDatabase.GetAssetPath(obj);
if (!string.IsNullOrEmpty(p)) targetPaths.Add(p);
}
}
else
{
if (targetAsset != null)
{
string p = AssetDatabase.GetAssetPath(targetAsset);
if (!string.IsNullOrEmpty(p)) targetPaths.Add(p);
}
}
if (targetPaths.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先选择目标资源。", "确定");
return;
}
try
{
EditorUtility.DisplayProgressBar("分析依赖", "正在收集依赖资源...", 0f);
int idx = 0;
foreach (string path in targetPaths)
{
EditorUtility.DisplayProgressBar("分析依赖", $"处理 {path}", (float)idx / targetPaths.Count);
AnalyzeSingle(path);
idx++;
}
// 收集类型列表
allTypes = dependencies.Select(d => d.typeName).Distinct().OrderBy(t => t).ToList();
EditorUtility.ClearProgressBar();
2026-07-29 22:59:26 +08:00
Log($"[依赖分析器] 分析完成,共 {dependencies.Count} 个依赖资源");
2026-07-28 22:56:35 +08:00
}
catch (Exception e)
{
EditorUtility.ClearProgressBar();
2026-07-29 22:59:26 +08:00
LogError($"[依赖分析器] 分析失败: {e}");
2026-07-28 22:56:35 +08:00
}
}
private void AnalyzeSingle(string assetPath)
{
if (string.IsNullOrEmpty(assetPath)) return;
// 自身也作为一项
AddDependencyInfo(assetPath);
if (!deepAnalyze) return;
string[] deps = AssetDatabase.GetDependencies(assetPath, true);
foreach (var dep in deps)
{
AddDependencyInfo(dep);
}
}
private void AddDependencyInfo(string assetPath)
{
if (dependencyMap.ContainsKey(assetPath)) return;
if (!includeScripts && assetPath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
return;
// 过滤 Editor 仅资源
if (assetPath.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase))
return;
var info = new DependencyInfo
{
assetPath = assetPath,
guid = AssetDatabase.AssetPathToGUID(assetPath)
};
var mainAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
if (mainAsset != null)
{
info.type = mainAsset.GetType();
info.typeName = info.type.Name;
}
else
{
info.typeName = Path.GetExtension(assetPath).TrimStart('.');
}
// 文件大小
try
{
string fullPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, assetPath);
if (File.Exists(fullPath))
{
var fi = new FileInfo(fullPath);
info.fileSizeBytes = fi.Length;
info.fileSizeReadable = FormatSize(info.fileSizeBytes);
}
else
{
info.fileSizeBytes = 0;
info.fileSizeReadable = "-";
}
}
catch
{
info.fileSizeBytes = 0;
info.fileSizeReadable = "-";
}
// AA 信息
if (aaSettings != null && !string.IsNullOrEmpty(info.guid))
{
var entry = aaSettings.FindAssetEntry(info.guid);
if (entry != null)
{
info.isInAddressables = true;
info.aaAddress = entry.address;
info.aaGroup = entry.parentGroup != null ? entry.parentGroup.Name : "(未知)";
info.aaLabels = entry.labels.ToList();
}
}
dependencies.Add(info);
dependencyMap[assetPath] = info;
}
#endregion
#region 反向查找
private void ReverseLookupSelected()
{
if (aaSettings == null)
{
EditorUtility.DisplayDialog("提示", "Addressables 未初始化。", "确定");
return;
}
var selected = Selection.objects;
if (selected == null || selected.Length == 0)
{
EditorUtility.DisplayDialog("提示", "请先在 Project 视图中选择目标资源。", "确定");
return;
}
reverseRefMap.Clear();
var targetGuids = new HashSet<string>();
foreach (var obj in selected)
{
string p = AssetDatabase.GetAssetPath(obj);
if (!string.IsNullOrEmpty(p))
{
string g = AssetDatabase.AssetPathToGUID(p);
if (!string.IsNullOrEmpty(g)) targetGuids.Add(g);
}
}
try
{
EditorUtility.DisplayProgressBar("反向查找", "正在遍历 AA 条目...", 0f);
var allEntries = aaSettings.groups.SelectMany(g => g.entries).ToList();
int idx = 0;
int matched = 0;
foreach (var entry in allEntries)
{
idx++;
if (idx % 50 == 0)
EditorUtility.DisplayProgressBar("反向查找", $"扫描 {idx}/{allEntries.Count}", (float)idx / allEntries.Count);
string entryPath = AssetDatabase.GUIDToAssetPath(entry.guid);
if (string.IsNullOrEmpty(entryPath)) continue;
string[] deps = AssetDatabase.GetDependencies(entryPath, true);
foreach (var dep in deps)
{
string depGuid = AssetDatabase.AssetPathToGUID(dep);
if (targetGuids.Contains(depGuid))
{
if (!reverseRefMap.ContainsKey(dep))
reverseRefMap[dep] = new List<string>();
if (!reverseRefMap[dep].Contains(entryPath))
reverseRefMap[dep].Add(entryPath);
matched++;
}
}
}
EditorUtility.ClearProgressBar();
var sb = new StringBuilder();
sb.AppendLine($"反向查找完成,命中 {matched} 处引用。");
sb.AppendLine();
foreach (var kv in reverseRefMap)
{
sb.AppendLine($"【目标】{kv.Key}");
foreach (var refPath in kv.Value)
{
string refGuid = AssetDatabase.AssetPathToGUID(refPath);
var refEntry = aaSettings.FindAssetEntry(refGuid);
string aaInfo = refEntry != null
? $"[AA] {refEntry.parentGroup.Name} / {refEntry.address}"
: "[非AA]";
sb.AppendLine($" ← {refPath} {aaInfo}");
}
sb.AppendLine();
}
string savePath = EditorUtility.SaveFilePanel("保存反向查找报告", Application.dataPath, "ReverseLookupReport", "txt");
if (!string.IsNullOrEmpty(savePath))
{
File.WriteAllText(savePath, sb.ToString(), Encoding.UTF8);
EditorUtility.DisplayDialog("完成", $"报告已保存到:\n{savePath}", "确定");
}
else
{
2026-07-29 22:59:26 +08:00
Log(sb.ToString());
2026-07-28 22:56:35 +08:00
}
}
catch (Exception e)
{
EditorUtility.ClearProgressBar();
2026-07-29 22:59:26 +08:00
LogError($"[依赖分析器] 反向查找失败: {e}");
2026-07-28 22:56:35 +08:00
}
}
#endregion
#region 导出报告
private void ExportReport()
{
if (dependencies.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先分析依赖。", "确定");
return;
}
string savePath = EditorUtility.SaveFilePanel("导出依赖报告", Application.dataPath, "DependencyReport", "csv");
if (string.IsNullOrEmpty(savePath)) return;
var sb = new StringBuilder();
sb.AppendLine("AssetPath,GUID,Type,Size,IsInAA,AAGroup,AAAddress,AALabels");
foreach (var info in dependencies)
{
sb.AppendLine(string.Join(",",
EscapeCsv(info.assetPath),
info.guid,
info.typeName,
info.fileSizeBytes,
info.isInAddressables,
EscapeCsv(info.aaGroup ?? ""),
EscapeCsv(info.aaAddress ?? ""),
EscapeCsv(string.Join(";", info.aaLabels))));
}
File.WriteAllText(savePath, sb.ToString(), Encoding.UTF8);
EditorUtility.DisplayDialog("完成", $"报告已导出到:\n{savePath}", "确定");
2026-07-29 22:59:26 +08:00
Log($"[依赖分析器] 报告已导出: {savePath}");
2026-07-28 22:56:35 +08:00
}
private string EscapeCsv(string s)
{
if (string.IsNullOrEmpty(s)) return "";
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;
}
#endregion
#region 复制地址
private void CopySelectedAddresses()
{
var filtered = GetFilteredCache();
var selected = filtered.Where(d =>
Selection.objects != null &&
Selection.objects.Any(o => o != null && AssetDatabase.GetAssetPath(o) == d.assetPath))
.ToList();
if (selected.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先在下方列表勾选资源。", "确定");
return;
}
var sb = new StringBuilder();
foreach (var info in selected)
{
sb.AppendLine(info.aaAddress ?? info.assetPath);
}
GUIUtility.systemCopyBuffer = sb.ToString();
2026-07-29 22:59:26 +08:00
Log($"[依赖分析器] 已复制 {selected.Count} 条地址到剪贴板");
2026-07-28 22:56:35 +08:00
}
#endregion
#region 加入AA
private void AddSelectedToAddressables()
{
if (aaSettings == null)
{
EditorUtility.DisplayDialog("错误", "Addressables 未初始化。", "确定");
return;
}
var filtered = GetFilteredCache();
var selected = filtered.Where(d =>
Selection.objects != null &&
Selection.objects.Any(o => o != null && AssetDatabase.GetAssetPath(o) == d.assetPath))
.ToList();
if (selected.Count == 0)
{
EditorUtility.DisplayDialog("提示", "请先在下方列表勾选资源。", "确定");
return;
}
// 选择 Group
var groupNames = aaSettings.groups.Where(g => g != null).Select(g => g.Name).ToArray();
int choice = EditorUtility.DisplayDialogComplex(
"选择目标 Group",
$"将 {selected.Count} 个资源添加到哪个 Group?",
groupNames.Length > 0 ? groupNames[0] : "Default",
groupNames.Length > 1 ? groupNames[1] : "Cancel",
groupNames.Length > 2 ? "其他..." : "Cancel");
if (choice == 2)
{
// 弹出菜单选择
var menu = new GenericMenu();
foreach (var gn in groupNames)
{
var captured = gn;
menu.AddItem(new GUIContent(gn), false, () => DoAddToGroup(selected, captured));
}
menu.ShowAsContext();
return;
}
else if (choice >= 0 && choice < groupNames.Length)
{
DoAddToGroup(selected, groupNames[choice]);
}
}
private void DoAddToGroup(List<DependencyInfo> list, string groupName)
{
var group = aaSettings.groups.FirstOrDefault(g => g != null && g.Name == groupName);
if (group == null)
{
EditorUtility.DisplayDialog("错误", $"找不到 Group: {groupName}", "确定");
return;
}
int count = 0;
var errors = new List<string>();
foreach (var info in list)
{
if (string.IsNullOrEmpty(info.guid))
{
errors.Add($"{info.assetPath}: GUID 为空");
continue;
}
var entry = aaSettings.FindAssetEntry(info.guid);
if (entry == null)
entry = aaSettings.CreateOrMoveEntry(info.guid, group);
if (entry == null)
{
errors.Add($"{info.assetPath}: 创建条目失败");
continue;
}
// 默认 address 用文件名
if (string.IsNullOrEmpty(entry.address) || entry.address == info.guid)
entry.address = Path.GetFileNameWithoutExtension(info.assetPath);
count++;
}
aaSettings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, null, true);
AssetDatabase.SaveAssets();
// 重新分析刷新
AnalyzeAll();
string msg = $"成功: {count} 个";
if (errors.Count > 0) msg += $"\n失败 {errors.Count} 个:\n" + string.Join("\n", errors);
EditorUtility.DisplayDialog("加入AA完成", msg, "确定");
}
#endregion
#region 工具
private static string FormatSize(long bytes)
{
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024.0).ToString("F1") + " KB";
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024.0 * 1024)).ToString("F2") + " MB";
return (bytes / (1024.0 * 1024 * 1024)).ToString("F2") + " GB";
}
#endregion
}