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); }
    }
}
相关推荐
郝学胜-神的一滴7 小时前
[简化版 GAMES 104] 现代游戏引擎 06:从Tick时序到邮局模型,拆解确定性世界的底层密码
开发语言·c++·游戏引擎·图形渲染·软件开发·opengl
鹿野素材屋1 天前
Unity超轻量级中文语音播报,仅5兆大小,无需联网即可使用,适用于弹幕、提示等动态语音播出
unity·游戏引擎
电子云与长程纠缠2 天前
UE5 Lyra PocketWorld进行3D内容UI预览 - 上
开发语言·学习·3d·ue5·游戏引擎
玖玥拾2 天前
Unity3D RPG 入门项目(八)游戏设置面板、帧率控制、快捷技能药品栏、技能解锁系统
游戏·3d·unity·游戏引擎
ellis19702 天前
u3d插件xLua[十] lua侧判空问题
unity
牛哇网络工作室3 天前
UnityHDRP写实数字人全流程基础5—语音输入和语音识别
android·unity·c#·游戏引擎·aigc·语音识别·xcode
ellis19703 天前
u3d插件xLua[九]例8 Hotfix
unity
淡海水3 天前
01-07-运行时-GC深度剖析-内存分配回收与结构选择
jvm·windows·unity·gc
优梦创客3 天前
游戏开发架构选型:第1篇|什么是架构?从进球事件看 MVC 的局限
unity·架构·游戏引擎·游戏开发