当前位置:首页 > 技术 > 正文内容

Unity项目中未使用资源的自动检测与清理方案

访客 技术 2026年6月2日 1

背景说明

在长期迭代的Unity项目中,Assets目录会积累大量未被引用的资源文件,如纹理、材质、模型等。这些冗余资产不仅占用磁盘空间,还可能影响构建效率和版本控制。手动排查既耗时又容易出错,因此需要借助自动化工具进行识别和清理。

解决方案一:ResourceChecker - 资源引用分析器

该工具用于扫描当前场景中所有被引用的资源,并提供可视化界面展示其使用情况,帮助开发者定位无用资源。

核心功能包括:

  • 扫描场景中所有渲染器、UI元素、动画系统及脚本中的资源引用
  • 支持检查禁用对象和精灵动画序列中的资源依赖
  • 显示每项资源的内存占用、尺寸、Mipmap层级等信息
  • 可一键选中关联的GameObject或材质球进行快速定位

实现机制基于反射与依赖收集:

public class ResourceAnalyzer : EditorWindow
{
    private Vector2 scrollPosition;
    private List<AssetEntry> textures = new List<AssetEntry>();
    private List<MaterialInfo> materials = new List<MaterialInfo>();

    [MenuItem("Tools/资源分析器")]
    public static void ShowWindow()
    {
        GetWindow<ResourceAnalyzer>().titleContent = new GUIContent("资源分析");
    }

    private void OnGUI()
    {
        EditorGUILayout.LabelField($"已加载纹理: {textures.Count}, 总显存: {GetFormattedMemory()}");
        scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);

        foreach (var entry in textures)
        {
            DrawTextureItem(entry);
        }

        EditorGUILayout.EndScrollView();
        if (GUILayout.Button("刷新数据")) ScanSceneResources();
    }

    private void ScanSceneResources()
    {
        textures.Clear();
        var renderers = Object.FindObjectsOfType<Renderer>(true);
        
        foreach (var renderer in renderers)
        {
            ProcessRendererMaterials(renderer);
        }
    }

    private void ProcessRendererMaterials(Renderer renderer)
    {
        foreach (var mat in renderer.sharedMaterials)
        {
            if (mat == null) continue;

            var mainTex = mat.GetTexture("_MainTex");
            if (mainTex != null && !textures.Exists(t => t.texture == mainTex))
            {
                textures.Add(new AssetEntry 
                { 
                    texture = mainTex, 
                    sizeKB = CalculateTextureSize(mainTex),
                    referencedBy = new List<Object> { renderer.gameObject } 
                });
            }
        }
    }

    private int CalculateTextureSize(Texture tex)
    {
        int bytes = 0;
        if (tex is Texture2D t2d)
        {
            var format = GetBitsPerPixel(t2d.format);
            bytes = t2d.width * t2d.height * format / 8;
        }
        return bytes / 1024;
    }

    private int GetBitsPerPixel(TextureFormat format)
    {
        return format switch
        {
            TextureFormat.RGBA32 or TextureFormat.ARGB32 => 32,
            TextureFormat.RGB24 => 24,
            TextureFormat.RGBA4444 => 16,
            TextureFormat.DXT1 => 4,
            TextureFormat.DXT5 => 8,
            _ => 32
        };
    }

    private void DrawTextureItem(AssetEntry entry)
    {
        GUILayout.BeginHorizontal("box");
        if (entry.texture)
        {
            GUILayout.Box(AssetPreview.GetMiniThumbnail(entry.texture), GUILayout.Width(40), GUILayout.Height(40));
            GUILayout.Label($"{entry.texture.name} ({entry.sizeKB}KB)", GUILayout.Width(150));
            
            if (GUILayout.Button($"被{entry.referencedBy.Count}个对象使用", GUILayout.Width(120)))
            {
                Selection.objects = entry.referencedBy.ToArray();
            }
        }
        GUILayout.EndHorizontal();
    }
}

class AssetEntry
{
    public Texture texture;
    public int sizeKB;
    public List<Object> referencedBy = new List<Object>();
}

class MaterialInfo
{
    public Material material;
    public List<Renderer> usedInRenderers = new List<Renderer>();
}

使用方式:菜单栏选择 Tools → 资源分析器 打开面板并点击"刷新数据"即可开始扫描。

解决方案二:SafeAssetCleaner - 安全资产清理工具

此插件专注于安全地移除未使用的资源,并具备自动备份机制,防止误删关键文件。

主要特性:

  • 三种清理模式适应不同需求
  • 删除前自动生成UnityPackage备份包
  • 智能排除Resources、Plugins、Gizmos等特殊目录
  • 支持保留编辑器扩展脚本(Editor文件夹下)

清理逻辑流程如下:

  1. 遍历Assets目录下所有非.meta文件
  2. 通过AssetDatabase.GetDependencies获取场景与Resources中的引用关系
  3. 过滤掉有引用的资源,剩余即为候选删除项
  4. 执行删除操作前导出为时间戳命名的package文件
  5. 调用AssetDatabase.DeleteAsset逐个移除文件
  6. 自动清理空文件夹

核心清理类简化实现:

public class SafeAssetCleaner : EditorWindow
{
    private List<string> candidates = new List<string>();
    private Vector2 scrollPos;

    [MenuItem("Assets/清理未使用资源/仅游戏运行时")]
    public static void CleanForBuild()
    {
        var window = CreateInstance<SafeAssetCleaner>();
        window.ScanUnusedAssets(includeEditorCode: false);
        window.Show();
    }

    private void ScanUnusedAssets(bool includeEditorCode = true)
    {
        var allGuids = AssetDatabase.FindAssets("", new[] { "Assets" })
            .Where(g => IsTargetAsset(AssetDatabase.GUIDToAssetPath(g)));

        var referenced = new HashSet<string>();

        // 收集构建相关引用
        var scenes = EditorBuildSettings.scenes.Where(s => s.enabled).Select(s => s.path).ToArray();
        foreach (var path in AssetDatabase.GetDependencies(scenes, true))
        {
            referenced.Add(AssetDatabase.AssetPathToGUID(path));
        }

        // Resources引用
        var resourcePaths = GetResourceFiles();
        foreach (var dep in AssetDatabase.GetDependencies(resourcePaths))
        {
            referenced.Add(AssetDatabase.AssetPathToGUID(dep));
        }

        candidates = allGuids.Where(guid => !referenced.Contains(guid)).ToList();
    }

    private bool IsTargetAsset(string path)
    {
        string ext = Path.GetExtension(path).ToLower();
        if (new[] { ".meta", ".dll", ".js" }.Contains(ext)) return false;
        if (Regex.IsMatch(path, @"[\\/](Gizmos|Plugins|Resources)[\\/]")) return false;
        return true;
    }

    private string[] GetResourceFiles()
    {
        return Directory.GetFiles("Assets", "*.*", SearchOption.AllDirectories)
            .Where(f => f.Contains("/Resources/") || f.Contains("\\Resources\\")) 
            .Where(f => Path.GetExtension(f) != ".meta").ToArray();
    }

    private void OnGUI()
    {
        EditorGUILayout.HelpBox($"发现 {candidates.Count} 个未使用资源", MessageType.Info);

        if (GUILayout.Button("执行清理并备份"))
        {
            PerformDeletionWithBackup();
        }

        scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
        foreach (var guid in candidates.Take(100)) // 限制显示数量
        {
            var path = AssetDatabase.GUIDToAssetPath(guid);
            GUILayout.Label(path.Length > 80 ? path.Substring(0, 77) + "..." : path);
        }
        EditorGUILayout.EndScrollView();
    }

    private void PerformDeletionWithBackup()
    {
        string backupDir = "BackupDeletedAssets";
        Directory.CreateDirectory(backupDir);

        string pkgName = $"{backupDir}/deleted_{DateTime.Now:yyyyMMdd_HHmmss}.unitypackage";
        string[] paths = candidates.Select(AssetDatabase.GUIDToAssetPath).ToArray();

        AssetDatabase.ExportPackage(paths, pkgName, ExportPackageOptions.Interactive);

        foreach (string path in paths)
        {
            AssetDatabase.DeleteAsset(path);
        }

        CleanEmptyFolders("Assets");
        AssetDatabase.Refresh();
        EditorUtility.RevealInFinder(backupDir);
    }

    private void CleanEmptyFolders(string root)
    {
        foreach (string dir in Directory.GetDirectories(root))
        {
            CleanEmptyFolders(dir);
            if (!Directory.EnumerateFiles(dir).Any(f => !f.EndsWith(".meta")) &&
                !Directory.GetDirectories(dir).Any())
            {
                FileUtil.DeleteFileOrDirectory(dir);
                FileUtil.DeleteFileOrDirectory(dir + ".meta");
            }
        }
    }
}

使用方法:右键菜单选择 Assets → 清理未使用资源,根据项目阶段选择合适模式后点击确认。

最佳实践建议

  • 定期执行资源审查,尤其是在版本发布前
  • 优先使用SafeAssetCleaner的备份功能以规避风险
  • 结合Git等版本控制系统,确保即使误删也可恢复
  • 对Resources目录保持谨慎,因其内容默认包含在构建中

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。