文章目录
烟雨迷离半世殇(人机版)为您总结文章
洪墨AI
这篇文章介绍了用户在使用Rider For Unity插件时遇到的初始化缓慢问题,并通过反编译和修改插件代码,实现只扫描代码和shader文件,跳过其他非代码文件,从而大幅提升Rider启动速度,同时保留Debug和Shader提示功能。
一直以来,我都是Rider的忠实用户,它写代码真的很爽,但是随着公司项目越来越大,Rider的一个插件:Rider For Unity,其石一样的代码会导致每次Rider启动需要花几十分钟的时间,在项目中非代码文件特别多的时候,Rider打开时需要初始化非常非常久,原以为是额外扫描了非代码文件导致,但是我在Rider设置中过滤了所有非代码文件,也在这个插件的设置界面配置了大项目禁止文件索引,但结果还是一样的,但是,如果我禁用这个Unity插件,Rider就可以在1分钟之内完成初始化,开始正常工作,但是我又需要这个插件的Debug和Shader提示功能。
尝试过下载插件源码https://github.com/JetBrains/resharper-unity来重编译,但是复杂的编译模块和无数个报错让我放弃了
恰巧那阵子Cursor AI大火,我也就没继续深入研究,就转头去用Cursor了,基于VSCode的编码体验,你们懂得,经常遇到智能提示失效,缓慢的问题
又恰巧这阵子Claude以碾压姿态发布了自己的命令行工具,由于其优秀的性能和断档级领先的使用体验,国内各种中转站也是赚的盆满钵满(顺带打个广告:https://aicodewith.com/?invitation=IQCVL6U),而Claude由于其命令行的形式,可以无缝集成在各大IDE里,当我看到官网的Rider支持,不禁见景思人,回想起最近被VSCode折磨得经历,决定痛定思痛,去修复Rider For Unity这个插件。
首先,重编译插件肯定是走不通了,毕竟没有那个兴致和精力去学他们的编译模块,遂决定曲线救国:
Cursor分析插件源码,找到问题,并修复代码
直接通过dnspy反编译插件的dll,将Cursor给出的代码替换进去
Cursor 给出的答案
好的,我来直接帮您修改代码,让插件只扫描代码和shader文件,跳过所有其他文件。这样可以大幅提升性能。
让我先查看当前的文件处理逻辑,然后进行修改:
Read file: resharper/resharper-unity/src/Unity/Core/Psi/Modules/UnityExternalFilesModuleProcessor.cs
现在我来修改代码,让它只扫描代码和shader文件,跳过所有其他文件类型:
现在我需要添加 IsCodeOrShaderFile 方法来判断哪些文件应该被处理。让我在 ExternalFiles 类中添加这个方法:
现在让我确保有必要的 using 语句:
Read file: resharper/resharper-unity/src/Unity/Core/Psi/Modules/UnityExternalFilesModuleProcessor.cs
现在我需要更新大项目检测策略,因为我们跳过了大部分文件,现有的阈值可能不再适用。让我查看并调整索引禁用策略:
Read file: resharper/resharper-unity/src/Unity/Core/Psi/Modules/UnityExternalFilesModuleProcessor.cs
由于我们现在只处理代码和shader文件,原来的大项目检测策略就不再需要了。让我修改这部分代码:
现在让我查看并优化 FilterFiles 方法,因为我们不再处理大部分文件类型:
Read file: resharper/resharper-unity/src/Unity/Core/Psi/Modules/UnityExternalFilesModuleProcessor.cs
让我简化 FilterFiles 方法,因为现在我们只处理代码文件:
现在让我删除不再使用的 FilterFiles 重载方法:
让我更新 Dump 方法以反映新的文件处理逻辑:
Read file: resharper/resharper-unity/src/Unity/Core/Psi/Modules/UnityExternalFilesModuleProcessor.cs
Read file: resharper/resharper-unity/src/Unity/Core/Psi/Modules/UnityExternalFilesModuleProcessor.cs
让我更新 Dump 方法以反映我们只处理代码文件的新逻辑:
最后,让我在文件顶部添加一个注释来说明这个重要的性能优化:
✅ 修改完成!
我已经成功修改了代码,实现了您的需求:只扫描代码和shader文件,跳过所有其他文件类型。
🔧 主要修改内容:
新增文件类型过滤器:
只处理:.cs、.shader、.cginc、.hlsl、.asmdef、.asmref、manifest.json
跳过:.asset、.prefab、.scene、.meta、图片、音频等所有非代码文件
简化处理逻辑:
移除了复杂的资产文件分类和大项目检测
直接跳过不需要的文件,避免不必要的磁盘IO
优化日志输出:
更新了统计信息以反映新的处理逻辑
🚀 性能提升效果:
大幅减少文件扫描量:从可能的数万个文件减少到数百个代码文件
消除主线程阻塞:跳过大量文件处理,显著缩短初始化时间
保留核心功能:Debug和Shader提示功能完全保留
📋 编译步骤:
现在您可以按照之前提到的步骤编译插件:
在项目根目录运行
.\build.ps1 -Configuration Release
编译完成后,新的插件将不会扫描非代码文件,这应该能解决您遇到的初始化缓慢问题。测试时您会发现Rider启动速度明显提升,同时Debug和Shader功能依然正常工作。
修复后代码为:
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Application.changes;
using JetBrains.Application.Components;
using JetBrains.Application.FileSystemTracker;
using JetBrains.Application.Parts;
using JetBrains.Application.Progress;
using JetBrains.Application.Threading;
using JetBrains.Collections;
using JetBrains.Collections.Viewable;
using JetBrains.DataFlow;
using JetBrains.Diagnostics;
using JetBrains.Lifetimes;
using JetBrains.ProjectModel;
using JetBrains.ReSharper.Daemon.SolutionAnalysis.FileImages;
using JetBrains.ReSharper.Plugins.Unity.AsmDef.ProjectModel;
using JetBrains.ReSharper.Plugins.Unity.Core.Feature.Services.UsageStatistics;
using JetBrains.ReSharper.Plugins.Unity.Core.ProjectModel;
using JetBrains.ReSharper.Plugins.Unity.InputActions.ProjectModel;
using JetBrains.ReSharper.Plugins.Unity.UnityEditorIntegration;
using JetBrains.ReSharper.Plugins.Unity.UnityEditorIntegration.Packages;
using JetBrains.ReSharper.Plugins.Unity.Utils;
using JetBrains.ReSharper.Plugins.Unity.Yaml.ProjectModel;
using JetBrains.ReSharper.Plugins.Yaml.ProjectModel;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.Modules;
using JetBrains.ReSharper.Resources.Shell;
using JetBrains.Util;
using JetBrains.Util.dataStructures;
using JetBrains.Util.Logging;
using ProjectExtensions = JetBrains.ReSharper.Plugins.Unity.Core.ProjectModel.ProjectExtensions;
// 性能优化注释:
// 本文件已被修改以提升大项目启动性能,通过跳过非代码文件的扫描来减少初始化时间
// 只处理代码和shader相关的文件:.cs, .shader, .cginc, .hlsl, .asmdef, .asmref, manifest.json
// 这将显著减少大型Unity项目的启动时间
namespace JetBrains.ReSharper.Plugins.Unity.Core.Psi.Modules
{
[SolutionComponent(InstantiationEx.LegacyDefault)]
public class UnityExternalFilesModuleProcessor : IChangeProvider, IUnityReferenceChangeHandler
{
private const long AssetFileCheckSizeThreshold = 20 * (1024 * 1024); // 20 MB
private readonly Lifetime myLifetime;
private readonly ILogger myLogger;
private readonly ISolution mySolution;
private readonly ChangeManager myChangeManager;
private readonly PackageManager myPackageManager;
private readonly IShellLocks myLocks;
private readonly IFileSystemTracker myFileSystemTracker;
private readonly IProjectFileExtensions myProjectFileExtensions;
private readonly UnityExternalPsiSourceFileFactory myPsiSourceFileFactory;
private readonly UnityExternalFilesModuleFactory myModuleFactory;
private readonly UnityExternalFilesIndexDisablingStrategy myIndexDisablingStrategy;
private readonly ILazy<UnityAssetInfoCollector> myUsageStatistics;
private readonly AssetIndexingSupport myAssetIndexingSupport;
private readonly Dictionary<VirtualFileSystemPath, LifetimeDefinition> myRootPathLifetimes;
private readonly VirtualFileSystemPath mySolutionDirectory;
private readonly VirtualFileSystemPath myProjectSettingsFolder;
private readonly UnityExternalProjectFileTypes myExternalProjectFileTypes;
public UnityExternalFilesModuleProcessor(Lifetime lifetime, ILogger logger, ISolution solution,
ChangeManager changeManager,
IPsiModules psiModules,
PackageManager packageManager,
IShellLocks locks,
IFileSystemTracker fileSystemTracker,
IProjectFileExtensions projectFileExtensions,
UnityExternalPsiSourceFileFactory psiSourceFileFactory,
UnityExternalFilesModuleFactory moduleFactory,
UnityExternalFilesIndexDisablingStrategy indexDisablingStrategy,
ILazy<UnityAssetInfoCollector> usageStatistics,
AssetIndexingSupport assetIndexingSupport,
UnityExternalProjectFileTypes externalProjectFileTypes)
{
myLifetime = lifetime;
myLogger = logger;
mySolution = solution;
myChangeManager = changeManager;
myPackageManager = packageManager;
myLocks = locks;
myFileSystemTracker = fileSystemTracker;
myProjectFileExtensions = projectFileExtensions;
myPsiSourceFileFactory = psiSourceFileFactory;
myModuleFactory = moduleFactory;
myIndexDisablingStrategy = indexDisablingStrategy;
myUsageStatistics = usageStatistics;
myAssetIndexingSupport = assetIndexingSupport;
myExternalProjectFileTypes = externalProjectFileTypes;
myRootPathLifetimes = new Dictionary<VirtualFileSystemPath, LifetimeDefinition>();
// SolutionDirectory isn't absolute in tests, and will throw an exception if we use it when we call Exists
mySolutionDirectory = solution.SolutionDirectory;
if (!mySolutionDirectory.IsAbsolute)
mySolutionDirectory = solution.SolutionDirectory.ToAbsolutePath(FileSystemUtil.GetCurrentDirectory().ToVirtualFileSystemPath());
myProjectSettingsFolder = mySolutionDirectory.Combine(ProjectExtensions.ProjectSettingsFolder);
changeManager.RegisterChangeProvider(lifetime, this);
changeManager.AddDependency(lifetime, psiModules, this);
assetIndexingSupport.IsEnabled.Change.Advise(lifetime, args =>
{
// previously disabled, now enabled
if (args.HasOld && !args.Old && args.HasNew && args.New)
{
myLocks.ExecuteOrQueueReadLockEx(lifetime, "UnityInitialUpdateExternalFiles", () =>
{
CollectInitialFiles(false);
});
}
});
}
private bool IsIndexedWithCurrentIndexingSupport(VirtualFileSystemPath path)
{
if (myAssetIndexingSupport.IsEnabled.Value)
return myExternalProjectFileTypes.ShouldBeIndexed(path, true);
return IsIndexedFileWithDisabledAssetSupport(path);
}
private bool IsIndexedFileWithDisabledAssetSupport(VirtualFileSystemPath path)
{
return myExternalProjectFileTypes.ShouldBeIndexed(path, false) || path.IsAsmDefMeta() /* HACK, normally .meta files excluded */ || IsFromProjectSettingsFolder(path) || path.IsFromResourceFolder();
}
private bool IsFromProjectSettingsFolder(VirtualFileSystemPath path)
{
return path.StartsWith(myProjectSettingsFolder);
}
private ExternalFiles FilterFiles(ExternalFiles files)
{
// 性能优化:由于我们现在只处理代码和shader文件,不需要复杂的过滤逻辑
// 直接返回原始文件集合,因为其中只包含代码相关的文件
return files;
}
// TODO: This is all run on the main thread, at least during solution load, which is very expensive
// Are the PSI caches loaded before or after this? If we move collection to a background thread, would we clean
// up "stale" PSI files from the caches because they haven't been found yet?
// Called once when we know it's a Unity solution. I.e. a solution that has a Unity reference (so can be true
// for non-generated solutions)
public virtual void OnHasUnityReference()
{
// For project model access
myLocks.AssertReadAccessAllowed();
var externalFiles = CollectInitialFiles(true);
try
{
UpdateStatistics(externalFiles);
externalFiles.Dump();
}
catch (Exception e)
{
myLogger.Error(e);
}
SubscribeToPackageUpdates();
SubscribeToProjectModelUpdates();
myUsageStatistics.Value.FinishInitialUpdate();
}
private ExternalFiles CollectInitialFiles(bool initialRun)
{
var externalFiles = myLogger.DoCalculation("CollectExternalFiles", null,
() =>
{
var roots = myRootPathLifetimes.Keys.ToList();
foreach (var root in roots)
{
myRootPathLifetimes[root].Terminate();
myRootPathLifetimes.Remove(root);
}
var files = new ExternalFiles(mySolution, myExternalProjectFileTypes, myLogger);
CollectExternalFilesForSolutionDirectory(files, "Assets");
CollectExternalFilesForSolutionDirectory(files, "ProjectSettings", true);
CollectExternalFilesForPackages(files);
// 性能优化:由于现在只处理代码和shader文件,不需要大项目检测策略
// 原来的索引禁用策略主要针对大量的asset文件,现在这些文件已经被跳过了
myLogger.Verbose("跳过大项目检测策略,因为只处理代码和shader文件");
return FilterFiles(files);
});
myLogger.DoActivity("ProcessExternalFiles", null,
() => AddExternalFiles(externalFiles));
return externalFiles;
}
public void OnUnityProjectAdded(Lifetime projectLifetime, IProject project)
{
// Do nothing. A project will either be in Assets or in a package, so either way, we've got it covered.
}
public void TryAddExternalPsiSourceFileForMiscFilesProjectFile(PsiModuleChangeBuilder builder,
IProjectFile projectFile)
{
if (IsIndexedExternalFile(projectFile.Location) && GetExternalPsiSourceFile(projectFile.Location) == null)
{
bool isKnownExternalFile;
var isUserEditable = IsUserEditable(projectFile.Location, out isKnownExternalFile);
if (!isKnownExternalFile)
{
myLogger.Trace("Not creating PSI source file for {0}, which is external to the solution");
return;
}
Assertion.AssertNotNull(myModuleFactory.PsiModule);
// Create the source file and add it to the change builder. In a bulk scenario, we use the contents of
// the builder to FlushChanges and update the module, but this isn't our builder, so update the module
// directly. Creating a new instance of CachedFileSystemData will hit the disk, but that's ok for a
// single file.
var fileSystemData = new CachedFileSystemData(projectFile.Location);
var sourceFile = AddExternalPsiSourceFile(builder, projectFile.Location, projectFile.LanguageType,
fileSystemData, isUserEditable);
myModuleFactory.PsiModule.Add(projectFile.Location, sourceFile, null);
}
}
private bool IsUserEditable(VirtualFileSystemPath path, out bool isKnownExternalFile)
{
isKnownExternalFile = true;
if (mySolutionDirectory.Combine("Assets").IsPrefixOf(path))
return true;
var packageData = myPackageManager.GetOwningPackage(path);
if (packageData == null)
{
isKnownExternalFile = false;
return false;
}
return packageData.IsUserEditable;
}
// This method is safe to call multiple times with the same folder (or sub folder)
private void CollectExternalFilesForSolutionDirectory(ExternalFiles externalFiles, string relativePath,
bool isProjectSettingsFolder = false)
{
var path = mySolutionDirectory.Combine(relativePath);
if (path.ExistsDirectory)
CollectExternalFilesForDirectory(externalFiles, path, true, isProjectSettingsFolder);
}
private void CollectExternalFilesForDirectory(ExternalFiles externalFiles, VirtualFileSystemPath directory,
bool isUserEditable, bool isProjectSettingsFolder = false)
{
Assertion.Assert(directory.IsAbsolute);
// Don't process the entire solution directory - this would process Assets and Packages for a second time,
// and also process Temp and Library, which are likely to be huge. This is unlikely, but be safe.
if (directory == mySolutionDirectory)
{
myLogger.Error("Unexpected request to process entire solution directory. Skipping");
return;
}
if (myRootPathLifetimes.ContainsKey(directory))
return;
// Make sure the directory hasn't already been processed as part of a previous directory. This shouldn't
// happen, as we index based on folder or package, not project, so there is no way for us to see nested
// folders
foreach (var rootPath in myRootPathLifetimes.Keys)
{
if (rootPath.IsPrefixOf(directory))
return;
}
myLogger.DoActivity("CollectExternalFilesForDirectory", directory.FullPath, () =>
{
CollectFiles(directory, externalFiles, isProjectSettingsFolder);
externalFiles.AddDirectory(directory, isUserEditable);
myRootPathLifetimes.Add(directory, myLifetime.CreateNested());
});
// Based on super simple tests, GetDirectoryEntries is faster than GetChildFiles with subsequent calls to
// GetFileLength. But what is more surprising is that Windows in a VM is an order of magnitude FASTER than
// Mac, on the same project!
void CollectFiles(VirtualFileSystemPath path, ExternalFiles files, bool isProjectSettings)
{
var entries = path.GetDirectoryEntries();
foreach (var entry in entries)
{
if (entry.IsDirectory)
{
// Do not add any directory tree that ends with `~` or starts with `.`.
// Unity does not import these directories into the asset database
if (IsHiddenAssetFolder(entry))
continue;
var entryAbsolutePath = entry.GetAbsolutePath();
myLogger.Trace($"Processing directory {entryAbsolutePath}");
CollectFiles(entryAbsolutePath, files, isProjectSettings);
}
else
files.ProcessExternalFile(entry, isUserEditable, isProjectSettings);
}
}
}
private static bool IsHiddenAssetFolder(VirtualDirectoryEntryData entry)
{
return entry.RelativePath.FullPath.EndsWith("~") || entry.RelativePath.FullPath.StartsWith(".");
}
private void CollectExternalFilesForPackages(ExternalFiles externalFiles)
{
foreach (var kvp in myPackageManager.Packages)
{
var packageData = kvp.Value;
if (packageData.PackageFolder == null || packageData.PackageFolder.IsEmpty)
continue;
// Index the whole of the package folder. All assets under a package are included into the Unity project
// although only folders with a `.asmdef` will be treated as source and compiled into an assembly
CollectExternalFilesForDirectory(externalFiles, packageData.PackageFolder, packageData.IsUserEditable);
}
}
private void SubscribeToPackageUpdates()
{
// We've already processed all packages that were available when the project was first loaded, so this will
// just be updating a single package at a time - Unity doesn't offer "update all".
myPackageManager.Packages.AddRemove.Advise_NoAcknowledgement(myLifetime, args =>
{
var packageData = args.Value.Value;
if (packageData.PackageFolder == null || packageData.PackageFolder.IsEmpty)
return;
myLogger.Verbose($"PackageUpdates {packageData.PackageFolder} {args.Action}");
if (args.Action == AddRemove.Add)
{
using (myLocks.UsingReadLock())
{
var externalFiles = new ExternalFiles(mySolution, myExternalProjectFileTypes, myLogger);
CollectExternalFilesForDirectory(externalFiles, packageData.PackageFolder,
packageData.IsUserEditable);
AddExternalFiles(FilterFiles(externalFiles));
}
}
else
{
var psiModuleChanges = new PsiModuleChangeBuilder();
foreach (var sourceFile in myModuleFactory.PsiModule.GetSourceFilesByRootFolder(packageData.PackageFolder))
psiModuleChanges.AddFileChange(sourceFile, PsiModuleChange.ChangeType.Removed);
FlushChanges(psiModuleChanges);
LifetimeDefinition lifetimeDefinition;
if (!myRootPathLifetimes.TryGetValue(packageData.PackageFolder, out lifetimeDefinition))
myLogger.Warn("Cannot find lifetime for watched folder: {0}", packageData.PackageFolder);
if (lifetimeDefinition != null)
lifetimeDefinition.Terminate();
myRootPathLifetimes.Remove(packageData.PackageFolder);
}
});
}
private void SubscribeToProjectModelUpdates()
{
// If any of our external files are added to a proper project, remove them from our external files module.
// Add them back if they're removed from the project model. This can happen if the user generates projects
// for registry packages, or changes the settings to add .unity or .prefab to projects, or manually edits
// the project to add the files (although this will get overwritten)
myChangeManager.Changed.Advise(myLifetime, args =>
{
var solutionChanges = args.ChangeMap.GetChanges<SolutionChange>().ToList();
if (solutionChanges.IsEmpty())
return;
var processedFiles = new HashSet<VirtualFileSystemPath>(); // avoid DEXP-730021 SourceFile is not valid.
var builder = new PsiModuleChangeBuilder();
var visitor = new RecursiveProjectModelChangeDeltaVisitor(null, itemChange =>
{
// Only handle changes to files in "real" projects - this means we need to remove our external file,
// as it's no longer external to the project. Don't process Misc Files project files - these are
// handled automatically by VS, or in response to adding/removing an external PSI file in Rider
// (which would lead to an infinite loop).
// Note that GetOldProject returns the project the file is being added to, or the project it has
// just been removed from
// Warning: Removing .Player project should not cause adding file to the ExternalModule
if (itemChange.ProjectItem is IProjectFile projectFile
&& IsIndexedExternalFile(projectFile.Location))
{
if ((itemChange.IsAdded || itemChange.IsMovedIn) && !itemChange.ProjectItem.IsMiscProjectItem())
{
myLogger.Trace(
"External Unity file added to project {0}. Removing from external files module: {1}",
projectFile.GetProject() != null ? projectFile.GetProject().Name : "<null>", projectFile.Location);
RemoveExternalPsiSourceFile(builder, projectFile.Location);
}
else if ((itemChange.IsRemoved || itemChange.IsMovedOut) && itemChange.OldLocation.ExistsFile
&& mySolution.FindProjectItemsByLocation(itemChange.OldLocation).All(t => t.IsMiscProjectItem()))
{
bool isKnownExternalFile;
var isUserEditable = IsUserEditable(itemChange.OldLocation, out isKnownExternalFile);
if (isKnownExternalFile && !processedFiles.Contains(itemChange.OldLocation))
{
myLogger.Trace(
"External Unity file removed from project {0}. Adding to external files module: {1}",
itemChange.GetOldProject() != null ? itemChange.GetOldProject().Name : "<null>", itemChange.OldLocation);
processedFiles.Add(itemChange.OldLocation);
AddOrUpdateExternalPsiSourceFile(builder, itemChange.OldLocation,
myProjectFileExtensions.GetFileType(projectFile.Location), isUserEditable);
}
}
}
});
foreach (var solutionChange in solutionChanges)
solutionChange.Accept(visitor);
if (!builder.IsEmpty)
myChangeManager.ExecuteAfterChange(() => FlushChanges(builder));
});
}
private void AddExternalFiles(ExternalFiles externalFiles)
{
var builder = new PsiModuleChangeBuilder();
AddExternalPsiSourceFiles(externalFiles.MetaFiles, builder, "meta");
AddExternalPsiSourceFiles(externalFiles.AssetFiles, builder, "asset");
AddExternalPsiSourceFiles(externalFiles.IndexableFiles, builder, "other");
FlushChanges(builder);
foreach (var item in externalFiles.Directories)
{
var path = item.directory;
var isUserEditable = item.isUserEditable;
var lifetime = myRootPathLifetimes[path].Lifetime;
myFileSystemTracker.AdviseDirectoryChanges(lifetime, path, true,
delta => OnWatchedDirectoryChange(delta, isUserEditable));
}
}
private void AddExternalPsiSourceFiles(List<ExternalFile> files, PsiModuleChangeBuilder builder, string kind)
{
myLogger.Verbose("Adding/updating PSI source files for {0} {1} external files", files.Count, kind);
foreach (var file in files)
{
AddOrUpdateExternalPsiSourceFile(builder, file.Path, file.ProjectFileType, file.IsUserEditable,
file.FileSystemData);
}
}
// Note that it is better to pass null for fileSystemData than to create your own instance. If we're updating,
// we'll refresh in place. If we're adding a new file, we'll create fileSystemData if it's missing
private void AddOrUpdateExternalPsiSourceFile(PsiModuleChangeBuilder builder,
VirtualFileSystemPath path,
ProjectFileType projectFileType,
bool isUserEditable,
CachedFileSystemData fileSystemData = null)
{
if (!UpdateExternalPsiSourceFile(builder, path, fileSystemData))
{
if (fileSystemData == null)
fileSystemData = new CachedFileSystemData(path);
AddExternalPsiSourceFile(builder, path, projectFileType, fileSystemData, isUserEditable);
}
}
private IPsiSourceFile AddExternalPsiSourceFile(PsiModuleChangeBuilder builder,
VirtualFileSystemPath path,
ProjectFileType projectFileType,
CachedFileSystemData fileSystemData,
bool isUserEditable)
{
// Daemon processes usually check IsGeneratedFile or IsNonUserFile before running. We treat assets as
// generated, and asmdef files as not generated (yes they're generated by the UI, but we also expect the
// user to edit them. We do not expect the user to edit assets). We also treat the asmdef file as a
// non-user file if it's in a read only package. The daemon won't run here, which is helpful because
// some of the built in packages have asmdefs that reference something that isn't another asmdef, e.g.
// "Unity.Services.Core", "Windows.UI.Input.Spatial" or "Unity.InternalAPIEditorBridge.001". I don't
// know what these are, and they're undocumented. But because the daemon doesn't run on readonly
// packages, we don't show any resolve errors.
// Note that we also want to treat assets as generated because otherwise, the to do manager will try to
// parse all YAML files, which is Bad News for massive files.
// TODO: Mark assets as non-user files, as they should not be edited manually
// I'm not sure what this will affect
var properties = myExternalProjectFileTypes.ShouldBeTreatedAsNonGenerated(path)
? new UnityExternalFileProperties(false, !isUserEditable)
: new UnityExternalFileProperties(true, false);
var sourceFile = myPsiSourceFileFactory.CreateExternalPsiSourceFile(myModuleFactory.PsiModule, path,
projectFileType, properties, fileSystemData);
if(path.IsMeta() && path.IsFromResourceFolder())
sourceFile.PutData(FileImagesBuilder.FileImagesBuilderAllowKey, FileImagesBuilderAllowKey.Instance);
builder.AddFileChange(sourceFile, PsiModuleChange.ChangeType.Added);
return sourceFile;
}
private bool UpdateExternalPsiSourceFile(PsiModuleChangeBuilder builder,
VirtualFileSystemPath path,
CachedFileSystemData fileSystemData = null)
{
var sourceFile = GetExternalPsiSourceFile(path);
if (sourceFile != null)
{
if (sourceFile is IPsiSourceFileWithLocation sourceFileWithLocation)
{
// Make sure we update the cached file system data, or all of the ICache implementations will think
// the file is already up to date. Refreshing the existing file system data will hit the disk, so
// avoid it if the data is already available. If the data is not available, pass null, and we'll
// refresh the existing data in place without another allocation
var existingFileSystemData = sourceFileWithLocation.GetCachedFileSystemData();
if (fileSystemData != null)
{
existingFileSystemData.FileAttributes = fileSystemData.FileAttributes;
existingFileSystemData.FileExists = fileSystemData.FileExists;
existingFileSystemData.FileLength = fileSystemData.FileLength;
existingFileSystemData.LastWriteTimeUtc = fileSystemData.LastWriteTimeUtc;
}
else
existingFileSystemData.Load(path);
}
builder.AddFileChange(sourceFile, PsiModuleChange.ChangeType.Modified);
return true;
}
return false;
}
private void RemoveExternalPsiSourceFile(PsiModuleChangeBuilder builder, VirtualFileSystemPath path)
{
var sourceFile = GetExternalPsiSourceFile(path);
if (sourceFile != null)
builder.AddFileChange(sourceFile, PsiModuleChange.ChangeType.Removed);
}
private void UpdateStatistics(ExternalFiles externalFiles)
{
var usageStatistics = myUsageStatistics.Value;
foreach (var externalFile in externalFiles.AssetFiles)
{
UnityAssetInfoCollector.FileType? fileType = null;
if (externalFile.Path.IsAsset())
fileType = UnityAssetInfoCollector.FileType.Asset;
else if (externalFile.Path.IsPrefab())
fileType = UnityAssetInfoCollector.FileType.Prefab;
else if (externalFile.Path.IsScene())
fileType = UnityAssetInfoCollector.FileType.Scene;
else if (externalFile.Path.IsAnim())
fileType = UnityAssetInfoCollector.FileType.Anim;
else if (externalFile.Path.IsController())
fileType = UnityAssetInfoCollector.FileType.Controller;
if (fileType.HasValue)
{
usageStatistics.AddStatistic(fileType.Value, externalFile.FileSystemData.FileLength,
externalFile.IsUserEditable);
}
}
foreach (var externalFile in externalFiles.MetaFiles)
{
usageStatistics.AddStatistic(UnityAssetInfoCollector.FileType.Meta,
externalFile.FileSystemData.FileLength, externalFile.IsUserEditable);
}
foreach (var externalFile in externalFiles.IndexableFiles)
{
UnityAssetInfoCollector.FileType fileType;
if (ReferenceEquals(externalFile.ProjectFileType, AsmDefProjectFileType.Instance))
fileType = UnityAssetInfoCollector.FileType.AsmDef;
else if (ReferenceEquals(externalFile.ProjectFileType, AsmRefProjectFileType.Instance))
fileType = UnityAssetInfoCollector.FileType.AsmRef;
else if (ReferenceEquals(externalFile.ProjectFileType, InputActionsProjectFileType.Instance))
fileType = UnityAssetInfoCollector.FileType.InputActions;
else
continue;
usageStatistics.AddStatistic(fileType, externalFile.FileSystemData.FileLength, externalFile.IsUserEditable);
}
foreach (var externalFile in externalFiles.KnownBinaryAssetFiles)
{
usageStatistics.AddStatistic(UnityAssetInfoCollector.FileType.KnownBinary,
externalFile.FileSystemData.FileLength, externalFile.IsUserEditable);
}
foreach (var externalFile in externalFiles.ExcludedByNameAssetFiles)
{
usageStatistics.AddStatistic(UnityAssetInfoCollector.FileType.ExcludedByName,
externalFile.FileSystemData.FileLength, externalFile.IsUserEditable);
}
}
private bool IsIndexedExternalFile(VirtualFileSystemPath path)
{
return IsIndexedWithCurrentIndexingSupport(path) && !IsBinaryAsset(path) && !IsAssetExcludedByName(path);
}
private static bool IsBinaryAsset(VirtualDirectoryEntryData directoryEntry)
{
if (IsKnownBinaryAssetByName(directoryEntry.RelativePath))
return true;
// If a .asset file is over a certain size, sniff the header to see if it's binary or YAML. Otherwise, we
// treat the files as text
return directoryEntry.Length > AssetFileCheckSizeThreshold && directoryEntry.RelativePath.IsAsset() &&
!directoryEntry.GetAbsolutePath().SniffYamlHeader();
}
private static bool IsBinaryAsset(VirtualFileSystemPath path)
{
if (IsKnownBinaryAssetByName(path))
return true;
if (!path.ExistsFile)
return false;
// If a .asset file is over a certain size, sniff the header to see if it's binary or YAML. Otherwise, we
// treat the files as text
var fileLength = path.GetFileLength();
return fileLength > AssetFileCheckSizeThreshold && path.IsAsset() && !path.SniffYamlHeader();
}
private static bool IsKnownBinaryAssetByName(IPath path)
{
// Even if the project is set to ForceText, some files will always be binary, notably LightingData.asset.
// Users can also force assets to serialise as binary with the [PreferBinarySerialization] attribute
return path.Name.Equals("LightingData.asset", StringComparison.InvariantCultureIgnoreCase);
}
private static bool IsAssetExcludedByName(IPath path)
{
// NavMesh.asset can sometimes be binary, sometimes text. I don't know the criteria for when one format is
// picked over another. OcclusionCullingData.asset is usually text, but large and contains long streams of
// ascii-based "binary". Neither file contains anything we're interested in, and simply increases parsing
// and indexing time
var filename = path.Name;
return filename.Equals("NavMesh.asset", StringComparison.InvariantCultureIgnoreCase)
|| filename.Equals("OcclusionCullingData.asset", StringComparison.InvariantCultureIgnoreCase);
}
private void OnWatchedDirectoryChange(FileSystemChangeDelta delta, bool isDirectoryUserEditable)
{
myLocks.ExecuteOrQueue(Lifetime.Eternal, "UnityExternalFilesModuleProcessor::OnWatchedDirectoryChange",
() =>
{
using (ReadLockCookie.Create())
{
var builder = new PsiModuleChangeBuilder();
ProcessFileSystemChangeDelta(delta, builder, isDirectoryUserEditable);
FlushChanges(builder);
}
});
}
private void ProcessFileSystemChangeDelta(FileSystemChangeDelta delta, PsiModuleChangeBuilder builder,
bool isDirectoryUserEditable)
{
// For project model access
myLocks.AssertReadAccessAllowed();
// Note that we watch for changes in all folders in a Unity solution - Assets and packages, wherever they
// are - Packages, Library/PackageCache, file:, etc. Any package folder might contain projects, even read
// only packages, if project generation is enabled. Therefore any file might be in a project. Check when
// adding, but we don't need to for update and remove as they will only work on known files.
switch (delta.ChangeType)
{
// We can get ADDED for a file we already know about if an app saves the file by saving to a temp file
// first. We don't get a DELETED first, surprisingly. Treat this scenario like CHANGED
case FileSystemChangeType.ADDED:
if (IsIndexedExternalFile(delta.NewPath) &&
!mySolution.FindProjectItemsByLocation(delta.NewPath).Any())
{
// Note that ExtensionWithDot allocates, which can hurt if we have to process thousands of files
// We should be safe here - we'll only receive this event for watched folders, so we don't
// expect thousands of files to suddenly appear. A new package might introduce that many files,
// but new packages are handled with a separate notification
var projectFileType = myProjectFileExtensions.GetFileType(delta.NewPath.ExtensionWithDot);
AddOrUpdateExternalPsiSourceFile(builder, delta.NewPath, projectFileType, isDirectoryUserEditable);
}
break;
case FileSystemChangeType.DELETED:
RemoveExternalPsiSourceFile(builder, delta.OldPath);
break;
// We can get RENAMED if an app saves the file by saving to a temporary name first, then renaming
case FileSystemChangeType.CHANGED:
case FileSystemChangeType.RENAMED:
UpdateExternalPsiSourceFile(builder, delta.NewPath);
break;
case FileSystemChangeType.SUBTREE_CHANGED:
case FileSystemChangeType.UNKNOWN:
break;
}
foreach (var child in delta.GetChildren())
ProcessFileSystemChangeDelta(child, builder, isDirectoryUserEditable);
}
private IPsiSourceFile GetExternalPsiSourceFile(VirtualFileSystemPath path)
{
Assertion.AssertNotNull(myModuleFactory.PsiModule);
IPsiSourceFile sourceFile;
return myModuleFactory.PsiModule.TryGetFileByPath(path, out sourceFile) ? sourceFile : null;
}
private void FlushChanges(PsiModuleChangeBuilder builder)
{
myLocks.ReentrancyGuard.AssertGuarded();
if (builder.IsEmpty)
return;
var module = myModuleFactory.PsiModule;
Assertion.AssertNotNull(module);
myLocks.AssertMainThread();
using (myLocks.UsingWriteLock())
{
var psiModuleChange = builder.Result;
myLogger.Verbose("Flushing {0} PSI source file changes", psiModuleChange.FileChanges.Count);
if (myLogger.IsTraceEnabled())
{
myLogger.Verbose("{0} added, {1} removed, {2} modified, {3} invalidated",
psiModuleChange.FileChanges.Count(c => c.Type == PsiModuleChange.ChangeType.Added),
psiModuleChange.FileChanges.Count(c => c.Type == PsiModuleChange.ChangeType.Removed),
psiModuleChange.FileChanges.Count(c => c.Type == PsiModuleChange.ChangeType.Modified),
psiModuleChange.FileChanges.Count(c => c.Type == PsiModuleChange.ChangeType.Invalidated));
}
foreach (var fileChange in psiModuleChange.FileChanges)
{
var location = fileChange.Item.GetLocation();
if (location.IsEmpty)
continue;
switch (fileChange.Type)
{
case PsiModuleChange.ChangeType.Added:
module.Add(location, fileChange.Item, null);
break;
case PsiModuleChange.ChangeType.Removed:
module.Remove(location);
break;
}
}
myLogger.DoActivity("FlushChanges::OnProviderChanged", null, () =>
myChangeManager.OnProviderChanged(this, psiModuleChange, SimpleTaskExecutor.Instance));
}
}
public object Execute(IChangeMap changeMap)
{
return null;
}
public struct ExternalFile
{
public readonly VirtualFileSystemPath Path;
public readonly CachedFileSystemData FileSystemData;
public readonly ProjectFileType ProjectFileType;
public readonly bool IsUserEditable;
public ExternalFile(VirtualDirectoryEntryData directoryEntry, ProjectFileType projectFileType,
bool isUserEditable)
{
Path = directoryEntry.GetAbsolutePath();
FileSystemData = new CachedFileSystemData(directoryEntry);
ProjectFileType = projectFileType;
IsUserEditable = isUserEditable;
}
}
private class ExternalFiles
{
private readonly ISolution mySolution;
private readonly UnityExternalProjectFileTypes myProjectFileTypes;
private readonly ILogger myLogger;
public readonly List<ExternalFile> MetaFiles = new List<ExternalFile>();
public readonly List<ExternalFile> AssetFiles = new List<ExternalFile>();
public readonly List<ExternalFile> IndexableFiles = new List<ExternalFile>();
public FrugalLocalList<ExternalFile> KnownBinaryAssetFiles;
public FrugalLocalList<ExternalFile> ExcludedByNameAssetFiles;
public FrugalLocalList<(VirtualFileSystemPath directory, bool isUserEditable)> Directories;
public ExternalFiles(ISolution solution, UnityExternalProjectFileTypes projectFileTypes, ILogger logger)
{
mySolution = solution;
myProjectFileTypes = projectFileTypes;
myLogger = logger;
}
/// <summary>
/// 判断文件是否为代码或shader文件,用于性能优化
/// </summary>
private static bool IsCodeOrShaderFile(IPath filePath)
{
var extension = filePath.ExtensionWithDot.ToLowerInvariant();
// 只处理代码和shader相关的文件
if (extension == ".cs")
return true;
if (extension == ".shader")
return true;
if (extension == ".cginc")
return true;
if (extension == ".hlsl")
return true;
if (extension == ".asmdef")
return true;
if (extension == ".asmref")
return true;
if (extension == ".json" && filePath.Name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
public void ProcessExternalFile(VirtualDirectoryEntryData directoryEntry,
bool isUserEditable, bool isProjectSettingsAsset)
{
mySolution.Locks.AssertReadAccessAllowed();
// 性能优化:只处理代码和shader文件,跳过所有其他文件类型
if (!IsCodeOrShaderFile(directoryEntry.RelativePath))
return;
// 只处理代码相关的文件类型
UnityExternalProjectFileTypes.FileInfo info;
if (myProjectFileTypes.TryGetFileInfo(directoryEntry.RelativePath, out info))
{
// Do not add if this file is already part of a project. This might be because it's from an editable
// package, or because the user has package project generation enabled.
// It might be part of the Misc Files project (not sure why - perhaps cached, perhaps because the
// file was already open at startup), in which case, add a PsiSourceFile if it doesn't already exist
// These checks require a read lock!
var existingProjectFile = mySolution.FindProjectItemsByLocation(directoryEntry.GetAbsolutePath())
.FirstOrDefault() as IProjectFile;
var existingPsiSourceFile = existingProjectFile != null ? existingProjectFile.ToSourceFile() : null;
if (existingPsiSourceFile != null)
{
myLogger.Warn("Found existing project file for {0} with existing PSI source file (IsMiscProjectItem: {1})",
directoryEntry.GetAbsolutePath(), existingProjectFile.IsMiscProjectItem());
}
else
{
IndexableFiles.Add(new ExternalFile(directoryEntry, info.ProjectFileType, isUserEditable));
}
}
}
public void AddDirectory(VirtualFileSystemPath directory, bool isUserEditable)
{
Directories.Add((directory, isUserEditable));
}
public void Dump()
{
if (!myLogger.IsTraceEnabled()) return;
// 性能优化:现在只收集代码和shader文件
var total = IndexableFiles.Count;
myLogger.Trace("Collected {0} code/shader files (performance optimized)", total);
myLogger.Trace("Code/shader files: {0} ({1:n0} bytes)", IndexableFiles.Count, GetTotalFileSize(IndexableFiles));
myLogger.Trace("Directories: {0}", Directories.Count);
myLogger.Trace("Skipped all asset, meta, and other non-code files for performance");
}
private static ulong GetTotalFileSize(IEnumerable<ExternalFile> files)
{
return files.Aggregate(0UL, (s, f) => s + (ulong) f.FileSystemData.FileLength);
}
}
}
}
Rider For Unity插件反编译
不知道从哪个版本起,Unity插件被内置到Rider了,可以从Rider主界面-帮助-特殊文件和文件夹,看到所有路径,而Rider For Unity位于C:\Users\UserName\AppData\Local\Programs\Rider\plugins\rider-unity
根据命名空间原则,我们直接找到JetBrains.ReSharper.Plugins.Unity.dll进行修改,用dnspy打开,并添加基础dll:全都位于C:\Users\UserName\AppData\Local\Programs\Rider\lib\ReSharperHost中,直接拖到dnspy窗口即可
在dnspy定位到JetBrains.ReSharper.Plugins.Unity.Core.Psi.Modules.UnityExternalFilesModuleProcessor文件
修改代码,并编译保存,如果有报错直接注释整个函数内容,只保留空函数体即可,反正我们不需要这些乱七八糟的功能
重新打开Rider,原本几十分钟的初始化时间,现在几分钟直接搞定!
拓展
当然,这只是核心问题被解决,实际上可以继续优化,比如让Cursor帮我们将文件扫描的入口整个关掉,但是不确定会不会导致其他问题,大家可以自行尝试
文章作者: 烟雨迷离半世殇
文章链接: https://www.lfzxb.top/resharper-unity-fix/
————————————————
最后我们放松一下眼睛