Xamarin.iOS 中的文件系统访问

Download Sample下载示例

可以使用 Xamarin.iOS 和 System.IO .NET 基类库 (BCL) 中的类访问 iOS 文件系统。 File 类可以创建、删除和读取文件,Directory 类可以创建、删除或枚举目录的内容。 还可以使用 Stream 子类,这些子类可以更好地控制文件操作(如文件中的压缩或位置搜索)。

iOS 对应用程序对文件系统可以执行的操作施加一些限制,以保护应用程序数据的安全性,并保护用户免受恶性应用的影响。 这些限制是应用程序沙盒一部分 - 一组规则,用于限制应用程序对文件、首选项、网络资源、硬件等的访问。应用程序仅限于在其主目录中读取和写入文件(已安装的位置):它无法访问其他应用程序的文件。

iOS 还具有一些特定于文件系统的功能:某些目录需要对备份和升级进行特殊处理,应用程序还可以彼此共享文件以及 文件 应用(自 iOS 11 起),以及通过 iTunes 共享文件。

本文讨论 iOS 文件系统的功能和限制,并包括一个示例应用程序,演示如何使用 Xamarin.iOS 执行一些简单的文件系统操作:

A sample of iOS executing some simple file system operations

常规文件访问

使用 Xamarin.iOS,可以在 iOS 上使用 .NET System.IO 类执行文件系统操作。

以下代码片段演示了一些常见的文件操作。 在本文的示例应用程序中,你将在 SampleCode.cs 文件中找到它们。

使用目录

此代码枚举当前目录中的子目录(由“./”参数指定),这是应用程序可执行文件的位置。 输出将是随应用程序一起部署的所有文件和文件夹的列表(在调试时显示在控制台窗口中)。

var directories = Directory.EnumerateDirectories("./");
foreach (var directory in directories) {
      Console.WriteLine(directory);
}

读取文件

若要读取文本文件,只需一行代码。 本示例将在“应用程序输出”窗口中显示文本文件的内容。

var text = File.ReadAllText("TestData/ReadMe.txt");
Console.WriteLine(text);

XML 序列化

尽管使用完整的 System.Xml 命名空间超出了本文的范围,但可以使用类似于以下代码片段的 StreamReader 轻松反序列化文件系统中的 XML 文档:

using (TextReader reader = new StreamReader("./TestData/test.xml")) {
      XmlSerializer serializer = new XmlSerializer(typeof(MyObject));
      var xml = (MyObject)serializer.Deserialize(reader);
}

有关详细信息,请参阅 System.Xml序列化的文档。 请参阅链接器上的 Xamarin.iOS 文档 - 通常需要将 [Preserve] 属性添加到要序列化的类。

创建文件和目录

此示例演示如何使用 Environment 类访问“文档”文件夹,我们可以在其中创建文件和目录。

var documents =
 Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments); 
var filename = Path.Combine (documents, "Write.txt");
File.WriteAllText(filename, "Write this text into a file");

创建目录的过程类似:

var documents =
 Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments);
var directoryname = Path.Combine (documents, "NewDirectory");
Directory.CreateDirectory(directoryname);

有关详细信息, 请参阅 System.IO API 参考

序列化 JSON

Json.NET 是一个高性能 JSON 框架,适用于 Xamarin.iOS,可在 NuGet 上使用。 使用 Visual Studio for Mac 中的“添加 NuGet”将 NuGet 包添加到应用程序项目:

Adding the NuGet package to the applications project

接下来,添加一个类来充当序列化/反序列化的数据模型(在本例 Account.cs中):

using System;
using System.Collections.Generic;
using Foundation; // for Preserve attribute, which helps serialization with Linking enabled

namespace FileSystem
{
    [Preserve]
    public class Account
    {
        public string Email { get; set; }
        public bool Active { get; set; }
        public DateTime CreatedDate { get; set; }
        public List<string> Roles { get; set; }

        public Account() {
        }
    }
}

最后,创建类的 Account 实例,将其序列化为 json 数据并将其写入文件:

// Create a new record
var account = new Account(){
    Email = "monkey@xamarin.com",
    Active = true,
    CreatedDate = new DateTime(2015, 5, 27, 0, 0, 0, DateTimeKind.Utc),
    Roles = new List<string> {"User", "Admin"}
};

// Serialize object
var json = JsonConvert.SerializeObject(account, Newtonsoft.Json.Formatting.Indented);

// Save to file
var documents = Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments);
var filename = Path.Combine (documents, "account.json");
File.WriteAllText(filename, json);

有关在 .NET 应用程序中使用 json 数据的详细信息,请参阅 Json.NET 的文档

特殊注意事项

尽管 Xamarin.iOS 和 .NET 文件操作之间存在相似之处,但 iOS 和 Xamarin.iOS 在一些重要方面与 .NET 不同。

使项目文件在运行时可访问

默认情况下,如果将文件添加到项目,则它不会包含在最终程序集中,因此不会对应用程序可用。 若要在程序集中包含文件,必须使用名为 Content 的特殊生成操作对其进行标记。

若要标记要包含的文件,请右键单击该文件,然后选择 Visual Studio for Mac 中的“ 生成操作 > 内容 ”。 还可以更改文件属性表中的生成操作

事例敏感性

请务必了解 iOS 文件系统区分 大小写。 区分大小写意味着文件和目录名称必须完全匹配 - README.txtreadme.txt 将被视为不同的文件名。

对于更熟悉 Windows 文件系统的 .NET 开发人员来说,这可能令人困惑,后者不区分大小写 – 文件、文件和文件都将引用同一目录。

警告

iOS 模拟器不区分大小写。 如果文件名大小写在文件本身和代码中对它的引用不同,则代码可能仍可在模拟器中工作,但在实际设备上会失败。 这是在 iOS 开发期间尽早和经常在实际设备上部署和测试非常重要的原因之一。

路径分隔符

iOS 使用正斜杠“/”作为路径分隔符(这与 Windows 不同,后者使用反斜杠“\”)。

由于这种令人困惑的差异,因此最好使用 System.IO.Path.Combine 该方法,该方法根据当前平台进行调整,而不是对特定路径分隔符进行硬编码。 这是一个简单的步骤,使代码更易于移植到其他平台。

应用程序沙盒

出于安全原因,应用程序对文件系统(以及其他资源(如网络和硬件功能)的访问受到限制。 此限制称为 应用程序沙盒。 就文件系统而言,应用程序仅限于在其主目录中创建和删除文件和目录。

主目录是文件系统中唯一的位置,其中应用程序及其所有数据都存储在其中。 不能为应用程序选择(或更改)主目录的位置;但是,iOS 和 Xamarin.iOS 提供用于管理内部文件和目录的属性和方法。

应用程序捆绑包

应用程序 捆绑 包是包含应用程序的文件夹。 它通过将.app后缀添加到目录名称来区分其他文件夹。 应用程序捆绑包包含可执行文件以及项目所需的所有内容(文件、图像等)。

在 Mac OS 中浏览到应用程序捆绑包时,它会显示与其他目录中不同的图标(并且 隐藏了.app 后缀);但是,它只是操作系统以不同方式显示的常规目录。

若要查看示例代码的应用程序捆绑包,请右键单击 Visual Studio for Mac 中的项目,然后选择“在查找器中显示”。 然后导航到 bin/ 目录,可在其中找到应用程序图标(类似于下面的屏幕截图)。

Navigate through the bin directory to find an application icon similar to this screenshot

右键单击此图标,然后选择“显示包内容以浏览应用程序捆绑目录的内容。 内容类似于常规目录的内容,如下所示:

The contents of the app bundle

应用程序捆绑包是在模拟器或设备上测试期间安装的内容,最终是提交到 Apple 以包含在 App Store 中的内容。

应用程序目录

在设备上安装应用程序时,操作系统会为应用程序创建主目录,并在可供使用的应用程序根目录中创建多个目录。 由于 iOS 8,用户可访问的目录 不在 应用程序根目录中,因此无法从用户目录派生应用程序捆绑包的路径,反之亦然。

下面列出了这些目录、如何确定其路径及其用途:

 

Directory 说明
[ApplicationName].app/ 在 iOS 7 及更早版本中,这是 ApplicationBundle 存储应用程序可执行文件的目录。 在应用中创建的目录结构存在于此目录中(例如,图像和标记为 Visual Studio for Mac 项目中的资源的其他文件类型)。

如果需要访问应用程序捆绑包中的内容文件,可通过 NSBundle.MainBundle.BundlePath 该属性访问此目录的路径。
文件/ 使用此目录存储用户文档和应用程序数据文件。

此目录的内容可以通过 iTunes 文件共享提供给用户(尽管默认情况下已禁用)。 UIFileSharingEnabled将布尔密钥添加到 Info.plist 文件,以允许用户访问这些文件。

即使应用程序没有立即启用文件共享,也应避免将应隐藏的文件置于此目录中(例如数据库文件,除非打算共享这些文件)。 只要敏感文件保持隐藏状态,如果将来的版本启用了文件共享,则这些文件将不会公开(并可能由 iTunes 移动、修改或删除)。

可以使用该方法 Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments) 获取应用程序文档目录的路径。

此目录的内容由 iTunes 备份。
图书馆/ 库目录是存储用户未直接创建的文件(如数据库或其他应用程序生成的文件)的好位置。 此目录的内容永远不会通过 iTunes 向用户公开。

可以在库中创建自己的子目录;但是,此处已有一些系统创建的目录,应注意这些目录,包括首选项和缓存。

此目录(缓存子目录除外)的内容由 iTunes 备份。 将在库中创建的自定义目录进行备份。
库/首选项/ 特定于应用程序的首选项文件存储在此目录中。 请勿直接创建这些文件。 请改用类 NSUserDefaults

此目录的内容由 iTunes 备份。
库/缓存/ 缓存目录非常适合用于存储有助于应用程序运行的数据文件,但可以轻松重新创建。 应用程序应根据需要创建和删除这些文件,并在必要时重新创建这些文件。 iOS 5 也可能删除这些文件(在存储不足的情况下),但在应用程序运行时不会删除这些文件。

此目录的内容不是由 iTunes 备份的,这意味着如果用户还原设备,则它们不会存在,并且安装应用程序的更新版本后可能不存在这些内容。

例如,如果应用程序无法连接到网络,可以使用缓存目录来存储数据或文件,以提供良好的脱机体验。 应用程序可以在等待网络响应时快速保存和检索此数据,但无需备份,在还原或版本更新后可以轻松恢复或重新创建这些数据。
Tmp/ 应用程序可以存储此目录中短期内只需要的临时文件。 为了节省空间,在不再需要文件时,应删除文件。 当应用程序未运行时,操作系统也可能从此目录中删除文件。

此目录的内容不受 iTunes 备份。

例如,tmp 目录可用于存储下载以供向用户显示的临时文件(例如 Twitter 头像或电子邮件附件),但一旦查看它们(如果将来需要,可以删除这些文件)。

此屏幕截图显示了 Finder 窗口中的目录结构:

This screenshot shows the directory structure in a Finder window

以编程方式访问其他目录

前面的目录和文件示例访问了 Documents 该目录。 若要写入另一个目录,必须使用“..”语法构造路径,如下所示:

var documents = Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments);
var library = Path.Combine (documents, "..", "Library");
var filename = Path.Combine (library, "WriteToLibrary.txt");
File.WriteAllText(filename, "Write this text into a file in Library");

创建目录类似于:

var documents = Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments);
var library = Path.Combine (documents, "..", "Library");
var directoryname = Path.Combine (library, "NewLibraryDirectory");
Directory.CreateDirectory(directoryname);

Caches可以构造路径和tmp目录,如下所示:

var documents = Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments);
var cache = Path.Combine (documents, "..", "Library", "Caches");
var tmp = Path.Combine (documents, "..", "tmp");

与文件应用共享

iOS 11 引入了 文件 应用 - iOS 的文件浏览器,允许用户在 iCloud 中查看和与其文件进行交互,并且也由支持它的任何应用程序存储。 若要允许用户直接访问应用中的文件,请在 Info.plist 文件中 LSSupportsOpeningDocumentsInPlace 创建新的布尔键并将其设置为 true,如下所示:

Set LSSupportsOpeningDocumentsInPlace in Info.plist

应用 的文档 目录现在可用于在 “文件” 应用中浏览。 在“文件”应用中,导航到“我的 i电话并且每个具有共享文件的应用都可见。 以下屏幕截图显示了 FileSystem 示例应用 的外观:

iOS 11 Files appBrowse my iPhone filesSample app files

通过 iTunes 与用户共享文件

用户可以通过编辑和创建支持“源”视图中 iTunes 共享UIFileSharingEnabled)条目的应用程序来Info.plist访问应用程序目录中的文件,如下所示:

Adding the Application supports iTunes sharing property

当设备已连接并且用户选择 Apps 选项卡时,可以在 iTunes 中访问这些文件。例如,以下屏幕截图显示了通过 iTunes 共享的所选应用中的文件:

This screenshot shows the files in selected app shared via iTunes

用户只能通过 iTunes 访问此目录中的顶级项。 他们看不到任何子目录的内容(尽管他们可以将其复制到其计算机或删除它们)。 例如,使用 GoodReader、PDF 和 EPUB 文件可以与应用程序共享,以便用户可以在其 iOS 设备上读取这些文件。

如果用户不小心,则修改其 Documents 文件夹的内容可能会导致问题。 应用程序应考虑到这一点,并且能够复原“文档”文件夹的破坏性更新。

本文的示例代码在 Documents 文件夹(SampleCode.cs)中创建一个文件和文件夹,并在 Info.plist 文件中启用文件共享。 此屏幕截图显示了这些内容在 iTunes 中的显示方式:

This screenshot shows how the files appear in iTunes

有关如何 为应用程序设置图标以及创建的任何自定义文档类型的信息,请参阅“使用图像 ”一文。

UIFileSharingEnabled如果密钥为 false 或不存在,则默认情况下,文件共享处于禁用状态,用户将无法与文档目录交互。

备份和还原

当设备由 iTunes 备份时,应用程序主目录中创建的所有目录都将保存,但以下目录除外:

  • [ApplicationName].app – 不要写入此目录,因为它已签名,因此在安装后必须保持不变。 它可能包含从代码访问的资源,但它们不需要备份,因为它们将通过重新下载应用来还原。
  • 库/缓存 – 缓存目录适用于不需要备份的工作文件。
  • tmp – 此目录用于在不再需要时创建和删除的临时文件,或用于 iOS 在需要空间时删除的文件。

备份大量数据可能需要很长时间。 如果决定需要备份任何特定文档或数据,应用程序应使用“文档”和“库”文件夹。 对于可从网络轻松检索的暂时性数据或文件,请使用缓存或 tmp 目录。

注意

当设备在磁盘空间严重不足时,iOS 将“清理”文件系统。 此过程将从当前未运行的应用程序的 Library/Caches 和 tmp 文件夹中删除所有文件。

遵守 iOS 5 iCloud 备份限制

注意

尽管此策略首次引入 iOS 5(这似乎是很久以前),但指南目前仍与应用相关。

Apple 引入了 iOS 5 的 iCloud 备份 功能。 启用 iCloud 备份后,应用程序主目录中的所有文件(不包括通常未备份的目录,例如应用捆绑包 Caches,以及 tmp)都备份到 iCloud 服务器。 此功能为用户提供完整的备份,以防其设备丢失、被盗或损坏。

由于 iCloud 仅向每个用户提供 5 Gb 的可用空间,并且为了避免不必要的使用带宽,因此 Apple 希望应用程序仅备份基本用户生成的数据。 若要遵守 iOS 数据存储准则,应遵循以下各项来限制备份的数据量:

  • 仅在“文档”目录中存储用户生成的数据或无法重新创建的数据(已备份)。
  • 存储可以轻松重新创建或重新下载 Library/Caches 的任何其他数据,或者 tmp (未备份,并且可以“清理”)。
  • 如果文件可能适用于 Library/Cachestmp 文件夹,但不希望“清理”出来,请将这些文件存储在其他位置(例如 Library/YourData),并应用“不备份”属性以防止文件使用 iCloud 备份带宽和存储空间。 此数据仍占用设备上的空间,因此应仔细管理它,并尽可能将其删除。

使用类设置 NSFileManager “不备份”属性。 确保你的类如下所示 using Foundation 进行调用 SetSkipBackupAttribute

var documents = Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments);
var filename = Path.Combine (documents, "LocalOnly.txt");
File.WriteAllText(filename, "This file will never get backed-up. It would need to be re-created after a restore or re-install");
NSFileManager.SetSkipBackupAttribute (filename, true); // backup will be skipped for this file

SetSkipBackupAttributetrue文件何时不会被备份,而不考虑它存储到的目录(甚至Documents目录)。 可以使用该方法查询属性 GetSkipBackupAttribute ,可以通过调用 SetSkipBackupAttribute 方法 false来重置它,如下所示:

NSFileManager.SetSkipBackupAttribute (filename, false); // file will be backed-up

在 iOS 应用和应用扩展之间共享数据

由于应用扩展作为主机应用程序的一部分运行(与其包含的应用相反),因此不会自动包含数据共享,因此需要额外的工作。 应用组是 iOS 用来允许不同的应用共享数据的机制。 如果应用程序已正确配置了正确的权利和预配,则可以在正常的 iOS 沙盒之外访问共享目录。

配置应用组

共享位置是使用应用组配置的,该组iOS 开发人员中心上的“证书、标识符和配置文件”部分中配置。 还必须在每个项目的“Entitlements.plist”中引用此值。

有关创建和配置应用组的信息,请参阅 应用组功能 指南。

文件

iOS 应用和扩展还可以使用通用文件路径共享文件(鉴于它们已正确配置了正确的权利和预配):

var FileManager = new NSFileManager ();
var appGroupContainer =FileManager.GetContainerUrl ("group.com.xamarin.WatchSettings");
var appGroupContainerPath = appGroupContainer.Path

Console.WriteLine ("Group Path: " + appGroupContainerPath);

// use the path to create and update files
...

重要

如果返回的组路径为null,检查权利和预配配置文件的配置,并确保它们正确。

应用程序版本更新

下载应用程序的新版本后,iOS 会创建新的主目录,并将新的应用程序捆绑包存储在其中。然后,iOS 将以下文件夹从旧版应用程序捆绑包移动到新的主目录:

  • 文档
  • Library

其他目录也可以复制并放入新的主目录下,但不能保证复制它们,因此应用程序不应依赖于此系统行为。

总结

本文演示了使用 Xamarin.iOS 的文件系统操作与其他任何 .NET 应用程序类似。 它还引入了应用程序沙盒,并检查了它导致的安全影响。 接下来,它探索了应用程序捆绑包的概念。 最后,它枚举了应用程序可用的专用目录,并在应用程序升级和备份期间解释了其角色。