Самый быстрый способ переместить папку в другое место в UWP

У меня есть SSD-хранилище, и этот код занимает 32 секунды для перемещения ~200 файлов и ~40 папок в одно и то же хранилище в режиме отладки или выпуска. общий размер папки ~30 МБ.

Как я могу сделать это быстрее?

// moves content from local folder to target folder.
async Task MoveContent(IStorageFolder source, IStorageFolder destination)
{
    foreach(var item in await source.GetItemsAsync())
    {
        switch (item)
        {
            case IStorageFile sourceFile:
                await sourceFile.MoveAsync(destination, sourceFile.Name, NameCollisionOption.ReplaceExisting);
                break;
            case IStorageFolder sourceSubFolder:
                var destinationSubFolder = await destination.CreateFolderAsync(sourceSubFolder.Name, CreationCollisionOption.ReplaceExisting);
                await MoveContent(sourceSubFolder, destinationSubFolder);
                break;
        }
    }
}

И я называю это так

await MoveContent(extractionFolder, targetFolder);

Обратите внимание, что extractionFolder в ApplicationData.Current.LocalCacheFolder а также targetFolder любая папка выбрана пользователем через FolderPicker

1 ответ

Есть несколько проблем с кодом, который вы разместили:

  1. Вы запускаете операции файлового ввода-вывода одну за другой и ждете их завершения. Поскольку файловый ввод / вывод в UWP осуществляется через посредников, это включает вызов в другой процесс. Так как большая часть времени тратится на обмен данными между процессами, вы становитесь узким местом ваших собственных ожиданий. Ваш диск вообще не активен в это время.

  2. WinRT File I/O API очень эффективен с точки зрения производительности. Вы хотите избежать этого как можно больше. Поскольку у вас есть правильный доступ к исходному пути, вы должны использовать класс C# DirectoryInfo для перечисления файлов. Затем вместо MoveAsync (поскольку у вас больше нет источника в качестве IStorageItem), используйте C / File File I/O.

С этими изменениями ему удается завершить мой синтетический тестовый пример (40 папок, в каждом по 5 файлов), что занимает 300 мс, по сравнению с 12 секундами, использующими ваш код. Это в 30 раз быстрее. Это могло бы стать намного быстрее, если бы нам было разрешено использовать Win32 API, такие как MoveFile, но, к сожалению, в настоящее время нет способа сделать это для папок и файлов, выбранных средствами выбора файлов / папок.

Вот код

        async Task MoveContentFast(IStorageFolder source, IStorageFolder destination)
        {
            await Task.Run(() =>
            {
                MoveContextImpl(new DirectoryInfo(source.Path), destination);
            });
        }

        private void MoveContextImpl(DirectoryInfo sourceFolderInfo, IStorageFolder destination)
        {
            var tasks = new List<Task>();

            var destinationAccess = destination as IStorageFolderHandleAccess;

            foreach (var item in sourceFolderInfo.EnumerateFileSystemInfos())
            {
                if ((item.Attributes & System.IO.FileAttributes.Directory) != 0)
                {
                    tasks.Add(destination.CreateFolderAsync(item.Name, CreationCollisionOption.ReplaceExisting).AsTask().ContinueWith((destinationSubFolder) =>
                    {
                        MoveContextImpl((DirectoryInfo)item, destinationSubFolder.Result);
                    }));
                }
                else
                {
                    if (destinationAccess == null)
                    {
                        // Slower, pre 14393 OS build path
                        tasks.Add(WindowsRuntimeStorageExtensions.OpenStreamForWriteAsync(destination, item.Name, CreationCollisionOption.ReplaceExisting).ContinueWith((openTask) =>
                        {
                            using (var stream = openTask.Result)
                            {
                                var sourceBytes = File.ReadAllBytes(item.FullName);
                                stream.Write(sourceBytes, 0, sourceBytes.Length);
                            }

                            File.Delete(item.FullName);
                        }));
                    }
                    else
                    {
                        int hr = destinationAccess.Create(item.Name, HANDLE_CREATION_OPTIONS.CREATE_ALWAYS, HANDLE_ACCESS_OPTIONS.WRITE, HANDLE_SHARING_OPTIONS.SHARE_NONE, HANDLE_OPTIONS.NONE, IntPtr.Zero, out SafeFileHandle file);
                        if (hr < 0)
                            Marshal.ThrowExceptionForHR(hr);

                        using (file)
                        {
                            using (var stream = new FileStream(file, FileAccess.Write))
                            {
                                var sourceBytes = File.ReadAllBytes(item.FullName);
                                stream.Write(sourceBytes, 0, sourceBytes.Length);
                            }
                        }

                        File.Delete(item.FullName);
                    }
                }
            }

            Task.WaitAll(tasks.ToArray());
        }

        [ComImport]
        [Guid("DF19938F-5462-48A0-BE65-D2A3271A08D6")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        internal interface IStorageFolderHandleAccess
        {
            [PreserveSig]
            int Create(
                [MarshalAs(UnmanagedType.LPWStr)] string fileName,
                HANDLE_CREATION_OPTIONS creationOptions,
                HANDLE_ACCESS_OPTIONS accessOptions,
                HANDLE_SHARING_OPTIONS sharingOptions,
                HANDLE_OPTIONS options,
                IntPtr oplockBreakingHandler,
                out SafeFileHandle interopHandle); // using Microsoft.Win32.SafeHandles
        }

        internal enum HANDLE_CREATION_OPTIONS : uint
        {
            CREATE_NEW = 0x1,
            CREATE_ALWAYS = 0x2,
            OPEN_EXISTING = 0x3,
            OPEN_ALWAYS = 0x4,
            TRUNCATE_EXISTING = 0x5,
        }

        [Flags]
        internal enum HANDLE_ACCESS_OPTIONS : uint
        {
            NONE = 0,
            READ_ATTRIBUTES = 0x80,
            // 0x120089
            READ = SYNCHRONIZE | READ_CONTROL | READ_ATTRIBUTES | FILE_READ_EA | FILE_READ_DATA,
            // 0x120116
            WRITE = SYNCHRONIZE | READ_CONTROL | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | FILE_WRITE_DATA,
            DELETE = 0x10000,

            READ_CONTROL = 0x00020000,
            SYNCHRONIZE = 0x00100000,
            FILE_READ_DATA = 0x00000001,
            FILE_WRITE_DATA = 0x00000002,
            FILE_APPEND_DATA = 0x00000004,
            FILE_READ_EA = 0x00000008,
            FILE_WRITE_EA = 0x00000010,
            FILE_EXECUTE = 0x00000020,
            FILE_WRITE_ATTRIBUTES = 0x00000100,
        }

        [Flags]
        internal enum HANDLE_SHARING_OPTIONS : uint
        {
            SHARE_NONE = 0,
            SHARE_READ = 0x1,
            SHARE_WRITE = 0x2,
            SHARE_DELETE = 0x4
        }

        [Flags]
        internal enum HANDLE_OPTIONS : uint
        {
            NONE = 0,
            OPEN_REQUIRING_OPLOCK = 0x40000,
            DELETE_ON_CLOSE = 0x4000000,
            SEQUENTIAL_SCAN = 0x8000000,
            RANDOM_ACCESS = 0x10000000,
            NO_BUFFERING = 0x20000000,
            OVERLAPPED = 0x40000000,
            WRITE_THROUGH = 0x80000000
        }

Чтобы повысить производительность вашего кода, вы можете попробовать перечислить все файлы в папке и подпапках сразу, а не по всей структуре папок (папка за папкой):

var results = storageFolder.CreateFileQueryWithOptions(
                  new QueryOptions() { FolderDepth = FolderDepth.Deep } );
var files = (await results.GetFilesAsync()).ToArray();

куда storageFolder это папка, которую вы хотите переместить. Пользовательский файл запроса имеет FolderDepth настройка установлена ​​на Deep так что он возвращает все файлы из всей структуры папок. После запуска этого, files массив будет содержать все файлы, и вы можете затем переместить их. Это будет по крайней мере немного быстрее, чем перечислять все папки одну за другой. Вы просто должны всегда проверять, созданы ли соответствующие подпапки в целевом местоположении.

Наконец, вы можете попытаться распараллелить движение Tasks - например, перемещение трех файлов одновременно. Вы можете создать несколько Task случаи и await все они используют Task.WhenAll,

Решение для копирования и вставки

Другим быстрым и грязным решением было бы использовать StorageFolder.CopyAsync() способ скопировать папку в новое место и удалить оригинал (это даже предлагается в Документах):

В настоящее время нет метода MoveAsync или аналогичного метода. Одна из простых реализаций перемещения папки может состоять в том, чтобы получить нужную папку, скопировать ее в нужное место и затем удалить исходную папку.

Однако стоимость дополнительного дискового пространства не очень привлекательна и может даже не повысить производительность, поскольку копирование обходится дороже, чем перемещение.

UWP в настоящее время не имеет ничего подобного MoveAsync по состоянию на август 2019 г. Этот ответ обеспечивает поведение, аналогичное функции MoveAsync, и предполагает, что вы работаете за пределами изолированной программной среды / локального состояния приложения UWP, поскольку в изолированной программной среде вы можете использовать классическую среду намного быстрее System.IO методы из.NET. Просто используйте последний в своей песочнице, иначе вы можете использовать это специальное:

public static async Task Move_Directory_Async(
    StorageFolder           sourceDir,
    StorageFolder           destParentDir,
    CreationCollisionOption repDirOpt,
    NameCollisionOption     repFilesOpt)
{
    try
    {
        if (sourceDir == null)
            return;

        List<Task> copies = new List<Task>();
        var files = await sourceDir.GetFilesAsync();
        if (files == null || files.Count == 0)
            await destParentDir.CreateFolderAsync(sourceDir.Name);
        else
        {
            await destParentDir.CreateFolderAsync(sourceDir.Name, repDirOpt);
            foreach (var file in files)
                copies.Add(file.CopyAsync(destParentDir, file.Name, repFilesOpt).AsTask());
        }

        await sourceDir.DeleteAsync(StorageDeleteOption.PermanentDelete);
        await Task.WhenAll(copies);
    }
    catch(Exception ex)
    {
        //Handle any needed cleanup tasks here
        throw new Exception(
          $"A fatal exception triggered within Move_Directory_Async:\r\n{ex.Message}", ex);
    }
}
Другие вопросы по тегам