960 lines
32 KiB
C#
960 lines
32 KiB
C#
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
|
||
{
|
||
// ============================ 日志开关 ============================
|
||
// 启用: 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); }
|
||
|
||
#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();
|
||
Log($"[依赖分析器] 已选中 {objs.Count} 个非AA资源到 Project 视图");
|
||
}
|
||
|
||
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();
|
||
Log($"[依赖分析器] 分析完成,共 {dependencies.Count} 个依赖资源");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
EditorUtility.ClearProgressBar();
|
||
LogError($"[依赖分析器] 分析失败: {e}");
|
||
}
|
||
}
|
||
|
||
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
|
||
{
|
||
Log(sb.ToString());
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
EditorUtility.ClearProgressBar();
|
||
LogError($"[依赖分析器] 反向查找失败: {e}");
|
||
}
|
||
}
|
||
|
||
#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}", "确定");
|
||
Log($"[依赖分析器] 报告已导出: {savePath}");
|
||
}
|
||
|
||
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();
|
||
Log($"[依赖分析器] 已复制 {selected.Count} 条地址到剪贴板");
|
||
}
|
||
|
||
#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
|
||
}
|