持久化方式全景
iOS 没有"唯一正确"的存储。按数据量、结构复杂度、查询需求与安全性选工具,是工程判断力的一部分。
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 会整体载入内存,存大数组/图片会拖慢启动。它只适合"小且常用"的偏好。敏感信息(令牌)更不该放这,必须进 Keychain。
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 与迁移管理)。
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 的并发规则(受限/私有队列)用错会崩溃,建议封装仓库层屏蔽细节。
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)
新项目(iOS 17+)优先 SwiftData,代码量显著降低;需要兼容老系统或超复杂迁移时,Core Data 仍更成熟可控。两者可共存于同一持久化栈。
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 等开源封装,一行读写、自动处理错误与重复项。
选型对照
| 方案 | 适合 | 不适合 |
|---|---|---|
| UserDefaults | 小偏好、开关、标记 | 大对象、敏感信息 |
| 文件 | 文档、导出、缓存、二进制 | 需要复杂关联查询 |
| SQLite | 复杂关系、高频查询、事务 | 不想手写/维护 SQL |
| Core Data | 中大型对象图、变更追踪 | 极简单存储(过度设计) |
| SwiftData | iOS 17+ 结构化数据 | 需兼容老系统 |
| Keychain | 密钥、令牌、凭证 | 普通业务数据 |
常见陷阱与 FAQ
SQLite/Core Data/SwiftData 都在沙盒内,升级不会清掉 Documents。关键是做"版本迁移":Core Data 用轻量/自定义迁移,SwiftData 用 SchemaMigrationPlan,SQLite 用 ALTER TABLE 脚本。忽略迁移会导致启动崩溃。
有。Documents 会被 iCloud 备份、用户可见;Caches 不备份、空间不足时系统可清除。可重新下载的内容放 Caches,用户数据放 Documents。
大批量保存会阻塞 UI、掉帧甚至看门狗强杀。用 performBackgroundTask 或私有队列上下文在后台提交。