需求很简单,提供一个工具,找出所有未被引用的资源。提供快捷删除功能。
实现思路:
- 找出目标路径下的所有资源
- 找出资源的依赖,并添加到依赖列表中
- 读取lua文件内容,并保存到列表中(由于项目是用lua热更新的,配置表最终也是生成lua文件,所以lua文件中会配置了引用资源的名称)
- 判断资源是否被引用(依赖列表中的引用,lua脚本内容列表中的引用)
- 绘制编辑器窗口内容
- 单个删除
- 批量删除同类型资源
- 筛选出某个文件夹中为引用的文件
备注:(首次扫描资源时比较耗时,我这边五千多个资源首次大概耗时:29.7秒,之后都是14.9秒,查了下大致是unity做了缓存);当然不同电脑时间肯定不一致,仅供参考
这里说明一下获取资源依赖API:
第二个参数是是否递归查找引用:我大概试了一下就是,资源A引用了另一个资源B,B引用了资源C,D
false:只会查找到引用了 B
true:会查到到 B C D
当然开启递归后就很耗时,对于该功能也没必要,因为最后还是会遍历到B,C,D。所以如果选了true就会多了很多无用的查找。
AssetDatabase.GetDependencies(info.path,false);
效果:
代码实现就是根据上边的思路步骤实现,就不做过多的解释了。
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditor.UI;
using UnityEngine;
using Object = UnityEngine.Object;
public class NoReferenceWindow:EditorWindow
{
private enum AssetType
{
None,
Prefab,
Material,
Scene,
Asset,
Lua,
Image,
FBX,
Controller,
Animation,
}
private class FileInfo
{
public string path;
public AssetType assetType;
public Object asset;
}
private static string[] ASSET_FILE_ROOT = {"Assets/Resources","Assets/Scene","Assets/StreamingAssets"};
// 过滤文件夹
private static string[] FilterDirectory = {"Assets/Resources/UI/GenAtlas"};
private Dictionary<string, FileInfo> allFileDic = null;
// key:luaPath,value:luaContent
private Dictionary<string, string> luaFileContentDic = null;
private List<FileInfo> assetDependsList = null;
// 未被引用资源
private Dictionary<AssetType,List<FileInfo>> noReferenceAssetDic = null;
private Vector2 ve2;
private bool initComplete = false;
// 筛选文件夹
private List<string> siftPathList = new List<string>();
// 筛选文件夹中未使用的文件
private Dictionary<AssetType,List<FileInfo>> noReferenceSiftAssetDic = null;
private bool isAll = true;
private void Awake()
{
siftPathList.Add("Assets/Resources");
}
// 绘制编辑器窗口内容
private void OnGUI()
{
EditorGUILayout.Space();
if (initComplete == false)
{
return;
}
if (noReferenceAssetDic == null)
{
InitAllFileDic();
return;
}
DrawSift();
ve2 = EditorGUILayout.BeginScrollView(ve2);
var tmpNoRefernceDic = noReferenceAssetDic;
if (!isAll)
{
tmpNoRefernceDic = noReferenceSiftAssetDic;
}
foreach (var assetType in tmpNoRefernceDic.Keys)
{
List<FileInfo> infos = tmpNoRefernceDic[assetType];
if(infos.Count == 0) continue;
EditorGUILayout.LabelField(assetType.ToString(),EditorStyles.boldLabel);
for (int i = 0; i < infos.Count; i++)
{
CreateNpReferenceItem(infos[i]);
}
if (GUILayout.Button("一键删除",GUILayout.Height(30)))
{
DeleteAllAsset(assetType);
}
EditorGUILayout.Space();
}
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
}
[MenuItem("Tools/Find no-Ref Assets", false, 10)]
private static void OpenNoReferenceWindow()
{
NoReferenceWindow window = (NoReferenceWindow)EditorWindow.GetWindow(typeof(NoReferenceWindow),true,"未被引用资源列表");
window.InitAllFileDic();
}
public void RefreshInfo()
{
InitAssetDependDic();
InitLuaFileContentDic();
InitNoReferenceAssetDic();
initComplete = true;
}
private bool IsInFilterDirectory(string assetPath)
{
for (int i = 0; i < FilterDirectory.Length; i++)
{
if (assetPath.Contains(FilterDirectory[i]))
{
return true;
}
}
return false;
}
/// <summary>
/// 找出目标路径下的所有资源
/// </summary>
private void InitAllFileDic()
{
Debug.Log("开始扫描");
long t = System.DateTime.Now.Ticks;
allFileDic = new Dictionary<string, FileInfo>();
string[] guids = AssetDatabase.FindAssets("",ASSET_FILE_ROOT);
for (int i = 0; i < guids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
if (EditorUtility.DisplayCancelableProgressBar($"资源加载中:({i}/{guids.Length})", path,
(float) i / (float) guids.Length))
{
EditorUtility.ClearProgressBar();
Close();
return;
}
if(IsInFilterDirectory(path)) continue;
AssetType assetType = GetAssetTypeByPath(path);
if(assetType == AssetType.None) continue;
FileInfo info = new FileInfo();
info.path = path;
info.assetType = assetType;
info.asset = AssetDatabase.LoadAssetAtPath<Object>(path);
allFileDic.Add(path,info);
}
EditorUtility.ClearProgressBar();
Debug.Log("资源扫描完成");
RefreshInfo();
Debug.Log($"耗时:{(System.DateTime.Now.Ticks - t)/10000} 毫秒");
}
/// <summary>
/// 找出资源的依赖,并添加到依赖列表中
/// </summary>
private void InitAssetDependDic()
{
assetDependsList = new List<FileInfo>();
List<FileInfo> infos = new List<FileInfo>(allFileDic.Values);
if(infos.Count == 0) return;
for (int i = 0; i < infos.Count; i++)
{
FileInfo info = infos[i];
if(info.assetType == AssetType.Lua || info.assetType == AssetType.Image) continue;
if (EditorUtility.DisplayCancelableProgressBar("扫描资源依赖关系...",$"({i}/{infos.Count})\n path:{info.path}",
(float) i / (float) infos.Count))
{
EditorUtility.ClearProgressBar();
Close();
return;
}
string[] depends = AssetDatabase.GetDependencies(info.path,false);
for (int j = 0; j < depends.Length; j++)
{
string dependPath = depends[j];
FileInfo dependInfo = FindFileInfo(dependPath);
// 依赖项为空或者是自己不需要添加
if (dependInfo == null || dependInfo == info) continue;
if (!assetDependsList.Contains(dependInfo))
{
assetDependsList.Add(dependInfo);
}
}
}
Debug.Log("扫描资源依赖关系完成");
EditorUtility.ClearProgressBar();
}
/// <summary>
/// 读取lua文件内容,并保存到列表中(由于项目是用lua热更新的,配置表最终也是生成lua文件,所以lua文件中会配置了引用资源的名称)
/// </summary>
private void InitLuaFileContentDic()
{
luaFileContentDic = new Dictionary<string, string>();
foreach (var info in allFileDic.Values)
{
if (info.assetType == AssetType.Lua)
{
luaFileContentDic.Add(info.path,File.ReadAllText(info.path));
}
}
}
// 初始化未被引用资源
private void InitNoReferenceAssetDic()
{
noReferenceAssetDic = new Dictionary<AssetType, List<FileInfo>>();
foreach (var info in allFileDic.Values)
{
if(info.assetType == AssetType.Lua) continue;
// 判断资源是否被引用(依赖列表中的引用,lua脚本内容列表中的引用)
// 被其他资源引用
if (CheckOtherAssetReference(info.path)) continue;
// 被lua脚本引用
if (CheckLuaReference(info.path)) continue;
AddNoReferenceAsset(info);
}
Debug.Log("未被引用资源添加完成");
}
private FileInfo FindFileInfo(string assetPath)
{
if (!allFileDic.ContainsKey(assetPath)) return null;
return allFileDic[assetPath];
}
private void AddNoReferenceAsset(FileInfo info)
{
List<FileInfo> list = null;
if (!noReferenceAssetDic.TryGetValue(info.assetType, out list))
{
list = new List<FileInfo>();
noReferenceAssetDic.Add(info.assetType,list);
}
list.Add(info);
}
private void RemoveNoReferenceAsset(Dictionary<AssetType,List<FileInfo>> noReferenceAsset,string path)
{
FileInfo info = FindFileInfo(path);
if(info == null) return;
List<FileInfo> list = null;
if (noReferenceAsset.TryGetValue(info.assetType, out list))
{
list.Remove(info);
}
}
private AssetType GetAssetTypeByPath(string assetPath)
{
string extension = Path.GetExtension(assetPath);
switch (extension.ToLower())
{
case ".prefab": return AssetType.Prefab;
case ".mat": return AssetType.Material;
case ".unity": return AssetType.Scene;
case ".asset": return AssetType.Asset;
case ".lua": return AssetType.Lua;
case ".png" : return AssetType.Image;
case ".jpg" : return AssetType.Image;
case ".fbx" : return AssetType.FBX;
case ".controller" : return AssetType.Controller;
case ".anim" : return AssetType.Animation;
}
return AssetType.None;
}
/// <summary>
/// 检查被其他资源引用
/// </summary>
/// <param name="assetPath"></param>
/// <returns></returns>
private bool CheckOtherAssetReference(string assetPath)
{
if (string.IsNullOrEmpty(assetPath))
{
Debug.LogError("CheckOtherAssetReference error: assetPath is null");
return false;
}
FileInfo info = FindFileInfo(assetPath);
if (info == null) return false;
if (info.assetType == AssetType.Scene)
{
return EditorSceneManager.GetActiveScene().name.Equals(Path.GetFileNameWithoutExtension(assetPath));
}
foreach (var dependInfo in assetDependsList)
{
if (dependInfo == info)
{
return true;
}
}
return false;
}
// 检查被lua脚本引用
private bool CheckLuaReference(string assetPath)
{
string fileName = Path.GetFileNameWithoutExtension(assetPath);
if (string.IsNullOrEmpty(fileName))
{
return false;
}
foreach (var content in luaFileContentDic.Values)
{
if (Regex.IsMatch(content, fileName + "\"") || Regex.IsMatch(content, fileName + "\'"))
{
return true;
}
}
return false;
}
private void DeleteAsset(FileInfo info,bool refresh = false)
{
RemoveDepends(info);
RemoveNoReferenceAsset(noReferenceAssetDic,info.path);
if (!isAll && noReferenceSiftAssetDic != null) RemoveNoReferenceAsset(noReferenceSiftAssetDic,info.path);
allFileDic.Remove(info.path);
AssetDatabase.DeleteAsset(info.path);
if(refresh)
AssetDatabase.Refresh();
}
// 删除资源
private void DeleteAllAsset(AssetType assetType)
{
List<FileInfo> infos = noReferenceAssetDic[assetType];
for (int i = infos.Count - 1; i >= 0; i--)
{
DeleteAsset(infos[i]);
}
AssetDatabase.Refresh();
}
// 移除依赖数据
private void RemoveDepends(FileInfo info)
{
if (assetDependsList.Contains(info))
{
assetDependsList.Remove(info);
}
}
// 初始化筛选文件中未被引用资源
private void RefreshNoReferenceSiltAssetDic()
{
noReferenceSiftAssetDic = new Dictionary<AssetType, List<FileInfo>>();
foreach (var info in allFileDic.Values)
{
if(info.assetType == AssetType.Lua) continue;
if(!CheckInSiltDirectory(info.path)) continue;
// 判断资源是否被引用(依赖列表中的引用,lua脚本内容列表中的引用)
// 被其他资源引用
if (CheckOtherAssetReference(info.path)) continue;
// 被lua脚本引用
if (CheckLuaReference(info.path)) continue;
AddNoReferenceSiltAsset(info);
}
}
private bool CheckInSiltDirectory(string assetPath)
{
for (int i = 0; i < siftPathList.Count; i++)
{
string path = siftPathList[i];
if(string.IsNullOrEmpty(path)) continue;
if (!Directory.Exists(path)) continue;
if (Regex.IsMatch(assetPath, path))
{
return true;
}
}
return false;
}
private void AddNoReferenceSiltAsset(FileInfo info)
{
List<FileInfo> list = null;
if (!noReferenceSiftAssetDic.TryGetValue(info.assetType, out list))
{
list = new List<FileInfo>();
noReferenceSiftAssetDic.Add(info.assetType,list);
}
list.Add(info);
}
#region // ---------------------- 绘制相关 ------------------------- //
private void CreateNpReferenceItem(FileInfo info)
{
GUILayout.BeginHorizontal();
// var obj = AssetDatabase.LoadAssetAtPath<Object>(info.path);
EditorGUILayout.ObjectField(info.asset,typeof(UnityEngine.Object));
// string assetName = Path.GetFileNameWithoutExtension(info.path);
// EditorGUILayout.DelayedTextField(assetName);
// if (GUILayout.Button("定位",GUILayout.Width(70)))
// {
// var obj = AssetDatabase.LoadAssetAtPath<Object>(info.path);
// if(obj != null)
// EditorGUIUtility.PingObject(obj);
// }
if (GUILayout.Button("删除资源",GUILayout.Width(70)))
{
DeleteAsset(info,true);
}
GUILayout.EndHorizontal();
}
private void DrawSift()
{
EditorGUILayout.LabelField("筛选列表",EditorStyles.boldLabel,GUILayout.Height(30));
for (int i = 0; i < siftPathList.Count; i++)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Path:",GUILayout.Width(50));
siftPathList[i] = EditorGUILayout.TextField(siftPathList[i]);
if (GUILayout.Button("x",GUILayout.Width(20)))
{
siftPathList.RemoveAt(i);
}
EditorGUILayout.EndHorizontal();
}
if (GUILayout.Button("+",GUILayout.Height(20)))
{
siftPathList.Add(string.Empty);
}
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("筛选资源",GUILayout.Height(40)))
{
string noExistDirectoryStr = "";
for (int i = 0; i < siftPathList.Count; i++)
{
string path = siftPathList[i];
if(string.IsNullOrEmpty(path)) continue;
if (!Directory.Exists(path))
{
noExistDirectoryStr = noExistDirectoryStr + path + "\r\n";
continue;
}
}
if (noExistDirectoryStr != "")
{
EditorUtility.DisplayDialog("提示", "文件夹不存在:\r\n" + noExistDirectoryStr, "ok");
}
RefreshNoReferenceSiltAssetDic();
isAll = false;
}
if (GUILayout.Button("全部资源", GUILayout.Height(40)))
{
isAll = true;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.Space();
}
#endregion // ---------------------- 绘制相关 ------------------------- //
}