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);

可以构造路径 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,如下所示:

Set LSSupportsOpeningDocumentsInPlace in Info.plist

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

iOS 11 Files appBrowse my iPhone filesSample app files

通过 iTunes 与用户共享文件

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

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 (的 Documents 文件夹中创建一个文件和文件夹) ,并在 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 备份后,应用程序主目录中的所有文件 (不包括通常未备份的目录,例如应用捆绑包 Cachestmp) 备份到 iCloud 服务器。 此功能为用户提供完整的备份,以防设备丢失、被盗或损坏。

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

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

使用 NSFileManager 类设置“请勿备份”属性。 确保你的类如下所示using FoundationSetSkipBackupAttribute

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

true文件SetSkipBackupAttribute何时不会被备份,无论该文件存储在 (甚至目录) Documents。 可以使用该方法查询属性,并通过调用false该方法来重置该SetSkipBackupAttribute属性GetSkipBackupAttribute,如下所示:

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 应用程序类似。 它还引入了应用程序沙盒,并检查了它导致的安全影响。 接下来,它探讨了应用程序捆绑包的概念。 最后,它枚举了应用程序可用的专用目录,并在应用程序升级和备份期间解释了其角色。