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的解构

ios 方法缓存 苹果怎样缓存_ios 方法缓存


如上图,每一个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、plist数据存储

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 文件的创建

ios 方法缓存 苹果怎样缓存_数据_02

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 文件的解析过程

ios 方法缓存 苹果怎样缓存_数据_03

4.5.4 Plist的使用注意

  • plist 的文件名不能叫做 “info”、“Info” 之类的。
  • 添加 plist 等文件资源的时候,一定要勾选下面的选项。
  • ios 方法缓存 苹果怎样缓存_iOS_04


5、归档与解归档

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:文件归档和解归档的详解和使用