Xamarin.iOS 中的文件系统访问

你可以使用 Xamarin.iOS 和 .NET 基类库 (BCL) 中的 System.IO 类访问 iOS 文件系统。 File 类可以创建、删除和读取文件,Directory 类可以创建、删除或枚举目录的内容。 此外,还可以使用 Stream 子类,它们可以实现对文件操作(例如,压缩或文件内位置搜索)的更高程度的控制。

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

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

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

执行一些简单文件系统操作的 iOS 示例

常规文件访问

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

以下代码片段演示了一些常见的文件操作。 在本文的示例应用程序中,你将在 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 类访问“Documents”文件夹(我们可在其中创建文件和目录)。

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 包添加到应用程序项目

将 NuGet 包添加到应用程序项目

接下来,添加一个类来充当用于实现序列化/反序列化的数据模型(在本例中为 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 有所不同。

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

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

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

事例敏感性

请务必注意,iOS 文件系统是区分大小写的。 区分大小写意味着文件名和目录名必须完全匹配:README.txt 和 readme.txt 将被视为不同的文件名

对于更熟悉 Windows 文件系统的 .NET 开发人员而言,这可能会令人感到困惑。因为后者不区分大小写,“Files”“FILES”和“files”指的都是同一个目录。

警告

iOS 模拟器不区分大小写。 如果文件本身和代码中对它进行引用时所使用的文件名大小写不同,那么代码可能仍可在模拟器中正常运行,但在实际设备上则会失败。 这是必须要在 iOS 开发期间尽早且经常在实际设备上进行部署和测试的原因之一。

路径分隔符

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

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

应用程序沙盒

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

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

应用程序包

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

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

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

浏览 bin 目录以查找类似于此屏幕截图的应用程序图标

右键单击此图标,然后选择“显示包内容”以浏览应用程序包目录的内容。 其中的内容看起来就像常规目录的内容一样,如下所示:

应用程序包的内容

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

应用程序目录

在设备上安装应用程序时,操作系统会为应用程序创建一个主目录,并在可供使用的应用程序根目录中创建多个目录。 从 iOS 8 开始,用户可访问的目录不再位于应用程序根目录中,因此你无法基于用户目录获得应用程序包的路径,反之亦然。

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

 

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

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

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

即使应用程序当下未启用文件共享,也应避免将应向用户隐藏的文件(例如数据库文件,除非你打算共享这些文件)放置在此目录中。 只要敏感文件保持隐藏状态,那么即使将来的版本启用了文件共享,也不会公开(以及可能由 iTunes 移动、修改或删除)这些文件。

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

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

你可以在 Library 中创建自己的子目录;但请注意,这里已有一些由系统创建的目录,包括 Preferences 和 Caches。

此目录的内容(Caches 子目录除外)由 iTunes 备份。 你在 Library 中创建的自定义目录将会进行备份。
Library/Preferences/ 特定于应用程序的首选项文件将存储在此目录中。 请使用 NSUserDefaults 类,而不是直接创建这些文件。

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

iTunes 不会对此目录的内容进行备份,这意味着如果用户还原设备,这些内容将不再存在,而且在安装应用程序的更新版本后,这些内容也可能不再存在。

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

iTunes 不会对此目录的内容进行备份。

例如,tmp 目录可用于存储为了向用户显示而下载的临时文件(例如 Twitter 头像或电子邮件附件),但这些文件可在查看后删除(如果将来需要,则可重新下载)。

以下屏幕截图显示了“查找器”窗口中的目录结构:

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

以编程方式访问其他目录

前面的目录和文件示例访问了 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);

可以通过如下所示的代码构造 Cachestmp 目录的路径:

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

在 Info.plist 中设置 LSSupportsOpeningDocumentsInPlace

应用的“Documents”目录现在可供在“文件”应用中进行浏览。 在“文件”应用中,导航到“我的 iPhone”,此时将显示每个具有共享文件的应用。 以下屏幕截图显示了该示例应用的样子:

iOS 11 Files 应用浏览 iPhone 文件示例应用文件

通过 iTunes 与用户共享文件

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

添加应用程序支持 iTunes 共享属性

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

此屏幕截图显示了通过 iTunes 共享的所选应用中的文件

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

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

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

此屏幕截图显示了这些文件在 iTunes 中的显示方式

请参阅使用图像一文,了解如何为应用程序以及你创建的任何自定义文档类型设置图标。

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

备份和还原

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

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

备份大量数据可能需要很长时间。 如果你决定需要备份任何特定的文档或数据,则应用程序应使用 Documents 和 Library 文件夹。 对于可以轻松从网络中检索的临时数据或文件,请使用 Caches 或 tmp 目录。

注意

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

遵守 iOS 5 iCloud 备份限制

注意

虽然这项政策最初是在 iOS 5 中推出的(这似乎是很久以前的事了),但该准则今天仍然适用于各种应用。

Apple 在 iOS 5 中推出了 iCloud 备份功能。 启用 iCloud 备份后,应用程序主目录中的所有文件(不包括通常不进行备份的目录,例如应用程序包、Cachestmp)都将备份到 iCloud 服务器。 此功能可为用户提供完整的备份,以防其设备丢失、被盗或损坏。

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

  • 仅在 Documents 目录(将进行备份)中存储用户生成的数据或无法重新创建的数据。
  • 将可轻松重新创建或重新下载的任何其他数据存储在 Library/Cachestmp(不进行备份且可以“清理”)中。
  • 如果你有一些文件可能适合存储在 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 应用程序时类似。 文中还介绍了应用程序沙盒,并探讨了它所带来的安全影响。 之后,探索了应用程序包的概念。 最后,枚举了应用程序可使用的专用目录,并解释了其在应用程序升级和备份期间所扮演的角色。