iOS的数据持久化总结

0x01、UserDefaults

特点

  1. UserDefaults是一个单利类
  2. 并且是线程安全
  3. 数据存储在.plist文件中
  4. 一般保存首选项、少量缓存数据
  5. 不能将自定义的对象保存起来

使用

1
2
3
4
5
6
7
8
9
10
11
12
//取得单利对象
let defaults = UserDefaults.standard

//使用范型方法保存数据
open func set(_ value: Any?, forKey defaultName: String)

//使用对应类型的方法获取数据
open func string(forKey defaultName: String) -> String?

//早期使用UserDefaults保存数据以后 都要使用synchronize方法 使数据同步到plist中 现在苹果建议 废弃掉此方法 并在未来版本中进行标记 不用在调用 synchronize 了
//-synchronize is deprecated and will be marked with the NS_DEPRECATED macro in a future release.
defaults.synchronize()

0x02、Settings Bundle

特点

  1. 无需开发单独的界面,由Settings提供
  2. 用户设置的参数也由Settings提供

使用

新建Bundle

新建文件 选择 Settings Bundle 类型

自动生成的plist中包含了一些默认设置

在手机的设置中就会生成对应的设置项

按需求修改

经过一番修改 变成了这个样子

设置界面也变了样子

读取设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ReadSettingsBundle {
public func readSettings() {
let defaults = UserDefaults.standard
print("用户名:\(defaults.string(forKey: "user_name") ?? "")")
print("密码是:\(defaults.string(forKey: "password") ?? "")")
print("选项是: \(defaults.string(forKey: "options") ?? "")")
print("音乐:\(defaults.bool(forKey: "music_switch"))")
print("音量:\(defaults.float(forKey: "music_value"))")
}
}

//输出
用户名:aaa
密码是:1234
选项是: 1
音乐:true
音量:0.5998252

0x03、keychain

特点

保存的数据不会随着应用卸载而被删除、一般保存密码、登陆Token、UDID等标识。

使用

首先在Capabilities中打开Keychain

直接给出苹果的样例代码

使用起来也是很麻烦 通常使用第三方库

Swift第三方库 KeychainAccessOC第三方库 SAMKeychain

0x04、Plist

特点

​ 可以直接将集合中的数据保存到plist文件中,同样不支持自定义对象。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//保存数据
public func saveToPlist() {
let dict: NSDictionary = [
"error_code": 0,
"message": "成功",
"list" : [
["name": "张三", "age": 18],
["name": "李四", "age": 20]
]
]

let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!

let path = URL(fileURLWithPath: documentPath).appendingPathComponent("dict").appendingPathExtension("plist").path

print("Docunemt Path: \(path)")

dict.write(toFile: path, atomically: true)
}

Plist文件长这个样子

保存的Plist文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//读取Plist文件
public func readPlist() {
let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!

let path = URL(fileURLWithPath: documentPath).appendingPathComponent("dict").appendingPathExtension("plist").path

print("Docunemt Path: \(path)")

if let dict = NSDictionary(contentsOfFile: path) {
print(dict)
}
}

//输出
{
"error_code" = 0;
list = (
{
age = 18;
name = "\U5f20\U4e09";
},
{
age = 20;
name = "\U674e\U56db";
}
);
message = "\U6210\U529f";
}

0x05、归档

特点:

  1. 数据存储archive文件中
  2. 可以将自定义的类型归档

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//自定义一个类
class Persion: NSObject, NSCoding {

var name: String = "长泽雅美"
var age: Int = 32
var sex: String = "女"

override init() {
super.init()
}

func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(age, forKey: "age")
aCoder.encode(sex, forKey: "sex")
}

required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as! String
age = aDecoder.decodeInteger(forKey: "age")
sex = aDecoder.decodeObject(forKey: "sex") as! String

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//NSKeyedArchiver
//写入

let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let path = URL(fileURLWithPath: documentPath).appendingPathComponent("person")
print(path)
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: Persion(), requiringSecureCoding: false)
try data.write(to: path)
} catch {
print("归档失败了 \(error)")
}

//NSKeyedUnarchiver
//读取
do {
if let data = FileManager.default.contents(atPath: path.path),let person = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Persion {
print("名字是:\(person.name)")
}
} catch {
print("解档失败了 \(error)")
}

//名字是:长泽雅美

0x06、文件

可以任意保存数据到硬盘上,并且可以移动、拷贝、删除等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//The methods of the shared NSFileManager object can be called from multiple threads safely. 
//However, if you use a delegate to receive notifications about the status of move, copy, remove, and link operations,
// you should create a unique instance of the file manager object, assign your delegate to that object,
//and use that file manager to initiate your operations.

class File {

//创建文件
public func createFile() {
let imageUrl = Bundle.main.url(forResource: "peppa", withExtension: "jpeg")!
let documentPath = URL.documentUrl.appendingPathComponent("Peppa").appendingPathExtension("jpg").path
print(documentPath)
DispatchQueue.global().async {
do {
let data = try Data(contentsOf: imageUrl)
if FileManager.default.createFile(atPath: documentPath, contents: data, attributes: nil) {
print("创建文件成功")
}
} catch {
print("失败了 \(error)")
}
}
}

//创建文件夹
public func createDirectory() {
let directoryUrl = URL.documentUrl.appendingPathComponent("父文件夹", isDirectory: true).appendingPathComponent("子文件夹", isDirectory: true)
print("文件夹路径: \(directoryUrl)")
DispatchQueue.global().async {
do {
try FileManager.default.createDirectory(at: directoryUrl, withIntermediateDirectories: true, attributes: nil)
print("文件夹创建成功")
} catch {
print("失败了 \(error)")
}
}
}

//读取文件夹
public func readDirectory() {
DispatchQueue.global().async {
if let enumerator = FileManager.default.enumerator(atPath: String.documentPath) {
for case let path as String in enumerator {
print(path)
}
}
}
}

// 删除文件夹
public func deleteItem() {
let directoryUrl = URL.documentUrl.appendingPathComponent("父文件夹", isDirectory: true)
DispatchQueue.global().async {
do {
try FileManager.default.removeItem(at: directoryUrl)
print("删除成功")
} catch {
print("失败了 \(error)")
}
}
}
}

0x07、sqlite3

特点

  1. 使用C语言编写、因此使用不太方便
  2. 数据存储在.db3数据库中
  3. 可以存储大量数据,并且存储、检索比较高效

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
class SQLiteDB {
private var db:OpaquePointer? = nil
private lazy var fm = {
return FileManager.default
}()


private lazy var dbPath = { [weak self] () -> String in
let documentPath = self?.fm.urls(for: .documentDirectory, in: .userDomainMask).first!
let dbPath = documentPath!.appendingPathComponent("data").appendingPathExtension("db").path
return dbPath
}()

public func createDB() -> Bool {
if !fm.isExecutableFile(atPath: dbPath) {
if fm.createFile(atPath: dbPath, contents: nil, attributes: nil) {
return openDB()
} else {
print("数据库创建失败")
return false
}
} else {
return openDB()
}
}

private func openDB() -> Bool {
let cPath = dbPath.cString(using: .utf8)!
let error = sqlite3_open(cPath, &db)

if error != SQLITE_OK {
print("数据库打开失败")
sqlite3_close(db)
return false
}

print("数据库打开成功")
return true
}

public func createTable() {
assert(db != nil, "数据库没有正确打开")

let createTableSql = "create table if not exists t_user(uid integer primary key AUTOINCREMENT,uname varchar(20),mobile varchar(20))"

let cSql = createTableSql.cString(using: .utf8)
var errMsg: UnsafeMutablePointer<Int8>?
let result = sqlite3_exec(db, cSql!, nil, nil, &errMsg)

if result != SQLITE_OK {
sqlite3_free(errMsg)
if let error = String(validatingUTF8: sqlite3_errmsg(db)) {
print("SQLiteDB - failed to prepare SQL: \(createTableSql), Error: \(error)")
}
return
}
sqlite3_free(errMsg)

print("创建成功")
}

public func insert() {
assert(db != nil, "数据库没有正确打开")

print("插入数据开始")
beginTransaction()

var stmt: OpaquePointer?
var sql = ""
var cSql:[CChar]?
for i in 0...100 {
sql = "insert into t_user(uname,mobile) values('user\(i)','13\(i)')"
cSql = sql.cString(using: .utf8)
if sqlite3_prepare_v2(db, cSql!, -1, &stmt, nil) != SQLITE_OK {
sqlite3_finalize(stmt)
print("执行失败")
rollback()
break
} else {
if sqlite3_step(stmt) != SQLITE_DONE {
sqlite3_finalize(stmt)
print("执行失败")
rollback()
break
}
}
}
sqlite3_finalize(stmt)

commitTransaction()
print("插入数据成功")

}

//开启事物
private func beginTransaction() {
var errMsg: UnsafeMutablePointer<Int8>?
//开启事物
//SQL国际标准使用START TRANSACTION开始一个事务(也可以用方言命令BEGIN)。COMMIT语句使事务成功完成。ROLLBACK语句结束事务,放弃从BEGIN TRANSACTION开始的一切变更。若autocommit被START TRANSACTION的使用禁止,在事务结束时autocommit会重新激活。
let result = sqlite3_exec(db, "BEGIN", nil, nil, &errMsg)

if result != SQLITE_OK {
sqlite3_free(errMsg)
print("启动事物失败")
return
}

sqlite3_free(errMsg)
print("启动事物成功")
}

//提交事物
private func commitTransaction() {
var errMsg: UnsafeMutablePointer<Int8>?
if sqlite3_exec(db, "COMMIT", nil, nil, &errMsg) != SQLITE_OK {
sqlite3_free(errMsg)
print("提交事物失败")
rollback()
}

sqlite3_free(errMsg)
print("提交事物成功")
}

//回滚事物
private func rollback() {
var errMsg: UnsafeMutablePointer<Int8>?
if sqlite3_exec(db, "ROLLBACK", nil, nil, &errMsg) == SQLITE_OK {
print("回滚成功")
} else {
print("回滚失败")
}
sqlite3_free(errMsg)
}

public func remove(ids:[Int]) {
if ids.count <= 0 {
return
}

print("删除数据开始")

beginTransaction()

var errMsg: UnsafeMutablePointer<Int8>?
var sql = ""
var cSql:[CChar]?
for id in ids {
sql = "delete from t_user where uid = \(id)"
cSql = sql.cString(using: .utf8)
if sqlite3_exec(db, cSql, nil, nil, &errMsg) != SQLITE_OK {
sqlite3_free(errMsg)
if let error = String(validatingUTF8: sqlite3_errmsg(db)) {
print("SQLiteDB - 执行SQL: \(sql), Error: \(error)")
}
rollback()
return
}
}

commitTransaction()
sqlite3_free(errMsg)

print("删除数据成功")
}

public func update(id:Int) {

print("开始更新")

let sql = "update t_user set uname = '长泽雅美' where uid = \(id)"
var errMsg: UnsafeMutablePointer<Int8>?
if sqlite3_exec(db, sql.cString(using: .utf8), nil, nil, &errMsg) != SQLITE_OK {
sqlite3_free(errMsg)
if let error = String(validatingUTF8: sqlite3_errmsg(db)) {
print("SQLiteDB - 执行SQL: \(sql), Error: \(error)")
}
return
}

sqlite3_free(errMsg)
print("更新结束")

}

public func query(lessThan: Int) {
print("执行查询")
let sql = "select * from t_user where uid < ?"
var stmt: OpaquePointer?
if sqlite3_prepare_v2(db, sql.cString(using: .utf8), -1, &stmt, nil) != SQLITE_OK {
sqlite3_finalize(stmt)
if let error = String(validatingUTF8: sqlite3_errmsg(db)) {
print("准备SQL: \(sql), Error: \(error)")
}
return
}
print("查询编译成功")

if sqlite3_bind_int(stmt, 1, CInt(lessThan)) != SQLITE_OK {
sqlite3_finalize(stmt)
if let error = String(validatingUTF8:sqlite3_errmsg(self.db)) {
let msg = "绑定参数 SQL: \(sql), uid: \(lessThan), 位置: \(1) Error: \(error)"
NSLog(msg)
}
return
}

print("查询执行成功")

//循环取得每一行
while sqlite3_step(stmt) == SQLITE_ROW {
let columnCount = sqlite3_column_count(stmt)
let uid = sqlite3_column_int(stmt, 0)
let uname = String(cString: sqlite3_column_text(stmt, 1))
let mobile = String(cString: sqlite3_column_text(stmt, 2))
print("每条记录有\(columnCount)列,第一列uid:\(uid), 第二列uname:\(uname),第三列mobile:\(mobile)")
}

sqlite3_finalize(stmt)
print("查询结束")
}

public func dropTable() {
let sql = "drop table if exists t_user"
var errMsg: UnsafeMutablePointer<Int8>?
if sqlite3_exec(db, sql.cString(using: .utf8), nil, nil, &errMsg) != SQLITE_OK {
sqlite3_free(errMsg)
if let error = String(validatingUTF8: sqlite3_errmsg(db)) {
print("删除表失败 SQL: \(sql), Error: \(error)")
}
return
}


print("删除表成功")
}

public func closeDB() {
if db != nil {
let result = sqlite3_close_v2(db)

if result != SQLITE_OK {
if let error = String(validatingUTF8: sqlite3_errmsg(db)) {
print("关闭数据库失败 Error: \(error)")
}
} else {
print("关闭数据库成功")
}
}

}
}

执行结果

直接使用sqlite库很麻烦 一般使用第三方的封装

Swift第三方库 SQLite.swiftOC第三方库 FMDB

0x08、Core Data

特点

  1. 是一个面向对象的持久化框架
  2. 持久化层可以选择SQLiteXML、甚至内存

使用

CoreData 是一个博大精深的技术,不要妄想几天之内可以用的很溜。
CoreData 是一个博大精深的技术,不要妄想几天之内可以用的很溜。
CoreData 是一个博大精深的技术,不要妄想几天之内可以用的很溜。

如果没有足够的时间和精力去接入 Core Data。 那选型的时候应当慎重考虑。

这里给出一个基础的介绍文章 Swift实现CoreData存储数据

后记

本文Demo:https://github.com/zhaofucheng1129/Swift_Persistence