using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
/// <summary>
/// 静态字段内存分析窗口。
/// 菜单:Tools / Memory / Static Fields
///
/// 扫描所有已加载程序集的静态字段,估算内存,定位所在文件,按大小降序排列。
/// 用途:快速找出"内存大户"(大数组 / 大集合 / 大字符串)以及它们定义在哪个文件。
/// </summary>
public class StaticMemoryWindow : EditorWindow
{
[MenuItem("Tools/Memory/Static Fields")]
static void Open() { GetWindow<StaticMemoryWindow>("静态字段内存"); }
class Entry
{
public long bytes;
public string fieldName;
public string fieldType;
public string declaringType; // 所属类全名
public string file; // 项目内 .cs 路径,或程序集名
public string summary; // 值摘要
public bool exact; // 是否为精确值(估算值显示「粗略」)
}
List<Entry> entries = new List<Entry>();
Vector2 scroll;
long totalBytes;
bool scanning;
bool cancelRequested;
float scanProgress;
string scanStatus = "";
System.Collections.IEnumerator scanRoutine;
static Dictionary<string, string> cachedTypeToFile;
HashSet<string> foldedTypes = new HashSet<string>();
string searchFilter = "";
string[] assemblyNameArray = new string[0];
int selectedAssemblyIndex = -1;
void OnEnable()
{
EditorApplication.update += Update;
RefreshAssemblyList();
BeginScan();
}
void OnDisable() { EditorApplication.update -= Update; StopScan(); scanRoutine = null; scanning = false; }
// 获取当前已加载的所有程序集名称,供下拉框选择
void RefreshAssemblyList()
{
var names = new List<string>();
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
{
string n = a.GetName().Name;
if (!names.Contains(n)) names.Add(n);
}
names.Sort();
assemblyNameArray = names.ToArray();
// 默认选中 Assembly-CSharp(若存在),否则选第一个
selectedAssemblyIndex = -1;
for (int i = 0; i < assemblyNameArray.Length; i++)
{
if (assemblyNameArray[i] == "Assembly-CSharp") { selectedAssemblyIndex = i; break; }
}
if (selectedAssemblyIndex < 0 && assemblyNameArray.Length > 0) selectedAssemblyIndex = 0;
}
// 分帧驱动:每帧最多执行 5ms 扫描工作,避免 UI 卡死
void Update()
{
if (scanRoutine == null) return;
if (cancelRequested) { StopScan(); return; }
var sw = System.Diagnostics.Stopwatch.StartNew();
while (sw.ElapsedMilliseconds < 5)
{
if (!scanRoutine.MoveNext()) { FinishScan(); return; }
}
Repaint();
}
void BeginScan()
{
if (scanRoutine != null) return; // 已在扫描
scanning = true;
cancelRequested = false;
scanProgress = 0f;
scanStatus = "准备扫描...";
scanRoutine = ScanRoutine();
}
void StopScan()
{
scanning = false;
scanRoutine = null;
cancelRequested = false;
scanStatus = "已取消";
Repaint();
}
void FinishScan()
{
scanning = false;
scanRoutine = null;
cancelRequested = false;
scanProgress = 1f;
Repaint();
}
// 分帧扫描所有程序集的静态字段
System.Collections.IEnumerator ScanRoutine()
{
scanning = true;
cancelRequested = false;
entries.Clear();
foldedTypes.Clear();
totalBytes = 0;
scanProgress = 0f;
// 构建类名 -> 文件映射(首次分帧构建,之后走缓存)
scanStatus = "构建类型索引...";
if (cachedTypeToFile == null)
{
var build = BuildTypeToFileMapRoutine();
while (build.MoveNext()) yield return null;
}
Dictionary<string, string> typeToFile = cachedTypeToFile;
// 只扫描下拉框选中的程序集
string targetName = (selectedAssemblyIndex >= 0 && selectedAssemblyIndex < assemblyNameArray.Length)
? assemblyNameArray[selectedAssemblyIndex]
: "Assembly-CSharp";
var allAssemblies = AppDomain.CurrentDomain.GetAssemblies();
var assemblies = new List<Assembly>();
foreach (var a in allAssemblies)
{
if (a.GetName().Name == targetName)
assemblies.Add(a);
}
int asmCount = assemblies.Count;
scanStatus = "扫描静态字段...";
for (int ai = 0; ai < asmCount; ai++)
{
if (cancelRequested) yield break;
Assembly asm = assemblies[ai];
Type[] types;
try { types = asm.GetTypes(); }
catch (ReflectionTypeLoadException e) { types = e.Types.Where(t => t != null).ToArray(); }
catch { continue; }
int processed = 0;
foreach (Type type in types)
{
if (cancelRequested) yield break;
if (type.IsGenericTypeDefinition) continue;
FieldInfo[] fields;
try { fields = type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); }
catch { continue; }
foreach (FieldInfo f in fields)
{
if (f.IsLiteral) continue; // const 编译期内联,不占静态内存
object value;
try { value = f.GetValue(null); }
catch { continue; }
bool isExact;
long size = MemoryEstimator.Estimate(value, out isExact);
if (size <= 0) continue;
totalBytes += size;
string loc;
if (!typeToFile.TryGetValue(type.Name, out loc))
loc = type.Assembly.GetName().Name;
entries.Add(new Entry
{
bytes = size,
fieldName = f.Name,
fieldType = f.FieldType.Name,
declaringType = type.FullName,
file = loc,
summary = Summarize(value),
exact = isExact
});
}
// 每处理完一个类型让出一次;每 32 个类型做一次增量排序,让列表实时有序显示
if ((++processed & 31) == 0)
entries.Sort((a, b) => b.bytes.CompareTo(a.bytes));
yield return null;
}
entries.Sort((a, b) => b.bytes.CompareTo(a.bytes)); // 程序集扫完确保有序
scanProgress = (ai + 1f) / asmCount;
yield return null;
}
}
void OnGUI()
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button(scanning ? "扫描中..." : "刷新", GUILayout.Width(80)) && !scanning)
BeginScan();
EditorGUILayout.LabelField(string.Format("字段数 {0},总计 {1}", entries.Count, FormatBytes(totalBytes)));
EditorGUILayout.EndHorizontal();
// 程序集下拉框(切换后自动重新扫描)
if (assemblyNameArray.Length > 0)
{
int newIdx = EditorGUILayout.Popup("程序集", selectedAssemblyIndex, assemblyNameArray);
if (newIdx != selectedAssemblyIndex)
{
selectedAssemblyIndex = newIdx;
StopScan();
BeginScan();
}
}
searchFilter = EditorGUILayout.TextField("搜索", searchFilter);
if (scanning)
{
Rect progressRect = EditorGUILayout.GetControlRect();
EditorGUI.ProgressBar(progressRect, scanProgress, scanStatus);
if (GUILayout.Button("取消扫描", GUILayout.Width(80)))
cancelRequested = true;
// 不 return:扫描过程中也继续渲染已扫描到的条目,实现"边扫边显示"
}
// 表头
EditorGUILayout.BeginHorizontal();
DrawHeader("内存", 140);
DrawHeader("字段类型", 120);
DrawHeader("字段名", 320);
DrawHeader("位置", 240);
DrawHeader("值摘要", 200);
EditorGUILayout.EndHorizontal();
scroll = EditorGUILayout.BeginScrollView(scroll);
// 按所属类分组(保持 entries 已有的内存降序顺序)
var groups = new Dictionary<string, List<Entry>>();
var order = new List<string>();
foreach (Entry e in entries)
{
if (!MatchesSearch(e)) continue;
List<Entry> list;
if (!groups.TryGetValue(e.declaringType, out list))
{
list = new List<Entry>();
groups[e.declaringType] = list;
order.Add(e.declaringType);
}
list.Add(e);
}
int shown = 0;
foreach (string typeName in order)
{
if (shown >= 1000) break;
List<Entry> list = groups[typeName];
bool folded = foldedTypes.Contains(typeName);
// 类折叠标题
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button(folded ? "▶" : "▼", GUILayout.Width(20)))
{
if (folded) foldedTypes.Remove(typeName);
else foldedTypes.Add(typeName);
}
GUILayout.Label(typeName, EditorStyles.boldLabel);
GUILayout.Label(string.Format("{0} 个字段", list.Count), GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
if (folded) continue;
foreach (Entry e in list)
{
if (shown++ >= 1000) break;
// 高亮内存大户
if (e.bytes >= 1024 * 1024) GUI.color = new Color(1f, 0.5f, 0.5f); // >=1MB 红
else if (e.bytes >= 1024) GUI.color = new Color(1f, 0.9f, 0.5f); // >=1KB 黄
EditorGUILayout.BeginHorizontal();
GUILayout.Label(FormatBytes(e.bytes) + (e.exact ? "" : " (粗略)"), GUILayout.Width(140));
GUILayout.Label(e.fieldType, GUILayout.Width(120));
GUILayout.Label(" " + e.fieldName, GUILayout.Width(320));
// 文件路径做成"看起来像标签"的按钮,点击在 Project 窗口定位
if (e.file.StartsWith("Assets/"))
{
if (GUILayout.Button(e.file, EditorStyles.label, GUILayout.Width(240)))
PingFile(e.file);
}
else
{
GUILayout.Label(e.file, GUILayout.Width(240));
}
GUILayout.Label(e.summary, GUILayout.Width(200));
EditorGUILayout.EndHorizontal();
GUI.color = Color.white;
}
}
EditorGUILayout.EndScrollView();
}
bool MatchesSearch(Entry e)
{
if (string.IsNullOrEmpty(searchFilter)) return true;
string f = searchFilter;
if (e.fieldName != null && e.fieldName.IndexOf(f, StringComparison.OrdinalIgnoreCase) >= 0) return true;
if (e.declaringType != null && e.declaringType.IndexOf(f, StringComparison.OrdinalIgnoreCase) >= 0) return true;
if (e.fieldType != null && e.fieldType.IndexOf(f, StringComparison.OrdinalIgnoreCase) >= 0) return true;
if (e.file != null && e.file.IndexOf(f, StringComparison.OrdinalIgnoreCase) >= 0) return true;
if (e.summary != null && e.summary.IndexOf(f, StringComparison.OrdinalIgnoreCase) >= 0) return true;
return false;
}
static void DrawHeader(string text, float width)
{
GUILayout.Label(text, EditorStyles.boldLabel, GUILayout.Width(width));
}
static void PingFile(string assetPath)
{
UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
if (obj != null) EditorGUIUtility.PingObject(obj);
}
static string FormatBytes(long b)
{
if (b >= 1024 * 1024 * 1024) return string.Format("{0:F2} GB", b / (1024.0 * 1024 * 1024));
if (b >= 1024 * 1024) return string.Format("{0:F2} MB", b / (1024.0 * 1024));
if (b >= 1024) return string.Format("{0:F1} KB", b / 1024.0);
return b + " B";
}
static string Summarize(object value)
{
if (value == null) return "null";
string s = value as string;
if (s != null) return s.Length > 40 ? "\"" + s.Substring(0, 40) + "...\"" : "\"" + s + "\"";
Array arr = value as Array;
if (arr != null) return "Length=" + arr.Length;
System.Collections.ICollection col = value as System.Collections.ICollection;
if (col != null) return "Count=" + col.Count;
string str = value.ToString();
return str.Length > 40 ? str.Substring(0, 40) + "..." : str;
}
// 分帧构建「类名 -> 文件路径」映射(用 Unity 资源库索引,只遍历项目脚本,避开 Library/Temp 等海量缓存目录)
static System.Collections.IEnumerator BuildTypeToFileMapRoutine()
{
if (cachedTypeToFile != null) yield break;
var map = new Dictionary<string, string>();
string[] guids = AssetDatabase.FindAssets("t:Script");
for (int i = 0; i < guids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
if (string.IsNullOrEmpty(path) || !path.EndsWith(".cs")) continue;
string content = File.ReadAllText(Path.GetFullPath(path));
foreach (Match m in Regex.Matches(content, @"\bclass\s+(\w+)"))
{
string name = m.Groups[1].Value;
if (!map.ContainsKey(name)) map[name] = path;
}
if ((i & 7) == 7) yield return null; // 每 8 个文件让出一次
}
cachedTypeToFile = map;
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
/// <summary>
/// 托管对象内存估算器(近似值,用于定位"内存大户",非精确值)。
///
/// 估算规则:
/// - 基础类型(int/long/char 等)精确;
/// - string 按 UTF-16(2 字节/char)+ 对象头;
/// - 数组按元素类型(值类型用 Marshal.SizeOf,引用类型递归);
/// - 集合(ICollection)采样前 64 个元素算平均,再乘总数;
/// - 其它引用对象给默认对象头(64B)。
/// 防循环:用引用相等比较器记录已访问对象,限制递归深度。
/// </summary>
public static class MemoryEstimator
{
public static long Estimate(object value)
{
bool exact;
return Estimate(value, out exact);
}
public static long Estimate(object value, out bool exact)
{
exact = IsExact(value);
var visited = new HashSet<object>(ReferenceComparer.Instance);
return EstimateInternal(value, visited, 0);
}
// 判断该值的内存是否为精确值:
// 值类型(基础类型/enum/struct/decimal/IntPtr)与值类型数组为精确;
// string、引用类型数组、集合、IEnumerable 及其它引用对象均为估算值。
static bool IsExact(object value)
{
if (value == null) return true;
Type t = value.GetType();
if (t.IsValueType) return true;
if (t.IsArray) return t.GetElementType().IsValueType;
return false;
}
static long EstimateInternal(object value, HashSet<object> visited, int depth)
{
if (value == null) return 0;
if (depth > 4) return 0; // 防深递归
Type t = value.GetType();
// 基础类型:精确
if (t.IsPrimitive)
{
if (t == typeof(bool)) return 1;
if (t == typeof(byte) || t == typeof(sbyte)) return 1;
if (t == typeof(char)) return 2;
if (t == typeof(short) || t == typeof(ushort)) return 2;
if (t == typeof(int) || t == typeof(uint)) return 4;
if (t == typeof(float)) return 4;
if (t == typeof(long) || t == typeof(ulong)) return 8;
if (t == typeof(double)) return 8;
return 8;
}
if (t == typeof(IntPtr)) return IntPtr.Size;
if (t == typeof(decimal)) return 16;
if (t.IsEnum) return Marshal.SizeOf(Enum.GetUnderlyingType(t));
// 引用类型:防循环
if (!t.IsValueType)
{
if (visited.Contains(value)) return 0;
visited.Add(value);
}
else
{
// 值类型 struct:尝试 marshal(含引用字段的会失败)
try { return Marshal.SizeOf(t); }
catch { return 0; }
}
// string
string s = value as string;
if (s != null)
return 2L * s.Length + 24;
// 数组
if (t.IsArray)
{
Array arr = (Array)value;
long total = 24;
Type elem = t.GetElementType();
if (elem.IsValueType)
{
try { total += arr.LongLength * Marshal.SizeOf(elem); }
catch { total += arr.LongLength * 16; }
}
else
{
// 引用类型数组:采样前 64 个估算,避免超大数组全量遍历卡死
long sampleSum = 0;
int sampled = 0;
foreach (object item in arr)
{
if (sampled >= 64) break;
sampleSum += EstimateInternal(item, visited, depth + 1);
sampled++;
}
long avg = sampled > 0 ? sampleSum / sampled : 0;
total += avg * arr.Length;
}
return total;
}
// 集合(非泛型 ICollection):采样 + 比例估算(大集合也只需遍历前 64 个)
ICollection col = value as ICollection;
if (col != null)
{
int count = col.Count;
if (count == 0) return 64;
long sampleSum = 0;
int sampled = 0;
foreach (object item in col)
{
if (sampled >= 64) break;
sampleSum += EstimateInternal(item, visited, depth + 1);
sampled++;
}
long avg = sampleSum / Math.Max(1, sampled);
return 64 + avg * count;
}
// 纯 IEnumerable(迭代器 / LINQ):限制遍历
IEnumerable en = value as IEnumerable;
if (en != null)
{
long total = 64;
int n = 0;
foreach (object item in en)
{
if (n++ >= 100) break;
total += EstimateInternal(item, visited, depth + 1);
}
return total;
}
// 其它引用对象:默认对象头
return 64;
}
// 引用相等比较器(确保 string 等按引用而非值去重)
sealed class ReferenceComparer : IEqualityComparer<object>
{
public static readonly ReferenceComparer Instance = new ReferenceComparer();
public new bool Equals(object x, object y) { return ReferenceEquals(x, y); }
public int GetHashCode(object obj) { return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); }
}
}