iOS本地缓存方案
- 1、沙盒机制(sandbox)
- 1.1 Bundle
- 1.2 沙盒原理
- 1.3 沙盒结构
- 1.3.1 Documents
- 1.3.2 Library包含Caches和Preferences目录
- 1.3.3 tmp
- 1.3.4 xxx.app
- 1.3.5 总结
- 2、UserDefaults
- 3、Keychain(钥匙串)
- 3.1、Keychain介绍
- 3.2 Keychain的解构
- 3.3 Keychain的特点
- 3.4 Keychain的使用
- 3.4.1 用户密码
- 3.4.2 iOS 应用间共享 Keychain 数据
- 3.5、KeychainAccess
- 4、plist数据存储
- 4.1 简介
- 4.2 Write写入方式
- 4.3 Plist 文件的读写
- 4.4 Plist 序列化
- 4.5 Plist的使用
- 4.5.1 Plist 文件的创建
- 4.5.2 Plist 文件的解析
- 4.5.3 Plist 文件的解析过程
- 4.5.4 Plist的使用注意
- 5、归档与解归档
- 5.1 简介
- 5.2用法
- 5.2.1 对内置单个对象进行归档和解归档
- 5.2.2 对自定义的单个对象进行归档和解归档
- 5.2.3 对多个对象进行归档和解归档
- 6、数据库
- 6.1 sqlite
- 6.2 CoreData
- 6.3 Realm
- 参考文章
1、沙盒机制(sandbox)
在苹果中,每个应用都有自己对应的沙盒,每个应用程序之间不能相互访问非本程序的沙盒。
1.1 Bundle
bundle就是通常所说的应用程序在手里里面的安装路径,是一个目录,这个目录就是程序的main bundle。这个目录里面包含nib文件、编译代码、以及其他资源的目录等。
- 查看bundle方法,可以通过 itunes下载任意应用,在Finder中找到下载的应用,以归档的方式打开 ipa包,系统会解压出来一个文件夹,在文件夹中找到 .app 的文件,这就是我们安装在手机里的bundle,右键显示包内容可以查看bundle中的文件。
- 通过使用下面的方法获取模拟器在Mac上的 main bundle 和 其路径:
let myBundle = Bundle.main
let myBundlePath = myBundle.bundlePath
- 从Bundle中获取资源文件中的常用方法
if let imagePath = Bundle.main.path(forResource: "tongyuan", ofType: "png") {
let img = UIImage(contentsOfFile: imagePath)
print(img)
}
1.2 沙盒原理
沙盒是一种安全机制,用于防止不同应用之间互相访问。iOS系统下每个应用都有自己对应的沙盒,每个沙盒之间都是相互独立的,互不能访问(没有越狱的情况下)。
- 每个应用程序都有自己的存储空间
- 应用程序不能翻过自己的围墙去访问别的存储空间的内容
- 应用程序请求的数据都要通过权限检测,假如不符合条件的话,不会被放行。
1.3 沙盒结构
沙盒的作用就是存储数据,每个沙盒就相当于每个每个应用的系统目录。
- 每个应用程序位于文件系统的严格限制部分
- 每个应用程序只能在为该程序创建的文件系统中读取文件
- 每个应用程序在iOS系统内都放在了统一的⽂件夹目录下
- 沙盒的本质就是一个⽂件夹,名字是随机分配的
沙盒目录中包含:Documents、Library、tmp和一个xxx.app⽂件
1.3.1 Documents
用于存放程序中的文件数据,应用程序在运行时生成的一些需要长久保存的数据(比如:游戏进度存档、应用程序个人设置等等),通过 iTunes、iCloud 备份时, 会备份这个目录下的数据,此目录下保存相对重要的数据。
//1、获取程序的根目录
let homeDirectory = NSHomeDirectory()
//2、获取document目录
let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last
1.3.2 Library包含Caches和Preferences目录
Caches:存放缓存文件,从网络上下载的文件或者数据(如:音乐缓存、图片缓存等),此目录下的文件不会在应用退出时自动删除 ,需要程序员手动清除改目录下的数据。iTunes、iCloud 备份时不会备份此目录下的数据。主要用于保存应用在运行时生成的需要长期使用的数据,一般用于存储体积较大,不需要备份的非重要数据。
//获取Library目录
let libraryPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).last
//获取Caches目录
let cachesPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).last
Preferences:存放的是基于NSUserDefaults的设置数据,文件格式为 “plist”。设置应用的一些功能会在该目录中查找相应设置的信息,iTunes、iCloud备份时会备份此目录下的数据。该目录由系统自动管理,通常用来储存一些基本的应用配置信息。比如账号密码、自动登录等。
1.3.3 tmp
tmp:存放应用运行时产生的一些临时数据和文件,当应用程序退出、系统磁盘空间不足、手机重启时,都会自动清除该目录的数据。无需程序员手动清除该目录中的数据,iTunes、iCloud备份时不会备份此目录。
//获取Tmp目录
let tmpDirectory = NSTemporaryDirectory()
print(tmpDirectory)
1.3.4 xxx.app
xxx.app(应用程序包):包含程序中的nib⽂件、图片、音频等资源。
//获取xxx.app程序包
let myBundle = Bundle.main
1.3.5 总结
Document : 存储用户数据,需要备份的信息
Library/Caches : 存储缓存文件,程序专用的支持文件
Library/Preferences : 存储应用程序的偏好设置⽂件
tmp : 存储临时文件,比如:下载的zip包,解压后的再删除
xxx.app : 应用程序包
iTunes在与iPhone同步时,会备份 `Documents` 和 `Preferences` 目录下的⽂件 。
2、UserDefaults
系统提供的最简便的key-value本地存储方案,适合比较轻量的数据存储,比如一些业务flag。主要原因数据是明文存储在 plist 文件中,不安全,即使只是修改一个 key 都会 load 整个文件,其底层是用plist文件存储的,在数据量逐步变大后,可能会发生性能问题。
它是单例的,也是线程安全的,是以键值对 key-value 的形式保存在沙盒中。
存储路径为:路径为Library/Preferences/xxx.plist,xxx为项目的Bundle Identifier。
NSUserDefault是用户轻量级的数据持久化,主要用于保存用户程序的配置等信息,以便下次启动程序后能恢复上次的设置。
具体使用:
let key = "age"
//1. 获取一个UserDefaults引用:
let userDefault = UserDefaults.standard
//2. 保存数据
userDefault.set(18, forKey: key)
userDefault.synchronize()
// 3. 读取数据
let age = userDefault.value(forKey: key)
print(age)
// 4. 删除
userDefault.removeObject(forKey: key)
3、Keychain(钥匙串)
3.1、Keychain介绍
项目中有时会需要存储敏感信息(如密码、密钥等),苹果官方提供了一种存储机制–钥匙串(keychain)。
keychain是一种存储在硬盘上的加密的SQLite数据库(AES256加密)。这个可能是卸载App后,keychain信息还在的原因。
keychain适合存储 较小的数据量(不超过上千字节或上兆字节)
的内容。
3.2 Keychain的解构
如上图,每一个keyChain的组成如图,整体是一个字典结构.
(1)kSecClass key 定义属于哪一种类型的keyChain(通用密码、互联网密码、证书、密钥和身份)
(2)不同的类型包含不同的Attributes,这些attributes定义了这个item的具体信息
(3)每个item可以包含一个密码项来存储对应的密码
Keychain可以包含任意数量的keychain item(keychain item称为SecItem,但它是存储在CFDictionary中的).每一个keychain item包含数据和一组属性。SecItem有五类:通用密码、互联网密码、证书、密钥和身份。在大多数情况下,我们用到的都是通用密码。
在macOS中,当keychain被锁的时候加密的item没办法访问,如果你想要该问被锁的item,就会弹出一个对话框,需要你输入对应keychain的密码。当然,未有密码的keychain你可以随时访问。但在iOS中,你只可以访问你自已的keychain items
3.3 Keychain的特点
- 数据并不存放在App的Sanbox中,即使删除了App,资料依然保存在keychain中。如果重新安装了app,还可以从keychain获取数据。
- keychain的数据可以通过group方式,让程序可以在App间共享。不过得要相同TeamID
- keychain的数据是经过加密的
3.4 Keychain的使用
Apple针对keychain也提供了丰富的开发文档说明,包括有Keychain Services Programming Guide:文章中包含了使用mac和ios的keychain开发,首先介绍的是keychain的基本功能和概念,然后还有一个基本的例子介绍了基本的使用keychain API的方法
对于每一个应用来说,KeyChain都有两个访问区,私有区和公共区。私有区是一个sandbox,本程序存储的任何数据都对其他程序不可见,其他应用程序无法访问该区数据。如果要想将存储的内容放在公共区,实现多个应用程序间可以共同访问一些数据,则可以先声明公共区的名称,官方文档管这个名称叫“keychain access group”
这里我们先介绍 KeyChain 私有区的访问存储,即该私有区只能给自己程序使用。比如保存用户密码,设备唯一码(UDID)等等
3.4.1 用户密码
大多数iOS应用需要用到Keychain, 都用来添加一个密码,修改一个已存在Keychain item或者取回密码。Keychain提供了以下的操作
1、SecItemAdd 添加一个item
2、SecItemUpdate 更新已存在的item
3、SecItemCopyMatching 搜索一个已存在的item
4、SecItemDelete 删除一个keychain item
当然对于只需要保存用户名和密码的应用来说,SSKeyChain可能更加适合,它对keychain做了相应的封装,接口相对来说更加简单 通过以下类方法来使用SSKeyChain(请查看SSKeyChain.h)。
+ (NSArray *)allAccounts;
+ (NSArray *)accountsForService:(NSString *)serviceName;
+ (NSString *)passwordForService:(NSString *)serviceNameaccount:(NSString *)account;
+ (BOOL)deletePasswordForService:(NSString *)serviceNameaccount:(NSString *)account;
+ (BOOL)setPassword:(NSString *)password forService:
(NSString*)serviceName account:(NSString *)account;
SSKeyChain的方法中涉及到的变量主要有三个,分别是password、service、account。password、account分别保存的是密码和用户名信息。service保存的是服务的类型,就是用户名和密码是为什么应用保存的一个标志。比如一个用户可以在不同的论坛中使用相同的用户名和密码,那么service保存的信息分别标识不同的论坛。由于包名通常具有一定的唯一性,通常在程序中可以用包的名称来作为service的标识。
3.4.2 iOS 应用间共享 Keychain 数据
在 iOS 3.0 之后,应用间共享 keychain 数据成为了一种可能。但这是被严格限制的,只有拥有相同 App ID 前缀的应用才有可能共享 keychai,并且各应用存储的 keychain item 都标记了相同的 kSecAccessGroup 字段值
- 相同的Team ID
- 有相同的 Team ID这个是应用间共享 Keychain 数据的前提条件。一个 App ID 分两部分:
1、Apple 为你生成的 Team ID
2、开发者注册的 Bundle ID
一个典型的 App ID 如:GPZ8FX842Q.com.apple.app, GPZ8FX842Q即为你的Team ID,是 Apple 为你生成的
一个开发者账号可以有不同的几个 Team ID。但 Apple 不会为不同的开发者生成一样的 Team ID。这样,不同的开发者账号发布的应用想共享 keychain 数据,在现在来看是无法实现的。而要做到 Keychain 数据共享,要求是同一个开发账号开发的,同时选择了相同的 Team ID
- 打开Keychain Sharing权限
如图打开Keychain Sharing开关,设置好正确的 Keychain Group,设置好后应该会生成一个文件,如下图
并且会在 Project->build setting->Code Signing Entitlements 里自动配置这个文件的路径
配置好后,须用你正式的证书签名编译才可通过。否则xcode会弹框告诉你code signing有问题。所以,苹果限制了你只能同公司的产品共享KeyChain数据,别的公司访问不了你公司产品的KeyChain。
3.5、KeychainAccess
一个Swift的第三方框架:
https://github.com/kishikawakatsumi/KeychainAccess
4.1 简介
直接将数据写在代码里面,不是一种合理的做法。如果数据经常改,就要经常翻开对应的代码进行修改,造成代码扩展性低。因此,可以考虑将经常变的数据放在文件中进行存储,程序启动后从文件中读取最新的数据。如果要变动数据,直接修改数据文件即可,不用修改代码。一般可以使用属性列表文件存储 NSArray 或者 NSDictionary 之类的数据,这种 “属性列表文件” 的扩展名是 plist,因此也称为 “plist 文件”。 plist 是以 xml 文件形式存储的。
如果对象是 NSString、NSArray、NSDictionary、NSData 和 NSNumber 类型,可以用这些类中实现的 writeToFile: atomically: 方法将数据写到文件中。
当根据字典创建属性列表时,字典中的键必须都是 NSString 对象。数组中的元素或字典中的值可以是 NSString、NSArray、NSDictionary、NSData、NSDate 和 NSNumber 对象。
iOS 实现的序列化方式的两种:NSKeyedArchiver,NSPropertyListSerialization。在这两种序列化方式中,NSData 都是序列化的目标。两种方式的不同点在于 NSPropertyListSerialization 是针对数组和字典类型的,而 NSKeyedArchiver 是针对对象的。
4.2 Write写入方式
永久保存在磁盘中。具体方法为:
第一步:获得文件即将保存的路径:
- 使用 C 函数 NSSearchPathForDirectoriesInDomains 来获得沙盒中目录的全路径。该函数有三个参数,目录类型、domain mask、布尔值。其中布尔值表示是否需要通过 ~ 扩展路径。而且第一个参数是不变的,即为 NSSearchPathDirectory 。在 iOS 中后两个参数也是不变的,即为:.DocumentDirectory 和 true。
let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last
- 还有一种方法是使用 NSHomeDirectory 函数获得 sandbox 的路径,但是不能在 sandbox 的本文件层上写文件也不能创建目录,而应该是此基础上创建一个新的可写的目录,例如 Documents, Library 或者 temp 。具体的用法为:
// 将 Documents 添加到 sandbox 路径上
let documentPath = NSHomeDirectory().appending("/Documents")
- 这两者的区别就是:使用 NSSearchPathForDirectoriesInDomains 比在 NSHomeDirectory 后面添加 Documents 更加安全。因为该文件目录可能在未来发送的系统上发生改变。
第二步:生成该路径下的文件:
let documentPath = NSHomeDirectory().appending("/Documents")
let filename = documentPath.appending("/filename")
第三步:往文件中写入数据:
let data = Data()
if let url = URL(string: documentPath) {
do {
try data.write(to: url, options: .atomic)
}catch {}
}
第四步:从文件中读出数据
if let url = URL(string: documentPath) {
let data = try? Data(contentsOf: url)
}
4.3 Plist 文件的读写
let dictionaryPath:String = NSHomeDirectory().appending("/PropertyDictionaryList.plist")
let arrayPath:String = NSHomeDirectory().appending("/PropertyArrayList.plist")
//待写入的数组数据
let array = ["my","name","is","lichangan"]
//待写入的字典数据
let dictionary = ["name":"lichangan","age":18,"sex":"female"] as [String : Any]
//写入数组
let writeArrayIsSucceed = (array as NSArray).write(toFile: arrayPath, atomically: true)
print(writeArrayIsSucceed)
//写入字典
let writeDictionaryIsSucceed = (dictionary as NSDictionary).write(toFile: dictionaryPath, atomically: true)
print(writeDictionaryIsSucceed)
//读plist文件
let arrayFromPlist = NSArray(contentsOfFile: arrayPath)
let dictionaryFromPlist = NSDictionary(contentsOfFile: dictionaryPath)
print(arrayFromPlist)
print(dictionaryFromPlist)
4.4 Plist 序列化
let dictionaryPath:String = NSHomeDirectory().appending("/PropertyDictionaryList.plist")
let arrayPath:String = NSHomeDirectory().appending("/PropertyArrayList.plist")
//待写入的数组数据
let array = ["my","name","is","lichangan"]
//待写入的字典数据
let dictionary = ["name":"lichangan","age":18,"sex":"female"] as [String : Any]
//序列化,将数据转换成 XML 格式的文件
let arrayData = try PropertyListSerialization.data(fromPropertyList: array, format: .xml, options: .bitWidth)
let dictionaryData = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .xml, options: .bitWidth)
do {
// 输出到 .txt 格式文件中
try (arrayData as NSData).write(toFile: arrayPath, options: .atomic)
}catch {
print(error.localizedDescription)
}
do{
// 输出到 .txt 格式文件中
try (dictionaryData as NSData).write(toFile: dictionaryPath, options: .atomic)
}catch{
print(error.localizedDescription)
}
//反序列化
let arrayFromFile:NSArray = NSArray(contentsOfFile: arrayPath) ?? []
let dictionaryFromFile:NSDictionary = NSDictionary(contentsOfFile: dictionaryPath) ?? [:]
print(arrayFromFile)
print(dictionaryFromFile)
4.5 Plist的使用
4.5.1 Plist 文件的创建
4.5.2 Plist 文件的解析
if let path = Bundle.main.path(forResource: "area", ofType: "plist") {
let areas:NSArray = NSArray(contentsOfFile: path) ?? []
print(areas)
}
4.5.3 Plist 文件的解析过程
4.5.4 Plist的使用注意
- plist 的文件名不能叫做 “info”、“Info” 之类的。
- 添加 plist 等文件资源的时候,一定要勾选下面的选项。
5.1 简介
所谓文件归档,就是把需要存储的对象数据存储到沙盒的Documents目录下的文件中,即存储到了磁盘上, 实现数据的持久性存储和备份。解归档,就是从磁盘上读取该文件下的数据,用来完成用户的需求。 对象归档是将对象归档以文件的形式保存到磁盘中(也称为序列化,持久化),使用的时候读取该文件的保存路径 的 读取文件的内容(也称为接档,反 序列化),(对象归档的文件是保密的,在磁盘上无法查看文件中的内容, 而 属性列表是明文的,可以查看)。
通过文件归档产生的文件是不可见的,如果打开归档文件的话,内容是乱码的;它不同于属性列表和plist文件是 可见的, 正因为不可见的缘故,使得这种持久性的数据保存更有可靠性。
5.2用法
5.2.1 对内置单个对象进行归档和解归档
func arcFunc(filename:String) {
//1、获取归档文件的路径(归档文件名可以自己随意取名)
var documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last
let arrayArcPath = documentPath?.appending("/\(filename)")
print(arrayArcPath)
//2、准备归档的对象数据
let array = [1,2,3,4,5]
guard let arrayArcPath = arrayArcPath else{return}
//3、对数据进行归档(根据flag标识进行判断,如果返回的是YES,归档成功;反之,归档失败)
let isSucceed = NSKeyedArchiver.archiveRootObject(array, toFile: arrayArcPath)
if isSucceed {
print("归档对象成功")
//4、对数据进行解归档
let array2 = NSKeyedUnarchiver.unarchiveObject(withFile: arrayArcPath) as? NSArray
print(array2)
}
}
5.2.2 对自定义的单个对象进行归档和解归档
由于自定义对象不具有归档的性质,所以要实现归档和解归档,首先必须实现协议。
//1.自定义一个归档对象类,实现协议
//新建立的类必须要继承自NSObject,否则会报错
//Unrecognized selector -[__lldb_expr_94.Person replacementObjectForKeyedArchiver:]
class Person : NSObject,NSCoding {
var name:String?
var age:Int?
override init() {
}
//解归档的协议方法
required init?(coder: NSCoder) {//将归档对象序列化
name = coder.decodeObject(forKey: "name") as? String
age = coder.decodeInteger(forKey: "age")
}
//归档的协议方法
func encode(with coder: NSCoder) {//将归档对象反序列化
coder.encode(name, forKey: "name")
//如果Int类型直接用Optional的会报错
//libc++abi: terminating with uncaught exception of type NSException
if let age = age {
coder.encode(age, forKey: "age")
}
}
}
func arcFunc(filename:String) {
//1、获取归档文件的路径(归档文件名可以自己随意取名)
var documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last
let arrayArcPath = documentPath?.appending("/\(filename)")
//2、准备归档的对象数据
let person = Person()
person.name = "lichangan"
person.age = 18
guard let arrayArcPath = arrayArcPath else{return}
print(arrayArcPath)
//3、对数据进行归档(根据flag标识进行判断,如果返回的是YES,归档成功;反之,归档失败)
let isSucceed = NSKeyedArchiver.archiveRootObject(person, toFile: arrayArcPath)
if isSucceed {
print("归档对象成功")
//4、对数据进行解归档
let person = NSKeyedUnarchiver.unarchiveObject(withFile: arrayArcPath) as? Person
print(person?.name)
print(person?.age)
}
}
arcFunc(filename: "Lichangan")
5.2.3 对多个对象进行归档和解归档
前面的两种方式都是针对于单个对象进行归档和解归档,如果需要对多个对象进行归档,它们就无用武之地了,局限性很大,因此,这里介绍一个新的归档方式,多对象归档和解归档。
6、数据库6.1 sqlite
6.2 CoreData
6.3 Realm
参考文章IOS沙盒基本机制(sandbox)iOS 钥匙串的基本使用iOS - Swift PList 数据存储iOS:文件归档和解归档的详解和使用