编写文件的最佳方法

重要的应用程序接口(API)

开发人员在使用 FileIOWrite 方法和 PathIO 类来执行文件系统 I/O 操作时,开发人员有时会遇到一组常见问题。 例如,常见问题包括:

  • 文件被部分写入。
  • 调用其中一种方法时,应用会收到异常。
  • 操作会留下 .TMP 文件,其文件名与目标文件名相似。

FileIOPathIO 类的 写入 方法包括:

  • WriteBufferAsync
  • WriteBytesAsync
  • WriteLinesAsync
  • WriteTextAsync

本文详细介绍了这些方法的工作原理,以便开发人员更好地了解何时以及如何使用它们。 本文提供了指南,并且不会尝试为所有可能的文件 I/O 问题提供解决方案。

注释

 本文重点介绍示例和讨论中的 FileIO 方法。 但是,PathIO 方法遵循类似的模式,本文中的大多数指南也适用于这些方法。

便利与控制

StorageFile 对象不同于本机 Win32 编程模型中的文件句柄。 相反,StorageFile 是文件的表示形式,其中包含操作其内容的方法。

使用 StorageFile执行 I/O 时,了解此概念非常有用。 例如,写入文件 章节提供了三种将数据写入文件的方法:

前两种方案是应用最常用的方案。 在单个操作中写入文件更容易编码和维护,同时也减轻了应用程序处理许多文件 I/O 复杂性问题的负担。 然而,这种便利性是以失去对整个操作的控制能力和无法在具体环节捕捉错误为代价的。

事务模型

FileIOPathIO 类的 Write 方法包装上述第三个写入模型上的步骤,并添加了层。 此层封装在存储事务中。

为了在写入数据时保护原始文件的完整性,写入 方法使用事务模型,方法是使用 OpenTransactedWriteAsync打开文件。 此过程创建 StorageStreamTransaction 对象。 创建此事务对象后,API 会以类似于 文件访问 示例或 StorageStreamTransaction 一文中的代码示例编写数据。

下图说明了成功写入操作中由 WriteTextAsync 方法执行的基础任务。 此图提供了操作的简化视图。 例如,它会跳过不同线程上的文本编码和异步完成等步骤。

UWP API 调用序列图,表示写入文件的过程

使用 FileIOPathIO 类的 Write 方法,而不是使用流进行更复杂的四步模型的优点包括:

  • 一个 API 调用,用于处理所有中间步骤,包括错误。
  • 如果出现问题,则保留原始文件。
  • 系统状态将尽量保持干净。

但是,由于有这么多可能的中间点故障,失败机会增加。 发生错误时,可能很难理解进程失败的位置。 以下部分介绍了在使用 写入 方法并提供可能的解决方案时可能会遇到的一些故障。

FileIO 和 PathIO 类的写入方法的常见错误代码

此表提供了应用开发人员在使用 写入 方法时遇到的常见错误代码。 表中的步骤对应于上图中的步骤。

错误名称(值) 步骤 原因 解决方案
ERROR_ACCESS_DENIED(0X80070005) 5 原始文件可能标记为要删除,可能来自以前的操作。 重试操作。
确保同步对文件的访问权限。
ERROR_SHARING_VIOLATION (0x80070020) 5 原始文件由另一个独占写入打开。 重试操作。
确保同步对文件的访问权限。
无法删除已替换错误 (错误代码:0x80070497) 19-20 无法替换原始文件(file.txt),因为它正在使用中。 另一个进程或操作在可以替换文件之前获得了对文件的访问权限。 重试操作。
确保同步对文件的访问权限。
ERROR_DISK_FULL(0x80070070) 7, 14, 16, 20 事务处理模型会创建一个额外的文件,这会占用额外的存储。
ERROR_OUTOFMEMORY(0x8007000E) 14, 16 这可能是由于多个未完成的 I/O 操作或大型文件大小导致的。 通过管理数据流的更细化方法可能解决错误。
E_FAIL(0x80004005) 任意 其他 重试操作。 如果它仍然失败,则可能是平台错误,应用应终止,因为它处于不一致状态。

可能导致错误的文件状态的其他注意事项

除了 写入 方法返回的错误外,下面是有关写入文件时应用可以期望的一些准则。

仅当操作完成时才将数据写入文件

当写入操作正在进行时,你的应用不应对文件中的数据做出任何假设。 在操作完成之前尝试访问文件可能会导致数据不一致。 你的应用应负责跟踪待处理的 I/O 操作。

读者

如果写入到的文件也由礼貌的读取器使用(也就是说,使用 FileAccessMode.Read打开),后续读取将失败,并出现错误ERROR_OPLOCK_HANDLE_CLOSED(0x80070323)。 有时,当 写入 操作正在进行时,应用会重试打开文件进行读取。 这可能会导致 写入 在尝试覆盖原始文件时因无法替换而最终失败的竞争条件。

KnownFolders 中的文件

你的应用可能不是唯一一个尝试访问位于任何 KnownFolders上的文件的应用。 不能保证如果操作成功,在下次尝试读取文件时,写入文件的应用的内容将保持不变。 此外,在这种情况下,共享或访问被拒绝的错误变得更加常见。

I/O 冲突

如果我们的应用使用 写入 方法处理本地数据中的文件,则并发错误的可能性会降低,但仍需保持谨慎。 如果同时向文件发送多个 写入 操作,则无法保证文件中最终保存的数据内容。 为了缓解此问题,我们建议您的应用将 写操作 进行序列化写入到文件。

~TMP 文件

有时,如果操作被强行取消(例如,如果应用程序被操作系统暂停或终止),则事务无法提交或正确关闭。 这会留下扩展名为 (.~TMP) 的文件。 处理应用激活时,请考虑删除这些临时文件(如果它们存在于应用的本地数据中)。

基于文件类型的注意事项

某些错误可能会变得更加普遍,具体取决于文件类型、访问频率及其文件大小。 通常,应用可以访问三类文件:

  • 用户在应用的本地数据文件夹中创建和编辑的文件。 这些内容仅在使用应用时创建和编辑,并且它们仅存在于应用中。
  • 应用元数据。 你的应用使用这些文件来跟踪其自己的状态。
  • 应用声明具有访问权限的文件系统位置中的其他文件。 这些通常位于已知文件夹 之一。

你的应用完全控制前两类文件,因为它们是应用的包文件的一部分,并且完全由你的应用访问。 对于最后一个类别中的文件,你的应用必须注意其他应用和 OS 服务可能同时访问这些文件。

根据应用的不同,对文件的访问权限可能会因频率而异:

  • 非常低。 这些通常是在应用启动时打开一次的文件,并在应用挂起时保存。
  • 低。 这些是用户专门操作的文件(例如保存和加载)。
  • 中等或高。 这些文件是应用必须不断更新数据的文件(例如自动保存功能或常量元数据跟踪)。

对于文件大小,请考虑以下图表中 WriteBytesAsync 方法的性能数据。 此图表比较了完成操作所需时间与文件大小,平均每种文件大小进行了 10000 次操作,且在受控环境中进行测试。

WriteBytesAsync 性能

从此图表中有意省略 y 轴上的时间值,因为不同的硬件和配置将产生不同的绝对时间值。 但是,我们在测试中一直观察到这些趋势:

  • 对于非常小的文件(<= 1 MB):完成操作所需的时间一致。
  • 对于较大的文件(> 1 MB):完成操作的时间开始呈指数级增加。

应用程序挂起期间的 I/O

如果你希望保留状态信息或元数据,以便在以后的会话中使用,你的应用必须设计为处理挂起。 有关应用挂起的背景信息,请参阅 应用生命周期此博客文章

除非 OS 向应用授予扩展执行,否则当应用暂停时,它有 5 秒的时间释放其所有资源并保存其数据。 为了获得最佳的可靠性和用户体验,请始终假设您处理挂起任务的时间是有限的。 请记住在5秒时间段内处理暂停任务时应遵循以下准则:

  • 尽量减少 I/O 操作,以避免由刷新和释放操作引起的竞争条件。
  • 避免写入需要几百毫秒或更长时间的文件。
  • 如果你的应用使用 写入 方法,请记住这些方法所需的所有中间步骤。

如果您的应用在挂起期间对少量状态数据进行操作,在大多数情况下,可以使用 写入 方法来刷新数据。 但是,如果应用使用大量的状态数据,请考虑使用流直接存储数据。 这有助于减少 写入 方法的事务模型引入的延迟。

有关示例,请参阅 BasicSuspension 样例。

其他示例和资源

下面是特定方案的几个示例和其他资源。

重试文件 I/O 示例的代码示例

下面是有关如何重试写入(C#)的伪代码示例,假设在用户选取保存文件后要完成写入:

Windows.Storage.Pickers.FileSavePicker savePicker = new Windows.Storage.Pickers.FileSavePicker();
savePicker.FileTypeChoices.Add("Plain Text", new List<string>() { ".txt" });
Windows.Storage.StorageFile file = await savePicker.PickSaveFileAsync();

Int32 retryAttempts = 5;

const Int32 ERROR_ACCESS_DENIED = unchecked((Int32)0x80070005);
const Int32 ERROR_SHARING_VIOLATION = unchecked((Int32)0x80070020);

if (file != null)
{
    // Application now has read/write access to the picked file.
    while (retryAttempts > 0)
    {
        try
        {
            retryAttempts--;
            await Windows.Storage.FileIO.WriteTextAsync(file, "Text to write to file");
            break;
        }
        catch (Exception ex) when ((ex.HResult == ERROR_ACCESS_DENIED) ||
                                   (ex.HResult == ERROR_SHARING_VIOLATION))
        {
            // This might be recovered by retrying, otherwise let the exception be raised.
            // The app can decide to wait before retrying.
        }
    }
}
else
{
    // The operation was cancelled in the picker dialog.
}

同步对文件的访问

并行编程与 .NET 博客 是有关并行编程的指南的绝佳资源。 具体而言,关于 AsyncReaderWriterLock 的 文章介绍了如何在维持对文件写入的独占访问的同时允许并发的读取访问。 请记住,序列化 I/O 会影响性能。

另请参阅