iOS 从零开始使用 Swift:iOS 上的数据持久性和沙盒

iOS 从零开始使用 Swift系列:
探索 iOS SDK
探索 Foundation 框架
使用 UIKit 的第一步
自动布局基础
表格视图基础
导航控制器和视图控制器层次结构
iOS 上的数据持久性和沙盒
构建购物清单应用程序 1
构建购物清单应用程序 2
下一步该去哪里

跨应用程序启动持久化数据是大多数 iOS 应用程序的要求,从在默认系统中存储用户偏好到在关系数据库中管理大型数据集。在本文中,我们将探讨用于在 iOS 应用程序中存储数据的最常用策略。我还将讨论 iOS 上的文件系统以及应用程序沙盒如何影响数据持久性。

介绍

你已经走了很长一段路,蚱蜢,你学到了很多东西。但是我们还没有讨论 iOS 开发的一个重要方面,即数据持久性。几乎每个 iOS 应用程序都会存储数据以供以后使用。您的应用程序存储的数据可以是任何东西,从用户偏好到临时缓存,甚至是大型关系数据集。

在讨论开发人员在 iOS 平台上最常见的数据持久性策略之前,我将花几分钟时间讨论文件系统和应用程序沙箱的概念。您真的认为可以将应用程序的数据存储在文件系统上的任何位置吗?再想一想,学徒。

文件系统和应用程序沙盒

自 2007 年推出 iPhone 以来,iOS 平台上的安全性一直是 Apple 的首要任务之一。与 OS X 应用程序相比,iOS 应用程序被放置在应用程序沙箱中。应用程序的沙箱不仅仅指文件系统中应用程序的沙箱目录。它还包括对存储在设备、系统服务和硬件上的用户数据的受控和有限访问。

随着 Mac App Store 的推出,Apple 也开始在 OS X 上实施应用程序沙盒。尽管对 OS X 应用程序施加的限制不如对 iOS 应用程序施加的限制那么严格,但基本概念是相似的。不过也有区别。例如,iOS 应用程序的应用程序沙箱包含应用程序包,而对于 OS X 应用程序则不是这样。造成这些差异的原因主要是历史原因。

沙盒和目录

操作系统将每个 iOS 应用程序安装在一个沙盒目录中,该目录包含应用程序包目录和三个附加目录 Documents、  Library和 tmp。应用程序的沙箱目录,通常称为它的  目录,可以通过调用一个简单的 Foundation 函数来访问,  NSHomeDirectory().

print(NSHomeDirectory())

你可以自己试试这个。基于 Single View Application 模板创建一个新的 Xcode 项目,并将其命名为 Data Persistence。

打开 AppDelegate.swift 并将上面的代码片段添加到 application(_:didFinishLaunchingWithOptions:). 如果您在模拟器中运行应用程序,控制台中的输出应如下所示:

/Users/Bart/Library/Developer/CoreSimulator/Devices/14F00EFB-2EAB-438C-B401-7FEFDA1D94AB/data/Containers/Data/Application/81B23594-3BA2-4AF9-B91A-F74A53FD6945

但是,如果您在物理设备上运行应用程序,输出看起来会有些不同,如下所示。但是,应用程序沙箱和施加的限制是相同的。

/var/mobile/Containers/Data/Application/41E7939B-6A39-4005-9C28-372FD9C7AD99

检索应用程序的 Documents 目录的路径需要更多的工作,您可以在下一个代码片段中看到。

let directories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
 
if let documents = directories.first {
    print(documents)
}

我们调用 NSSearchPathForDirectoriesInDomains() 在 Foundation 框架中定义的函数。作为第一个参数,我们传入 DocumentDirectory type NSSearchPathDirectory 以表明我们只对应用程序的 Documents 目录感兴趣。第二和第三个论点对我们的讨论不太重要。该函数返回一个类型为 的数组,其中包含一个结果,即应用程序Documents 目录[String]的路径 。

为什么要沙盒?

沙盒有什么好处?沙盒应用程序的主要原因是安全性。通过将应用程序限制在自己的沙箱中,受感染的应用程序不会对操作系统或其他应用程序造成损害。

受 感染 的应用程序是指已被黑客入侵的应用程序、故意恶意的应用程序,以及包含可能无意中造成损害的严重错误的应用程序。

即使应用程序在 iOS 平台上被沙箱化,应用程序也可以通过许多系统接口请求访问其应用程序沙箱之外的某些文件或资产。这方面的一个例子是存储在设备上的音乐库。但是要知道,系统框架负责与文件访问相关的任何操作。

什么去哪里?

尽管您可以在应用程序的沙箱中做几乎任何您想做的事情,Apple 还是提供了一些关于哪些内容应该存储在哪里的指南。出于多种原因,了解这些指南很重要。将 iOS 设备备份到您的计算机或 iCloud 时,并非沙盒中的所有文件都包含在备份中。

例如,  tmp 目录应该只用于临时存储文件。操作系统可以随时清空该目录,例如,当设备磁盘空间不足时。tmp 目录不包含在备份中。 

Documents 目录用于存储用户数据,而 Library 目录 用于存储与用户没有严格关联的应用程序数据。Library目录中的 Caches 目录  是另一个未备份的目录。

还要记住,您的应用程序不应该修改应用程序包目录的内容。安装应用程序时对应用程序包目录进行签名。通过以任何方式修改应用程序包目录的内容,上述签名被更改,这意味着操作系统不允许应用程序再次启动。这是 Apple 为保护客户而采取的另一项安全措施。

数据持久性选项

有几种将应用程序数据存储在磁盘上的策略。在本文中,我们将简要介绍 iOS 上的四种常见方法:

  • defaults system
  • property lists
  • SQLite
  • Core Data
  • 默认系统
  • 财产清单
  • SQLite
  • 核心数据

本文中描述的选项不应被视为可互换的。每种策略都有优点和缺点。让我们先来看看默认系统。

用户默认值

默认系统是 iOS 从 OS X 继承而来的。尽管它是为存储用户首选项而创建和设计的,但它可以用于存储任何类型的数据,只要它是属性列表类型、  NSString、  NSNumber、  NSDate、  NSArrayNSDictionary和 NSData,或它们的任何可变变体。

Swift 数据类型呢?幸运的是,Swift 足够聪明。它可以通过将字符串和数字转换为NSString和来存储它们NSNumber。这同样适用于 Swift 数组和字典。

默认系统只不过是一组属性列表,每个应用程序一个属性列表。属性列表存储在  应用程序 库文件夹中的Preferences文件夹中 ,暗示了属性列表的用途和功能。

开发人员喜欢默认系统的原因之一是它非常易于使用。看看下面的例子,看看我的意思。

let userDefaults = NSUserDefaults.standardUserDefaults()
 
// Setting Values
userDefaults.setBool(true, forKey: "Key1")
userDefaults.setInteger(123, forKey: "Key2")
userDefaults.setObject("Some Object", forKey: "Key3")
userDefaults.setObject([1, 2, 3, 4], forKey: "Key4")
 
// Getting Values
userDefaults.boolForKey("Key1")
userDefaults.integerForKey("Key2")
userDefaults.objectForKey("Key3")
userDefaults.objectForKey("Key4")
 
userDefaults.synchronize()

通过调用standardUserDefaults() on  NSUserDefaults,返回对共享默认对象的引用。

在最后一行,我们调用 synchronize() 共享默认对象将任何更改写入磁盘。很少需要调用 synchronize(),因为默认系统会在必要时保存更改。但是,如果您使用默认系统存储或更新设置,有时将更改显式保存到磁盘可能很有用或必要。

乍一看,默认系统似乎只不过是位于特定位置的键值存储。但是, NSUserDefaults 在 Foundation 框架中定义的类不仅仅是管理键值存储的接口。查看其 类参考 以获取更多信息。

在我们继续之前,将上面的代码片段粘贴到应用程序委托的 application(_:didFinishLaunchingWithOptions:) 方法中并在模拟器中运行应用程序。打开一个新的 Finder 窗口并导航到 Library > Developer > CoreSimulator > Devices > <DEVICE_ID> > data > Containers > Data > Application > <APPLICATION_ID>

<DEVICE_ID>和 <APPLICATION_ID>分别是模拟器和您的应用程序唯一的两个标识符。模拟器的应用程序沙箱的位置取决于您使用的 Xcode 版本。如果您不使用 Xcode 7,则路径可能会有所不同。

您可以通过将应用程序主目录的路径打印到 Xcode 的控制台来使您的生活更轻松。将以下打印语句添加到 application(_:didFinishLaunchingWithOptions:).

print(NSHomeDirectory())

神秘命名的文件夹是应用程序沙箱目录。在应用程序沙箱目录中,打开  位于 Library文件夹中的Preferences文件 夹,并检查其内容。

您应该会看到一个名称与应用程序包标识符相同的属性列表。这是您的应用程序的用户默认存储。这就是属性列表在文本编辑器中的样子。如您所见,属性列表作为 XML 文件存储在磁盘上。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Key1</key>
    <true/>
    <key>Key2</key>
    <integer>123</integer>
    <key>Key3</key>
    <string>Some Object</string>
    <key>Key4</key>
    <array>
        <integer>1</integer>
        <integer>2</integer>
        <integer>3</integer>
        <integer>4</integer>
    </array>
</dict>
</plist>

如果您想更轻松地在模拟器中访问应用程序的沙箱,那么我鼓励您查看 SimPholders。这是一个小型实用程序,可以使使用模拟器变得更加容易。

属性列表

我们已经在本系列中介绍了属性列表。事实上,用户默认数据库的后备存储是一个属性列表。使用属性列表是存储和检索对象图的便捷策略。属性列表已经存在了很长时间,易于使用,因此,它们是在 iOS 应用程序中存储数据的绝佳选择。

正如我之前提到的,重要的是要记住属性列表只能存储属性列表数据。这是否意味着无法使用属性列表存储自定义模型对象?这是可能的。但是,自定义模型对象需要存档(一种序列化形式),然后才能存储在属性列表中。归档对象只是意味着需要将对象转换为可以存储在属性列表中的数据类型,例如 NSData 实例。

归档对象

你还记得 NSCoding Foundation 框架中定义的协议吗?该 NSCoding 协议定义了两种方法,init(coder:) 和 encodeWithCoder(_:). 类实现这些方法以允许对类的实例进行编码和解码。

编码和解码是对象归档和分发的基础机制。对象归档的工作原理将在本系列的稍后部分变得清晰。在本课中,我只向您展示如何使用属性列表将数组和字典写入磁盘。

写入文件

下面的代码片段应该让您了解将数组或字典写入磁盘是多么容易。理论上,存储在属性列表中的对象图可以像您想要的那样复杂或大。但是,请记住,属性列表并不意味着存储数十或数百兆字节的数据,尝试以这种方式使用它们可能会导致性能下降。

let directories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
 
if let documents = directories.first {
    if let urlDocuments = NSURL(string: documents) {
        let urlFruits = urlDocuments.URLByAppendingPathComponent("fruits.plist")
        let urlDictionary = urlDocuments.URLByAppendingPathComponent("dictionary.plist")
         
        // Write Array to Disk
        let fruits = ["Apple", "Mango", "Pineapple", "Plum", "Apricot"] as NSArray
        let dictionary = ["anArray" : fruits, "aNumber" : 12345, "aBoolean" : true] as NSDictionary
         
        fruits.writeToFile(urlFruits.path!, atomically: true)
        dictionary.writeToFile(urlDictionary.path!, atomically: true)
         
        // Load from Disk
        let loadedFruits = NSArray(contentsOfURL: urlFruits)
        if let fruits = loadedFruits {
            print(fruits)
        }
         
        let loadedDictionary = NSDictionary(contentsOfURL: urlDictionary)
        if let dictionary = loadedDictionary {
            print(dictionary)
        }
    }
}

让我们看一下上面的代码片段。我们首先将对数组字面量的引用存储在名为 的变量中 fruits。我们创建文件 URL 来存储我们将要创建的属性列表。文件 URL 是通过将字符串附加到 Documents 目录的文件 URL 来创建的。我们附加的字符串是我们在一秒钟内创建的属性列表的名称,包括它的扩展名 .plist

将数组写入磁盘就像调用 writeToFile(_:atomically:) 数组一样简单。您现在可以忽略该 atomically 标志。如示例所示,将字典写入磁盘遵循类似的模式。该示例还说明了如何从属性列表创建数组和字典,但这是我们在本系列前面已经介绍过的内容。在模拟器中运行应用程序并导航到应用程序的 Documents 目录。在此目录中,您应该会看到我们刚刚创建的两个属性列表。

这是在文本编辑器中打开字典时的属性列表。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>aBoolean</key>
    <true/>
    <key>aNumber</key>
    <integer>12345</integer>
    <key>anArray</key>
    <array>
        <string>Apple</string>
        <string>Mango</string>
        <string>Pineapple</string>
        <string>Plum</string>
        <string>Apricot</string>
    </array>
</dict>
</plist>

SQLite

如果您的应用程序是数据驱动的并且处理大量数据,那么您可能需要研究 SQLite。什么是 SQLite?SQLite 网站上的标语是“小。快。可靠。选择任何三个。”,很好地总结了它。

SQLite 是一个实现轻量级嵌入式关系数据库的库。顾名思义,它与 MySQL 和 PostgreSQL 一样基于 SQL 标准(结构化查询语言)。

与其他 SQL 数据库的主要区别在于 SQLite 可移植且非常轻量级。SQLite 不是从客户端应用程序访问的单独进程,而是无服务器的。换句话说,它嵌入在应用程序中或由运行应用程序的系统管理,这意味着它非常快。

SQLite 网站声称它是部署最广泛的 SQL 数据库。我不知道情况是否仍然如此,但它肯定是客户端数据存储的流行选择。SQLite 与直接处理对象相比的优势在于 SQLite 快得多。这主要是由于关系数据库和面向对象编程语言的根本不同。

为了弥合 SQLite 和 Objective-C 之间的差距,  随着时间的推移,已经创建了许多对象关系映射(ORM) 解决方案。Apple 为 iOS 和 OS X 创建的 ORM 被命名为 Core Data,我们将在本课的后面部分了解它。

Flying Meat’s FMDB

在 iOS 上使用 SQLite 意味着使用基于 C 的库。如果您更喜欢面向对象的解决方案,那么我强烈推荐 Gus Mueller ( Flying Meat, Inc. ) 用于 SQLite、  FMDB的 Objective-C 包装器。

该库非常高效。我过去使用过 FMDB,并且对它的 API 和库的健壮性和可靠性非常满意。

核心数据

刚接触 Core Data 的开发人员经常将 Core Data 误认为是数据库,而它实际上是由 Apple 创建和维护的对象关系映射解决方案。 Matt Gallagher 写了一篇关于 Core Data 和数据库之间差异的精彩文章。Core Data 提供了一个关系型面向对象模型,可以序列化为 XML、二进制或 SQLite 存储。Core Data 甚至支持内存存储。

为什么要使用 Core Data 而不是 SQLite?通过问这个问题,你错误地认为 Core Data 是一个数据库。使用 Core Data 的优势在于您使用对象而不是原始数据,例如 SQLite 数据库中的行或存储在 XML 文件中的数据。尽管 Core Data 在首次发布时经历了一些艰难的岁月,但它已经成长为一个强大的框架,具有许多功能,包括自动迁移、更改跟踪、故障排除和集成验证。

许多开发人员欣赏的另一个很棒的功能是内置在 Xcode 中的 Core Data 模型编辑器。该编辑器允许开发人员通过图形界面对应用程序的数据模型进行建模。

Core Data 是否适合您的应用程序取决于您计划管理的数据,无论是在数据量方面还是在底层模型方面。如果您计划管理非常大的数据集,那么随着时间的推移,Core Data 可能会成为性能瓶颈。在这种情况下,SQLite 可能是更好的解决方案。

iCloud

您可能听说过 iCloud,并且想知道 iCloud 在数据持久性故事中的位置。与 SQLite 和 Core Data 不同,iCloud 不是一种数据持久性形式。相反,它是一个平台或服务,用于使用户数据在多个设备和应用程序的多个实例(甚至是应用程序系列)中可用。

iCloud 平台包含多个服务或组件。我们感兴趣的组件是 iCloud Storage,它包括四种类型的存储:

  • CloudKit
  • key-value storage
  • document storage
  • Core Data storage
  • CloudKit
  • 键值存储
  • 文件存储
  • 核心数据存储

如果您想了解更多关于 iCloud Storage 的信息,我建议您阅读 我的关于 iCloud Storage 的系列

结论

您现在应该对在为 iOS 平台进行开发时必须存储数据的选项有了一个很好的了解。请记住,并非我们介绍的所有策略都是平等的。

这个系列正在慢慢接近尾声。在接下来的两部分中,我们将创建另一个应用程序来将我们目前所学的知识付诸实践。最好的学习方式是边做边学。

ios-from-scratch-with-swift-data-persistence-and-sandboxing-on-ios–cms-25505