Outbox架構:為了不漏掉資訊而設計
本頁面由日文原文機器翻譯而成。 閱讀日文原文
2026年2月13日
課題:即使傳送即清除,也不會漏掉資訊
在SimpleMemo中,使用者點選傳送按鈕的瞬間UI被清除,可以馬上開始寫下一個備忘錄。 這就是Captio風格的根基UX。
但是現實的網路有300ms~2秒的延遲,也會失敗,可能本來就離線。 很多應用程式在等待網路響應後才清除UI。 雖然安全,但是UX不好。 相反,如果立即清除的話,有丟失資訊的風險。
Outbox模式同時解決了這個相反的要求。 在傳送點選的瞬間將資訊永久儲存在本地的加密儲存(Outbox)中,立即清除UI,在後臺進行網路傳送。 傳送成功後從Outbox中刪除,失敗後自動重試。 因此將未傳送的資訊留在終端上並重新傳送,體感延遲接近零。
Outbox圖案的設計流程
以下是從點選傳送按鈕到傳送資訊的全部流程。
- 傳送點選→首先Outbox對AES-GCM進行加密並永久儲存
使用者點選傳送按鈕的瞬間,資訊被AES-GCM 256bit加密,永久儲存到裝置的本地儲存中。 在這個步驟完成之前,不會進行UI清除。 也就是說,Outbox儲存成為防止溢位的第一個防波堤。 - 立即清除UI(使用者可以開始寫下一個備忘錄)
Outbox儲存成功時,清除文字檢視,播放傳送動畫。 從使用者的角度來看,與“傳送了”是同義。 完全沒有等待網路響應。 Send-to-Reset的目標是感覺不到等待的短(包括動畫0.25秒)。 - 在後臺透過Relay API傳送郵件與
UI清除並行,SendManager向Cloudflare Workers上的Relay API傳送HTTP請求。 這個處理不阻止主執行緒的完全非同步。 - 成功後從Outbox中刪除
Relay 從API收到200響應後,從Outbox安全地刪除對應資訊。 這樣資訊的生命週期就完成了。 - 失敗後自動重新傳送
網路錯誤或伺服器錯誤時,留待傳送,線路恢復時、開啟應用程式時、後臺處理(BGTaskScheduler。 執行的時機由iOS決定)為契機重新傳送。 自動重新傳送有次數上限。 - 離線時用NWPathMonitor檢測線路恢復並自動重新傳送
傳送時離線時,Network.framework的NWPathMonitor監視線路狀態,檢測到Wi-Fi和蜂窩網路恢復時,自動重新傳送Outbox的未傳送資訊(和上面的5一樣,自動重新傳送有次數上限)。
SendManager的程式碼
SendManager的send()函式是Outbox模式的入口點。 首先儲存在Outbox,沒有網路連線就排隊,有連線就立即執行傳送。
// 保存完了と送信結果を分ける説明用の簡略API
@MainActor
func send(message: String, onSaved: () -> Void,
completion: @escaping (SendResult) -> Void) {
// まずOutboxに保存(クラッシュや圏外で失わないように)
let outboxMessage: OutboxMessage
do {
outboxMessage = try OutboxManager.shared.add(body: message)
} catch {
DispatchQueue.main.async { completion(.failure(error)) }
return
}
// 同期保存に成功した場合だけ、メインスレッドでUIへ通知する
onSaved()
// ネットワーク接続がない場合はキューに入れたことを通知
guard NetworkMonitor.shared.isConnected else {
BackgroundTaskManager.shared.scheduleRetryTask()
DispatchQueue.main.async { completion(.queued) }
return
}
// 送信処理を実行
performSend(message: message, outboxId: outboxMessage.id, ...)
}
重要的一點是,OutboxManager.shared.add(body:)同步寫入永久儲存。 這個呼叫成功的時候,資訊會安全地儲存在終端上。 無論網路傳送結果如何,資訊都會一直留在Outbox。
AES-GCM加密的實施
Outbox中儲存的資訊是使用者的私人備忘錄。 雖說是本地儲存,但不能用平文儲存。 使用Apple CryptoKit的AES-GCM,在加密方面完全不透過外部SDK的實現。
- Apple CryptoKit — iOS 13以後可以使用。 加密處理不透過外部SDK(僅限Apple純正)。
- 256-bit對稱鍵 — 儲存在Keychain中。
- 認證加密 — AES-GCM同時保證資料的機密性和完整性。 被篡改的資料在解密時會被檢測到。
enum OutboxEncryption {
private static var encryptionKey: SymmetricKey {
if let existingKey = KeychainHelper.load(key: "outbox_encryption_key") {
return SymmetricKey(data: existingKey)
}
let newKey = SymmetricKey(size: .bits256)
let keyData = newKey.withUnsafeBytes { Data($0) }
KeychainHelper.save(key: "outbox_encryption_key", data: keyData)
return newKey
}
static func encrypt(_ plainText: String) throws -> Data {
guard let data = plainText.data(using: .utf8) else {
throw OutboxError.encryptionFailed
}
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
guard let combined = sealedBox.combined else {
throw OutboxError.encryptionFailed
}
return combined
}
}
鑰匙在第一次需要時自動製作,並儲存在Keychain中。 儲存的設定是“只有這個終端・第一次解鎖後才能讀取”(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly),不能從其他應用程式讀取,也不會從備份轉移到其他終端。 重啟後解鎖一次,從鎖定中的後臺處理中也能讀取。 另外,上面的程式碼例子是寫報道時的簡略版,現在的程式碼在鑰匙鏈讀取失敗時,不會處理“沒有鑰匙”來製作新的鑰匙。
資料路徑只由Apple純正構成的意義
Outbox架構中使用的框架都是Apple原生的。
- CryptoKit — AES-GCM加密。 不需要第三方的加密庫。
- Network.framework — 透過NWPathMonitor監控線路狀態。 不需要Reachability庫。
- BackgroundTasks — BGTaskScheduler的後臺重試。
等待傳送的路徑中沒有外部庫,有以下含義。
- 不受外部更新的影響 — 等待傳送的處理不會因為第三方庫的更新或提供終止而停止執行。
- 安全 — 接觸等待傳送的備忘錄正文的處理(加密、儲存、傳送)中,第三方的程式碼無法進入。
另外,整個應用程式也使用外部的SDK(Google登入、Firebase、AppsFlyer)(參考隱私第一設計)。 這裡所說的“只有Apple純正”是關於等待傳送的資料路徑。
UI清除和網路傳送的分離
Outbox模式的真正價值在於UI層和網路層的完全分離。 看看傳送按鈕的點選處理程式吧。
@objc private func sendButtonTapped() {
PerformanceLogger.shared.beginSendToReset()
let message = textView.text ?? ""
isSending = true
sendBarButton.isEnabled = false
SendManager.shared.send(message: message, onSaved: { [weak self] in
// 端末への保存成功後にだけ消す。ネットワーク応答は待たない。
self?.performSendAnimation { [weak self] in
guard let self = self else { return }
// アニメーション中に編集された入力は残す。
if self.textView.text == message {
self.clearTextView()
}
PerformanceLogger.shared.endSendToReset()
}
}) { [weak self] result in
self?.isSending = false
self?.updateSendButtonState()
switch result {
case .success:
PerformanceLogger.shared.logEvent(name: "SendSuccess")
case .failure(let error):
self?.handleSendError(error)
case .queued:
self?.showQueuedFeedback()
}
}
}
在這個簡化範例中,只有成功儲存到裝置後才呼叫 onSaved,接著播放動畫並清空輸入。儲存失敗時保留輸入。completion 是另一個通知,用於回報傳送結果或已加入佇列;清空介面不等待網路回應。
Send-to-Reset的目標是感覺不到等待的短。 包括動畫也是0.25秒。 使用者在點選傳送按鈕後,就可以開始寫下一個備忘錄。 這就是Captio風格的UX。
常見問題
問。 離線傳送的備忘錄會怎麼樣?
Outbox加密儲存,線路恢復(NWPathMonitor檢測)・開啟應用程式時・以後臺處理為契機,自動重新傳送。 自動重新傳送有次數上限,達到上限的備忘錄在“履歷”中長按並選擇“重新傳送”。
問。 關閉應用程式也不會丟失備忘錄嗎?
Outbox是永久儲存。 即使關閉應用程式,重啟iPhone也會保留。 下次啟動應用程式時,或者透過後臺任務自動重試。
問。 為什麼選擇AES-GCM?
Apple CryptoKit原生支援,透過認證加密(Authenticated Encryption)也可以檢測篡改。 處理加密不需要外部庫這一點也是優點。
問。 傳送失敗時的重試次數是多少?
自動重新傳送。 契機是,線路恢復時,開啟應用程式時,後臺處理(BGTaskScheduler。 執行的時間由iOS決定)。 無法傳送的備忘錄不會從等待傳送中刪除,而是重新傳送,但自動重新傳送有次數上限。
相關報道
參考文獻
- Apple Developer — AES.GCM (CryptoKit) — Outbox用於資料加密的帶有AES-GCM認證的加密的官方參考
- Apple Developer — NWPathMonitor (Network Framework) — 用於離線檢測和網路狀態監視
- Apple Developer — BGTaskScheduler — 用於後臺的再傳送處理排程
- Chris Richardson — Pattern: Transactional outbox (microservices.io) — 交易Outbox模式的解說(傳送的訊息在同一處理中寫在等待傳送的表中,其他處理傳送)