unity 静态字段内存查找工具

unity 静态字段内存查找工具

StaticMemoryWindow

csharp 复制代码
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;
    }
}

MemoryEstimator

csharp 复制代码
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); }
    }
}
相关推荐
警醒与鞭策18 小时前
【无标题】
android·unity·性能优化·游戏引擎·perforce
zhchyun200820 小时前
【Unity UI 进阶】仿 Element UI 打造企业级 Unity UI 组件库(10)
ui·unity·游戏引擎
地狱为王1 天前
Unity使用NVIDIA的Audio2Face驱动口型
unity·游戏引擎
ebiobiz1 天前
STM32F4 Unity 单元测试与多次测试失败率统计详细步骤
stm32·unity·单元测试
一线灵1 天前
跨平台游戏引擎 Axmol-2.11.5 发布
游戏引擎·lua
淡海水1 天前
16-01-C#数据结构源码-附录A-术语表
unity·c#·游戏引擎·il2cpp
淡海水1 天前
16-03-C#数据结构源码-附录C-参考资源
unity·c#·游戏引擎·il2cpp
天神哥哥啊1 天前
unity联调注意事项-安卓
android·unity·游戏引擎
天神哥哥啊1 天前
unity联调注意事项-IOS
unity·ios·游戏引擎
可爱系程序猿2 天前
Unity 材质预览才弹 d3dcompiler_47.dll?先分清编辑器 SDK、着色器缓存和玩家包
程序人生·3d·unity·电脑·材质