经过进一步的研究,我发现问题是用于枚举文件和文件夹的方法非常慢,当文件夹无法访问时
UnauthorizedAccessException
抛出,每个事件的固有延迟约为200ms。这些异常会叠加并导致很大的延迟。
此外,Davids关于向TreeView添加项时的二次指数会导致进一步延迟的说法也是正确的,但在这种情况下,额外延迟与TreeView节点添加的缩放比例不成比例。
为了解决这个问题,我把它缩小到了3个问题,其中两个问题我已经完全解决了,因此控制功能的这些部分在合理的时间范围内。分解一下,以下是导致OP问题延迟的3个问题:
-
TreeView节点添加速度随着树的加深而呈指数级降低,添加的节点也越多。
-
文件系统访问不访问NTFS可用的本机日志系统,因此每个文件或目录都是每个调用单独来源的。此外,如果文件夹标记为受限,则
未授权访问异常
每次遭遇造成大约200ms的人工延迟。
-
检索自定义文件夹图标需要多个IO操作(具有各自的延迟),此外,上述存储每个图标对的方法效率低下,导致其自身的额外延迟,即使在这个范围内很小,延迟也是相关的。
在缩小延迟的范围之后,我能够针对这些因素逐个减少它们。
加快获取文件夹列表的速度
我必须做的第一件事是,用更可行的方法替换文件系统访问方法——直接访问NTFS Journal系统,我可以从中获取一些代码
StCroixSkipper's USN Journal Explorer v1.3
和
MFT Scanner in VB.NET
制作以下类
NtfsUsnJournal.cs
这超出了我在StackOverflow上发布的内容,所以我把它放在了粘贴箱中。
此更改允许我在4秒内递归检索C驱动器上的所有文件夹
注意:到目前为止,我还无法找到一种不需要应用程序的提升(管理员)权限就可以访问Journal的方法。所有在没有提升的情况下访问日志的尝试都会导致拒绝访问异常。
提高大型嵌套节点集的TreeView性能
接下来,我需要改进TreeView的性能,以便能够在加载当前文件夹结构时添加100000多个嵌套节点。为了做到这一点,我们做了一点Googlefu,并做了一些修改来调整代码,使其适用于上述类的Usn格式。
结果是在扩展TreeView的客户用户控件中添加了以下内容:
#region TreeViewFast
private readonly Dictionary<ulong, TreeNode> _treeNodes = new Dictionary<ulong, TreeNode>();
/// <summary>
/// Load the TreeView with items.
/// </summary>
/// <typeparam name="T">Item type</typeparam>
/// <param name="items">Collection of items</param>
/// <param name="getId">Function to parse Id value from item object</param>
/// <param name="getParentId">Function to parse parentId value from item object</param>
/// <param name="getDisplayName">Function to parse display name value from item object. This is used as node text.</param>
public void LoadItems<T>(IEnumerable<T> items, Func<T, ulong> getId, Func<T, ulong?> getParentId, Func<T, string> getDisplayName)
{
// Clear view and internal dictionary
Nodes.Clear();
_treeNodes.Clear();
// Load internal dictionary with nodes
foreach (var item in items)
{
var id = getId(item);
var displayName = getDisplayName(item);
var node = new TreeNode { Name = id.ToString(), Text = displayName, Tag = item };
_treeNodes.Add(getId(item), node);
}
// Create hierarchy and load into view
foreach (var id in _treeNodes.Keys)
{
var node = GetNode(id);
var obj = (T)node.Tag;
var parentId = getParentId(obj);
if (parentId.HasValue)
{
var parentNode = GetNode(parentId.Value);
if(parentNode == null)
{
Nodes.Add(node);
} else
{
parentNode.Nodes.Add(node);
}
}
else
{
Nodes.Add(node);
}
}
}
/// <summary>
/// Get a handle to the object collection.
/// This is convenient if you want to search the object collection.
/// </summary>
public IQueryable<T> GetItems<T>()
{
return _treeNodes.Values.Select(x => (T)x.Tag).AsQueryable();
}
/// <summary>
/// Retrieve TreeNode by Id.
/// Useful when you want to select a specific node.
/// </summary>
/// <param name="id">Item id</param>
public TreeNode GetNode(ulong id)
{
try
{
return _treeNodes[id];
} catch (KeyNotFoundException)
{
return null;
}
}
/// <summary>
/// Retrieve item object by Id.
/// Useful when you want to get hold of object for reading or further manipulating.
/// </summary>
/// <typeparam name="T">Item type</typeparam>
/// <param name="id">Item id</param>
/// <returns>Item object</returns>
public T GetItem<T>(ulong id)
{
return (T)GetNode(id).Tag;
}
/// <summary>
/// Get parent item.
/// Will return NULL if item is at top level.
/// </summary>
/// <typeparam name="T">Item type</typeparam>
/// <param name="id">Item id</param>
/// <returns>Item object</returns>
public T GetParent<T>(ulong id) where T : class
{
var parentNode = GetNode(id).Parent;
return parentNode == null ? null : (T)Parent.Tag;
}
/// <summary>
/// Retrieve descendants to specified item.
/// </summary>
/// <typeparam name="T">Item type</typeparam>
/// <param name="id">Item id</param>
/// <param name="deepLimit">Number of generations to traverse down. 1 means only direct children. Null means no limit.</param>
/// <returns>List of item objects</returns>
public List<T> GetDescendants<T>(ulong id, int? deepLimit = null)
{
var node = GetNode(id);
var enumerator = node.Nodes.GetEnumerator();
var items = new List<T>();
if (deepLimit.HasValue && deepLimit.Value <= 0)
return items;
while (enumerator.MoveNext())
{
// Add child
var childNode = (TreeNode)enumerator.Current;
var childItem = (T)childNode.Tag;
items.Add(childItem);
// If requested add grandchildren recursively
var childDeepLimit = deepLimit.HasValue ? deepLimit.Value - 1 : (int?)null;
if (!deepLimit.HasValue || childDeepLimit > 0)
{
var childId = ulong.Parse(childNode.Name);
var descendants = GetDescendants<T>(childId, childDeepLimit);
items.AddRange(descendants);
}
}
return items;
}
#endregion
为了使用,我创建了一个新方法,它充当一个简单的“加载程序”,如下所示:
public void PopulateTree(string path)
{
Tag = path;
using (NtfsUsnJournal ntfs = new NtfsUsnJournal(new DriveInfo(path)))
{
List<NtfsUsnJournal.UsnEntry> folders;
ntfs.GetNtfsVolumeFolders(out folders);
Func<NtfsUsnJournal.UsnEntry, ulong> getId = (x => x.FileReferenceNumber);
Func<NtfsUsnJournal.UsnEntry, ulong?> getParentId = (x => x.ParentFileReferenceNumber);
Func<NtfsUsnJournal.UsnEntry, string> getDisplayName = (x => x.Name);
LoadItems(folders, getId, getParentId, getDisplayName);
}
}
测试这一点,现在只需6秒钟即可将所有100000多个文件夹完全加载到TreeView中,用户体验正在迅速扩展
带有自定义图标的文件夹
这是我目前最不想去的地方,我仍在寻找一种方法来完全改善这一点。
到目前为止,我所做的就是检查一下
desktop.ini
存在于文件夹中,如果存在,
然后
呼叫
SHGetFileInfo
pinvoke以获取自定义文件夹图标。然后,我将要展开的文件夹添加到一个内部列表中,表示我已经检查了该文件夹并获得了任何相关图标,这些都发生在
OnBeforeExpand
事件虽然这些调用成本较低,但它仍然会给进程增加很大的延迟(扩展c:\windows需要12秒)。
这是它的代码(也在自定义TreeView中)
private List<string> _expandedCache;
protected override void OnBeforeExpand(TreeViewCancelEventArgs e)
{
if (!_expandedCache.Contains(e.Node.FullPath))
{
BeginUpdate();
ShellFileGetInfo.FolderIcons fi;
_expandedCache.Add(e.Node.FullPath);
string curPath;
foreach(TreeNode n in e.Node.Nodes)
{
curPath = Path.Combine((string)Tag, n.FullPath.Replace('/', Path.DirectorySeparatorChar));
if (File.Exists(Path.Combine(curPath, "desktop.ini")) == true)
{
fi = ShellFileGetInfo.GetFolderIcon(curPath, false);
if(fi.closed != null || fi.open != null)
{
ImageList.Images.Add(fi.closed);
ImageList.Images.Add(fi.open);
n.SelectedImageIndex = ImageList.Images.Count - 1;
n.ImageIndex = ImageList.Images.Count - 2;
}
}
}
EndUpdate();
}
base.OnBeforeExpand(e);
}
这是最后一次重大挫折,我相信有一种方法可以比传统方法更快地实现
File.Exists()
方法和
pinvoke以廉价的方式获取自定义文件夹图标
使现代化