代码之家  ›  专栏  ›  技术社区  ›  Kraang Prime

正在枚举100000+个文件夹层次结构,耗时数小时

  •  0
  • Kraang Prime  · 技术社区  · 9 年前

    为什么此代码需要数小时才能完成:

    public void SetRoot(string path)
    {
        Tag = path;
        BeginUpdate();
        AddFolderRecursive(Nodes, path);
        EndUpdate();
    }
    
    private void AddFolderRecursive(TreeNodeCollection nodes, string path)
    {
        try
        {
            var dirs = Directory.EnumerateDirectories(path).OrderBy(d => d).Select(d => d.Split(Path.DirectorySeparatorChar).Last());
            TreeNode node;
            ShellFileGetInfo.FolderIcons fi;
            foreach (var d in dirs)
            {
                node = nodes.Add(Path.Combine(path, d), d, ImageList.Images.Count);
                node.Tag = Path.Combine(path, d);
                node.SelectedImageIndex = ImageList.Images.Count + 1;
                fi = ShellFileGetInfo.GetFolderIcon((string)node.Tag, false);
    //                  ImageList.Images.Add(fi.closed);
    //                  ImageList.Images.Add(fi.open);
                AddFolderRecursive(node.Nodes, (string)node.Tag);
            }
        }
        catch (UnauthorizedAccessException)
        {
    
        }
    }
    

    我已经让这段代码运行了14个小时了,它仍然没有完成在传递时获取所有文件夹的列表 SetRoot( @"c:\" ); 代码正在运行,它正在添加到树中,但这太荒谬了。

    124,157 folders

    本质上,我想用驱动器上的所有文件夹来流行树视图(这样树视图就可以搜索),并使用实际的文件夹图标(我正在使用 SHGetFileInfo p/invoke和必要的参数)。即使没有得到图标(这是我遇到的另一个问题,即在每个文件夹的基础上获取文件夹图标是唯一的,即使图标图像本身可能是相同的。我无法快速确定是否已将该图像保存在TreeView中 ImageList -也就是说,“c:\windows”的文件夹图标与“c:\windows\system32”相同,但句柄等都返回不同的信息,因此似乎没有任何东西可以对其进行唯一索引。)。

    在我的代码中可以调整哪些内容,使这个过程更快,同时仍保留系统中的文件夹图标?请记住,我还希望所有文件夹都不跳过空文件夹

    (我甚至无法显示TreeView的图片,因为我在14小时后停止了循环,直到它完成显示)。

    为了测试TreeView控件的速度,我编写了以下代码:

    DateTime start = DateTime.UtcNow;
    treeView1.BeginUpdate();
    await Task.Run(() =>
    {
        int x = 0;
        while (x < 100000)
        {
            x++;
            if (treeView1.InvokeRequired)
            {
                treeView1.Invoke((MethodInvoker)delegate { treeView1.Nodes.Add("Node - " + x); } );
            }
        }
    });
    treeView1.EndUpdate();
    Text = start.ToLongTimeString() + " - " + DateTime.UtcNow.ToLongTimeString();
    

    下面是结果的屏幕截图:

    100,000 items in TreeView

    如您所见,使用100000个项目填充TreeView 非常 如果您使用 BeginUpdate EndUpdate

    3 回复  |  直到 6 年前
        1
  •  2
  •   David Heffernan    9 年前

    Windows树状视图控件根本不适合容纳这么多节点。您根本不可能在实际时间内用数千个节点填充此控件。此外,在短时间内列举所有这些项目肯定是不现实的。更尴尬的是,试图提前提取树中每个对象的图标。

    前进的方向是不要尝试用所有项填充控件。只填充父节点。然后,当它们打开时,枚举并添加子级。这就是所有shell程序的操作方式。

        2
  •  1
  •   FloatingKiwi    9 年前

    经过进一步的研究,我发现问题是用于枚举文件和文件夹的方法非常慢,当文件夹无法访问时 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以廉价的方式获取自定义文件夹图标

    使现代化

        3
  •  -1
  •   Dr.Haimovitz    9 年前

    我敢打赌,代码不是原因,它可能是两件事之一:

    1. 你的硬盘一团糟,因此你可以尝试碎片化方法。

    2. (对我来说也是这样)你的文件夹没有被windows索引(索引- https://en.m.wikipedia.org/wiki/Indexing_Service )要修复它,您需要转到正在处理的主文件夹,并要求windows为该文件夹及其所有子文件夹编制索引(在文件夹框中的某个位置),此过程将花费大约一天的时间,但在此之后,您的程序应能正常(快速)运行。