关于 Unity 中引用查询的研究

博主目前所知道的 Unity 引用查询的方式有两种:

  1. 将 Unity 的序列化模式设置为 Force Text, 通过文本比对的方式查询引用情况
  2. 通过收集复合资源的依赖情况反向转换为引用情况, 直接进行查询

文本方式

这种查询方式可以自己实现也可以借助工具实现

手动实现

这里就以最简单的方式举例 (只说明思路)

  1. 先收集全部的复合资源

    var guids = AssetDatabase.FindAssets("t:Prefab t:Material t:Scriptableobject t:Scene t:AnimatorController t:Textasset", new string[] {"Assets"});

  2. 依次加载全部文本

    rawData.text = File.ReadAllText(assetFullPath);

  3. 获得要查询资源的 GUID

    guidRules = Selection.assetGUIDs

  4. 依次进行比对

    1
    2
    3
    4
    5
    6
    7
    foreach (var guidRule in guidRules)
    {
    if (Regex.IsMatch(rawData.text, guidRule))
    {
    results.Add(guidRule, rawData.fullPath);
    }
    }

最后汇总结果就可以了, 其中关键的点在于要使用多线程来加快读取 text 以及遍历查询的速度, 否则效率将会非常低

【总结】

在较大的项目中, 初始化需要 33s 时间, 而且每次初始化的时间差异很大, 有时会长达 60s 以上

总时间

查询 1 个文件的耗时 0.7s

单文件

但是随着文件增多, 需要的查询时间也会随之上升, 查询 10 个文件时需要 6s

!110个文件

查询 20 个文件时需要 16s

20个文件

适用情况

  • 由于每次修改 C# 代码都会导致缓存清空, 因此适用于关卡, 美术和优化等基本不会修改代码的人员

  • 制作缓存时, 基本都会使用 OnPostprocesser 这个方法, 从而会加长资源导入的时间, 如果项目中有大量的此方法, 便会导致资源导入非常缓慢

  • 由于查询数增多时, 时间消耗也会大幅度增加, 因此只适用于人工查询, 不适合批量查询

使用 Ripgrep 软件

使用 Ripgrep 软件的查询功能实现引用查询

Ripgrep 是命令行下一个基于行的搜索工具, 使用 Rust 开发, 可以在多平台下运行, RipGrep 官方号称比其它类似工具在搜索速度上快上 N 倍, VSCode 的搜索功能默认就是用的 Ripgrep

vscode

使用 VSCode 执行搜索时在任务管理器中就可以看到

vscode

软件下载地址: Ripgrep 开源地址

将软件导入到 Unity 中, 编写代码使用 Process 类, 设置好参数, 启动软件查询即可

推荐将参数写为可配置的方式, 这样使用过程中需要调整参数时可以直接外部调整

参数

另外 .exe 程序和 .ignore 文件都可以直接放到项目中

程序

在较大的项目中, 每次查询需要消耗 6 ~ 10s

耗时

适用情况

  • 没有了初始化操作带来的时间消耗, 单次查询的时间大幅度缩短, 适用于程序等经常修改代码的人员

  • 由于无法做缓存, 因此也只适用于人工查询, 不适用于批量查询

  • 由于没有使用 OnPostprocesser 方法, 也带来一个好处, 不会对资源导入速度造成任何影响

逆向依赖方式

Unity 目前提供了 AssetDatabase.GetDependencies()EditorUtility.CollectDependencies() 两个方法收集复合类资源的依赖, 通过将依赖关系转化为引用关系, 便可以实现引用情况的查询 (编辑器中可以直接: 资源右键 => Select Dependencies)

【注】由于使用了 Unity 内部的引用关系来做查询, 直接通过代码调用资源接口进行加载的资源将无法被统计

先说一下两个接口的差异

  • AssetDatabase.GetDependencies() 收集的依赖包含无效引用
  • EditorUtility.CollectDependencies() 收集的依赖不包含无效引用

无效引用指的就是 Unity 为了方便开发者而做的一些引用缓存; 以材质球为例, 在切换 Shader 的时候, Unity 并不会将之前 Shader 的相关序列化信息删除, 旧的纹理引用依旧会序列化保存下来

更详细的差异可以看这里: 详细差异

下面说一下实现思路:

  1. 得到全部的复合资源
1
2
3
4
5
6
7
8
var sprites = AssetDatabase.FindAssets("t:SpriteAtlas");
var materials = AssetDatabase.FindAssets("t:Material");
var prefabs = AssetDatabase.FindAssets("t:Prefab");
var sos = AssetDatabase.FindAssets("t:ScriptableObject");
var models = AssetDatabase.FindAssets("t:Model");
var cons = AssetDatabase.FindAssets("t:AnimatorController");
var scenes = AssetDatabase.FindAssets("t:Scene");
var animas = AssetDatabase.FindAssets("t:Animation");
  1. 获取全部依赖

使用 AssetDatabase.GetDependencies(), 因为其可以直接传递路径, 另一个方法需要传递物体, 多一个加载过程的耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
foreach (var key in typeFilter.Keys)
{
foreach (var guid in typeFilter[key])
{
if (dependencies.ContainsKey(guid))
{
// DebugUtil.LogError($"存在相同的 GUID : {guid}", DebugUtil.红);
}
else
{
dependencies.Add(guid, AssetDatabase.GetDependencies(AssetDatabase.GUIDToAssetPath(guid), false));
}
}
}
  1. 将依赖关系转换为引用关系, 有了引用关系的字典就可以直接查询了
1
2
3
4
5
6
7
8
9
10
11
12
foreach (var guid in dependencies.Keys)
{
foreach (var dependPath in dependencies[guid])
{
var dependGuid = AssetDatabase.AssetPathToGUID(dependPath);
if (!assetData.referencedAssets.TryGetValue(dependGuid, out _))
{
assetData.referencedAssets[dependGuid] = new List<string>();
}
assetData.referencedAssets[dependGuid].Add(guid);
}
}

这个方法的耗时主要在构建依赖关系上, 经测试每次构建需要耗时 70s 以上

构建耗时

而将依赖关系转换为引用关系仅花费了 1s 左右

转换数据

查询耗时连 1ms 都不到

查询耗时

适用情况

  • 因为每次构建字典的时间过长, 不适合经常使用, 多适用于批量查询, 全局查询

  • 也可以将字典做成缓存, 使用 OnPostprocesser 来做, 虽然会对资源导入速度有影响, 但是就不必每次都进行字典的构建, 借助字典的查询友好, 可快速查询引用, 资源商店中的 FR2 便是使用了这个思路

总结

思路 方案 初始化耗时 单次查询耗时 多次查询耗时 是否适合批量查询
文本比对 手动实现逻辑 33s ~ 2 min (耗时不稳定) 0.7s 高于线性 不适合
文本比对 Ripgrep 6 ~ 10s 线性 不适合
逆向依赖 逆向依赖 72s 左右 (耗时稳定) ~0ms [字典: O(1)] 线性 适合