DevToysMac 数据持久化与意外退出恢复方案深度剖析
当 DevToysMac 在处理关键数据时遭遇异常退出,如何保障已编辑的内容不丢失?答案在于内建的崩溃恢复体系,该体系由 RestorableState 与 RestorableData 两个核心模块协同实现,分别位于 CoreUtil/CoreUtil/Class/RestorableState.swift 和 RestorableData.swift。
声明式持久化:属性包装器的力量
开发者无需手动编写保存与恢复逻辑,只需通过 @RestorableState 和 @RestorableData 标记需要保护的数据。这种属性包装器(Property Wrapper)方式将持久化细节完全封装,实现了透明化的自动存储。
// 用于简单配置值的包装器
@propertyWrapper
public struct PersistentState<Value: RawRepresentable> {
private let key: String
private let store: UserDefaults
private var currentValue: Value
public var wrappedValue: Value {
get { currentValue }
set {
currentValue = newValue
store.set(newValue.rawValue, forKey: key)
}
}
public init(wrappedValue initial: Value, _ storageKey: String, defaults: UserDefaults = .standard) {
self.key = storageKey
self.store = defaults
self.currentValue = (defaults.object(forKey: key) as? Value.RawValue).flatMap(Value.init) ?? initial
}
}
上面的 PersistentState 在初始化时尝试从 UserDefaults 读取已存值,若读取失败则使用预设的初始值;写操作会立即同步到标准化存储。
双层存储:简单状态与复杂数据分离
| 包装器 | 数据类型 | 存储位置 | 核心文件 |
|---|---|---|---|
RestorableState | 布尔、数值等简单配置 | UserDefaults | RestorableState.swift |
RestorableData | 文本、会话等复杂对象 | 临时目录下的 JSON 文件 | RestorableData.swift |
简单状态直接写入 UserDefaults,适合偏好开关、窗口尺寸等;大型编辑内容或嵌套结构则通过 Codable 序列化为独立 JSON 文件保存。
实时保存:Combine 驱动的自动写入
数据变更与磁盘同步之间几乎没有延迟。当包装器的值被修改时,写入操作立即触发:
public var wrappedValue: Value {
get { internalValue }
set {
internalValue = newValue
let url = Self.restorableDataDirectory.appendingPathComponent("\(identifier).json")
do {
try JSONEncoder().encode(newValue).write(to: url)
} catch {
// 静默失败,但不影响用户操作
}
}
}
存储目录构建于应用专属临时路径,系统会自动创建必要的文件夹结构:
static let restorableDataDirectory: URL = {
let temp = FileManager.default.temporaryDirectory
let folder = temp.appendingPathComponent("StateRecovery")
try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
return folder
}()
每个 RestorableData 实例对应一个以标识符命名的 JSON 文件,例如 editor_content.json。
崩溃恢复流程
应用重启时,包装器会自动尝试从持久化存储中恢复数据。以下是简化版的初始化逻辑:
public init(wrappedValue fallback: Value, _ identifier: String) {
self.identifier = identifier
let fileURL = Self.restorableDataDirectory.appendingPathComponent("\(identifier).json")
var restored: Value?
if let data = try? Data(contentsOf: fileURL) {
restored = try? JSONDecoder().decode(Value.self, from: data)
}
self.internalValue = restored ?? fallback
}
如果 JSON 文件损坏或不存在,代码会优雅降级,使用传入的备用值继续运行,确保工具链的鲁棒性。
恢复的流程图如下:
graph TD
A[应用启动] --> B{存有恢复数据?}
B -->|是| C[从临时目录读档]
C --> D{解码成功?}
D -->|是| E[恢复对应状态]
D -->|否| F[使用初始缺省值]
B -->|否| F
(该流程描述状态恢复的决策路径)
二进制内容的引用存储
对于图片等较大二进制对象,DevToysMac 使用 ImageAssetContainer 实现引用分离。序列化时只保存标识符,实际数据写入独立文件:
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(assetID)
let imageURL = Self.assetStorageURL.appendingPathComponent(assetID)
let imageData = image.tiffRepresentation
try imageData?.write(to: imageURL)
}
解码时反向操作,通过标识符定位并加载文件。这种方式避免了巨型 Base64 字符串嵌入 JSON,显著提升了序列化性能和存储效率。
常见应用场景
文本编辑器内容保护
class EditorViewModel {
@RestorableData("text_editor_content")
var currentContent: String = ""
func updateContent(newText: String) {
currentContent = newText // 赋值即触发持久化
}
}
偏好设置自动记忆
class Preferences {
@RestorableState("ui_sound_enabled")
var soundEnabled: Bool = true
@RestorableState("sidebar_width_pts")
var sidebarWidth: Double = 260.0
}
异常安全与临时文件清理
所有写操作均包含容错处理,确保 I/O 错误不会破坏应用交互流程。临时目录在应用卸载时会被系统自动回收,不会产生遗留数据。同时,恢复文件时也采用防御式解码,最大程度保障可用性。