iOS 开发手册IOS DEVELOPMENT

CHAPTER 06本地存储 · PERSISTENCE

数据持久化

App 关掉再打开,数据还在——这就是持久化。从最简单的 UserDefaults 到对象图 Core Data / SwiftData,再到安全存储 Keychain,每种方案都有自己的主场。

开始阅读 难度进阶 先修Swift 基础 约3小时
06.1

持久化方式全景

iOS 没有"唯一正确"的存储。按数据量、结构复杂度、查询需求与安全性选工具,是工程判断力的一部分。

UserDefaults 小键值 文件 文档/缓存 SQLite 结构化查询 Core Data 对象图 Keychain 安全
从左到右:从简单键值到对象图,Keychain 独立承担安全存储职责。复杂度与能力同步上升。
06.2

UserDefaults 与文件

UserDefaults 适合小体积偏好设置(开关、主题、登录态标记)。大对象或二进制应写文件,用 FileManager 落到 Documents / Caches 目录。

// 偏好:小而简单的键值
UserDefaults.standard.set(true, forKey: "darkMode")
let dark = UserDefaults.standard.bool(forKey: "darkMode")

// 文件:可序列化模型落地
let url = FileManager.default
    .urls(for: .documentDirectory, in: .userDomainMask)[0]
    .appendingPathComponent("profile.json")
try JSONEncoder().encode(profile).write(to: url)
let loaded = try JSONDecoder().decode(Profile.self, from: Data(contentsOf: url))
别把什么塞进 UserDefaults

UserDefaults 会整体载入内存,存大数组/图片会拖慢启动。它只适合"小且常用"的偏好。敏感信息(令牌)更不该放这,必须进 Keychain。

06.3

SQLite / FMDB

当关系复杂、查询频繁、需要事务时,直接上 SQLite。原生的 sqlite3 C API 偏底层,常用 FMDB(Objective-C/Swift 封装)或 GRDB 提升开发效率。

import FMDB

let db = FMDatabase(path: dbPath)
db.open()
db.executeUpdate(
    "INSERT INTO users (name, age) VALUES (?, ?)",
    values: ["林可", 28]
)
if let rs = db.executeQuery("SELECT * FROM users WHERE age > ?", values: [18]) {
    while rs.next() { print(rs.string(forColumn: "name") ?? "") }
}
db.close()
优先参数化查询

永远用占位符 ? 传参,不要字符串拼接 SQL——既防 SQL 注入,又避免引号转义坑。复杂项目推荐 GRDB(带 Swift 查询 DSL 与迁移管理)。

06.4

Core Data 对象图

Core Data 不是数据库,而是"对象图管理框架"——它用 SQLite 作默认后端,但给你的是带关系的 Swift 对象、变更追踪与懒加载。适合中大型结构化数据。

let ctx = persistentContainer.viewContext
let user = User(context: ctx)
user.name = "林可"
try ctx.save()                       // 写入底层存储

let req = User.fetchRequest()
req.predicate = NSPredicate(format: "age > %@", 18)
let results = try ctx.fetch(req)
上下文线程

View context 在主线程;重量写入应放到 backgroundContext,否则会卡 UI。Core Data 的并发规则(受限/私有队列)用错会崩溃,建议封装仓库层屏蔽细节。

06.5

SwiftData

iOS 17 推出的 SwiftData 是 Core Data 的 Swift 原生封装:用 @Model 宏把普通类声明成可持久化实体,查询用 Swift 语法,大幅降低门槛。

import SwiftData

@Model
final class Trip {
    var title: String
    var startDate: Date
    @Relationship(deleteRule: .cascade) var stops: [Stop] = []
    init(title: String) { self.title = title; self.startDate = .now }
}

// 查询用 #Predicate 宏,纯 Swift 表达
let descriptor = FetchDescriptor(
    predicate: #Predicate { $0.startDate > Date.distantPast }
)
let trips = try modelContext.fetch(descriptor)
SwiftData vs Core Data

新项目(iOS 17+)优先 SwiftData,代码量显著降低;需要兼容老系统或超复杂迁移时,Core Data 仍更成熟可控。两者可共存于同一持久化栈。

06.6

Keychain 安全存储

密码、令牌、密钥等敏感数据必须进 Keychain——它加密存储、与 App 沙盒隔离、可配置跨 App 重装保留。绝不要放 UserDefaults 或明文文件。

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "authToken",
    kSecValueData as String: token.data(using: .utf8)!
]
SecItemAdd(query as CFDictionary, nil)          // 写入

// 读取
var item: CFTypeRef?
SecItemCopyMatching(query as CFDictionary, &item)
用封装库省心

原生 SecItem API 繁琐易错,生产推荐 KeychainAccess 等开源封装,一行读写、自动处理错误与重复项。

06.7

选型对照

方案适合不适合
UserDefaults小偏好、开关、标记大对象、敏感信息
文件文档、导出、缓存、二进制需要复杂关联查询
SQLite复杂关系、高频查询、事务不想手写/维护 SQL
Core Data中大型对象图、变更追踪极简单存储(过度设计)
SwiftDataiOS 17+ 结构化数据需兼容老系统
Keychain密钥、令牌、凭证普通业务数据
06.8

常见陷阱与 FAQ

App 升级后旧数据怎么保留?

SQLite/Core Data/SwiftData 都在沙盒内,升级不会清掉 Documents。关键是做"版本迁移":Core Data 用轻量/自定义迁移,SwiftData 用 SchemaMigrationPlan,SQLite 用 ALTER TABLE 脚本。忽略迁移会导致启动崩溃。

Documents 和 Caches 目录有区别吗?

有。Documents 会被 iCloud 备份、用户可见;Caches 不备份、空间不足时系统可清除。可重新下载的内容放 Caches,用户数据放 Documents。

主线程做 Core Data 保存会怎样?

大批量保存会阻塞 UI、掉帧甚至看门狗强杀。用 performBackgroundTask 或私有队列上下文在后台提交。

06.9

实战练习与资源

延伸资源