在CoreData中使用持久化歷史跟蹤

語言: CN / TW / HK

theme: juejin highlight: a11y-dark


2022年2月更新:我已經重寫了程式碼,並將其整理成庫 PersistentHistoryTrackingKit 以方便大家使用。

前言

知道持久化歷史跟蹤功能已經有一段時間了,之前簡單地瀏覽過文件但沒有太當回事。一方面關於它的資料不多,學習起來並不容易;另一方面也沒有使用它的特別動力。

在計劃中的【健康筆記3】中,我考慮為App新增Widget或者其他的Extentsion,另外我也打算將WWDC21上介紹的NSCoreDataCoreSpotlightDelegate用到App的新版本中。為此就不得不認真地瞭解該如何使用持久化歷史跟蹤功能了。

什麼是持久化歷史跟蹤(Persistent History Tracking)

使用持久化歷史跟蹤(Persistent History Tracking)來確定自啟用該項功能以來,儲存(Store)中發生了哪些更改。 —— 蘋果官方文件

在CoreData中,如果你的資料儲存形式是Sqlite(絕大多數的開發者都採用此種方式)且啟用了持久化歷史跟蹤功能,無論資料庫中的資料有了何種變化(刪除、新增、修改等),呼叫此資料庫並註冊了該通知的應用,都會收到一個數據庫有變化的系統提醒。

為什麼要使用它

持久化歷史跟蹤目前主要有以下幾個應用的場景:

  • 在App中,將App的批處理(BatchInsert、BatchUpdate、BatchDelete)業務產生的資料變化合併到當前的檢視上下文(ViewContext)中。

批處理是直接通過協調器(PersistentStoreCoordinator)來操作的,由於該操作並不經過上下文(ManagedObejctContext),因此如果不對其做特別的處理,App並不會及時的將批處理導致的資料變化在當前的檢視上下文中體現出來。在沒有Persistent History Tracking之前,我們必須在每個批處理操作後,使用例如mergeChanegs將變化合併到上下文中。在使用了Persistent History Tracking之後,我們可以將所有的批處理變化統一到一個程式碼段中進行合併處理。

  • 在一個App Group中,當App和App Extension共享一個數據庫檔案,將某個成員在資料庫中做出的修改及時地體現在另一個成員的檢視上下文中。

想象一個場景,你有一個彙總網頁Clips的App,並且提供了一個Safari Extentsion用來在瀏覽網頁的時候,將合適的剪輯儲存下來。在Safari Extension將一個Clip儲存到資料庫中後,將你的App(Safari儲存資料時,該App已經啟動且切換到了後臺)切換到前臺,如果正在顯示Clip列表,最新的(由Safari Extentsion新增)Clip並不會出現在列表中。一旦啟用了Persistent History Tracking,你的App將及時得到資料庫發生變化的通知、並做出響應,使用者便可以在第一時間在列表中看到新新增的Clip。

  • 當使用PersistentCloudKitContainer將你的CoreData資料庫同Cloudkit進行資料同步時。

Persistent History Tracking是實現CoreData同CloudKit資料同步的重要保證。無需開發者自行設定,當你使用PersistentCloudKitContainer作為容器後,CoreData便已經為你的資料庫啟用了Persistent History Tracking功能。不過除非你在自己的程式碼中明確宣告啟用持久化歷史跟蹤,否則所有網路同步的資料變化都並不會通知到你的程式碼,CoreData會在後臺默默地處理好一切。

  • 當使用NSCoreDataCoreSpotlightDelegate時。

在今年的WWDC2021上,蘋果推出了NSCoreDataCoreSpotlightDelegate,可以非常便捷的將CoreData中的資料同Spotlight整合到一起。為了使用該功能,必須為你的資料庫開啟Persistent History Tracking功能。

Persistent History Tracking的工作原理

Persistent History Tracking是通過Sqlite的觸發器來實現的。它在指定的Entity上建立觸發器,該觸發器將記錄所有的資料的變化。這也是持久化歷史跟蹤只能在Sqlite上啟用的原因。

資料變化(Transaction)的記錄是直接在Sqlite端完成的,因此無論該事務是由何種方式(通過上下文還是不經過上下文)產生的,由那個App或Extension產生,都可以事無鉅細的記錄下來。

所有的變化都會被儲存在你的Sqlite資料庫檔案中,蘋果在Sqlite中建立了幾個表,用來記錄了Transaction對應的各類資訊。

image-20210727092416404

蘋果並沒有公開這些表的具體結構,不過我們可以使用Persistent History Tracking提供的API來對其中的資料進行查詢、清除等工作。

如果有興趣也可以自己看看這幾個表的內容,蘋果將資料組織的非常緊湊的。ATRANSACTION中是尚未消除的transaction,ATRANSACTIONSTRING中是author和contextName的字串標識,ACHANGE是變化的資料,以上資料最終轉換成對應的ManagedObjectID。

Transaction將按照產生順序被自動記錄。我們可以檢索特定時間後發生的所有更改。你可以通過多種表達方式來確定這個時間點:

  • 基於令牌(Token)
  • 基於時間戳(Timestamp)
  • 基於交易本身(Transaction)

一個基本的Persistent History Tracking處理流程如下:

  1. 響應Persistent History Tracking產生的NSPersistentStoreRemoteChange通知
  2. 檢查從上次處理的時間戳後是否仍有需要處理的Transaction
  3. 將需要處理的Transaction合併到當前的檢視上下文中
  4. 記錄最後處理的Transaction時間戳
  5. 擇機刪除已經被合併的Transaction

App Groups

在繼續聊Persisten History Tracking之前,我們先介紹一下App Groups。

由於蘋果對App採取了嚴格的沙盒機制,因此每個App,Extension都有其自己的儲存空間。它們只能讀取自己沙盒檔案空間的內容。如果我們想讓不同的App,或者在App和Extension之間共享資料的話,在App Groups出現之前只能通過一些第三方庫來進行簡單的資料交換。

為了解決這個問題,蘋果推出了自己的解決方案App Groups。App Group讓不同的App或者App&App Extension之間可以通過兩種方式來共享資料(必須是同一個開發者賬戶):

  • UserDefauls
  • Group URL(Group 中每個成員都可以訪問的儲存空間)

絕大多數的Persistent History Tracking應用場合,都是發生在啟用了App Group的情況下。因此瞭解如何建立App Grups、如何訪問Group共享的UserDefaults、如何讀取Group URL中的檔案非常有必要。

讓App加入App Groups

在專案導航欄中,選擇需要加入Group的Target,在Signing&Capabilities中,點選+,新增App Group功能。

image-20210726193034435

在App Groups中選擇或者建立group

image-20210726193200091

只有在Team設定的情況下,Group才能被正確的新增。

App Group Container ID必須以group.開始,後面通常會使用逆向域名的方式。

如果你有開發者賬號,可以在App ID下加入App Groups

image-20210726193614636

其他的App或者App Extension也都按照同樣的方式,指定到同一個App Group中。

建立可在Group中共享的UserDefaults

swift public extension UserDefaults { /// 用於app group的userDefaults,在此處設定的內容可以被app group中的成員使用 static let appGroup = UserDefaults(suiteName: "group.com.fatbobman.healthnote")! }

suitName是你在前面建立的App Group Container ID

在Group中的App程式碼中,使用如下程式碼建立的UserDefaults資料,將被Group中所有的成員共享,每個成員都可以對其進行讀寫操作

swift let userDefaults = UserDefaults.appGroup userDefaults.set("hello world", forKey: "shareString")

獲取Group Container URL

swift let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.fatbobman.healthnote")!

對這個URL進行操作和對於App自己沙盒中的URL操作完全一樣。Group中的所有成員都可以在該資料夾中對檔案進行讀寫。

接下來的程式碼都假設App是在一個App Group中,並且通過UserDefaults和Container URL來進行資料共享。

啟用持久化歷史跟蹤

啟用Persistent History Tracking功能非常簡單,我們只需要對NSPersistentStoreDescription`進行設定即可。

以下是在Xcode生成的CoreData模版Persistence.swift中啟用的例子:

```swift init(inMemory: Bool = false) { container = NSPersistentContainer(name: "PersistentTrackBlog") if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") }

    // 新增如下程式碼:
    let desc = container.persistentStoreDescriptions.first!
    // 如果不指定 desc.url的話,預設的URL當前App的Application Support目錄
    // FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
    // 在該Description上啟用Persistent History Tracking
    desc.setOption(true as NSNumber,
                   forKey: NSPersistentHistoryTrackingKey)
    // 接收有關的遠端通知
    desc.setOption(true as NSNumber,
                   forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
     // 對description的設定必須在load之前完成,否則不起作用
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
}

```

如果建立自己的Description,類似的程式碼如下:

```swift let defaultDesc: NSPersistentStoreDescription let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.fatbobman.healthnote")! // 資料庫儲存在App Group Container中,其他的App或者App Extension也可以讀取 defaultDesc.url = groupURL defaultDesc.configuration = "Local" defaultDesc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) defaultDesc.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) container.persistentStoreDescriptions = [defaultDesc]

    container.loadPersistentStores(completionHandler: { _, error in
        if let error = error as NSError? {
        }
    })

```

Persistent History Tracking功能是在description上設定的,因此如果你的CoreData使用了多個Configuration的話,可以只為有需要的configuration啟用該功能。

響應持久化儲存跟蹤遠端通知

```swift final class PersistentHistoryTrackingManager { init(container: NSPersistentContainer, currentActor: AppActor) { self.container = container self.currentActor = currentActor

    // 註冊StoreRemoteChange的響應
    NotificationCenter.default.publisher(
        for: .NSPersistentStoreRemoteChange,
        object: container.persistentStoreCoordinator
    )
    .subscribe(on: queue, options: nil)
    .sink { _ in
        // notification的內容沒有意義,僅起到提示需要處理的作用
        self.processor()
    }
    .store(in: &cancellables)
}

var container: NSPersistentContainer
var currentActor: AppActor
let userDefaults = UserDefaults.appGroup

lazy var backgroundContext = { container.newBackgroundContext() }()

private var cancellables: Set<AnyCancellable> = []
private lazy var queue = {
    DispatchQueue(label: "com.fatbobman.\(self.currentActor.rawValue).processPersistentHistory")
}()

/// 處理persistent history
private func processor() {
    // 在正確的上下文中進行操作,避免影響主執行緒
    backgroundContext.performAndWait {
        // fetcher用來獲取需要處理的transaction
        guard let transactions = try? fetcher() else { return }
        // merger將transaction合併噹噹前的檢視上下文中
        merger(transaction: transactions)
    }
}

} ```

我簡單的解釋一下上面的程式碼。

我們註冊processor來響應NSNotification.Name.NSPersistentStoreRemoteChange

每當你的資料庫中啟用Persistent History Tracking的Entity發生資料變動時,processor都將會被呼叫。在上面的程式碼中,我們完全忽視了notification,因為它本身的內容沒有意義,只是告訴我們資料庫發生了變化,需要processor來處理,具體發生了什麼變化、是否有必要進行處理等都需要通過自己的程式碼來判斷。

所有針對Persistent History Tracking的資料操作都放在 backgroundContext中進行,避免影響主執行緒。

PersistentHistoryTrackingManager是我們處理Persistent History Tracking的核心。在CoreDataStack中(比如上面的persistent.swift),通過在init中新增如下程式碼來處理Persistent History Tracking事件

swift let persistentHistoryTrackingManager : PersistentHistoryTrackingManager init(inMemory: Bool = false) { .... // 標記當前上下文的author名稱 container.viewContext.transactionAuthor = AppActor.mainApp.rawValue persistentHistoryTrackingManager = PersistentHistoryTrackingManager( container: container, currentActor: AppActor.mainApp //當前的成員 ) }

因為App Group中的成員都可以讀寫我們的資料庫,為了在接下來的處理中更好的分辨到底是由那個成員產生的Transaction,我們需要建立一個列舉型別來對每個成員進行標記。

swift enum AppActor:String,CaseIterable{ case mainApp // iOS App case safariExtension //Safari Extension }

按照自己的需求來建立成員的標記。

獲取需要處理的Transaction

在接收到NSPersistentStoreRemoteChange訊息後,我們首先應該將需要處理的Transaction提取出來。就像在前面的工作原理中提到的一樣,API為我們提供了3種不同的方法:

swift open class func fetchHistory(after date: Date) -> Self open class func fetchHistory(after token: NSPersistentHistoryToken?) -> Self open class func fetchHistory(after transaction: NSPersistentHistoryTransaction?) -> Self

獲取指定時間點之後且滿足條件的Transaction

這裡我更推薦使用Timestamp也就是Date來進行處理。主要有兩個原因:

  • 當我們用UserDefaults來儲存最後的記錄時,Date是UserDefaults直接支援的結構,無需進行轉換
  • Timestamp已經被記錄在Transaction中(表ATRANSACTION),可以直接查詢,無需轉換,而Token是需要再度計算的

通過使用下面的程式碼,我們可以獲取當前sqlite資料庫中,所有的Transaction資訊:

swift NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)

這些資訊包括任意來源產生的Transaction,無論這些Transaction是否是當前App所需要的,是否已經被當前App處理過了。

在上面的處理流程中,我們已經介紹過需要通過時間戳來過濾不必要的資訊,並儲存最後處理的Transaction時間戳。我們這些資訊儲存在UserDefaults中,方便App Group的成員來共同處理。

```swift extension UserDefaults { /// 從全部的app actor的最後時間戳中獲取最晚的時間戳 /// 只刪除最晚的時間戳之前的transaction,這樣可以保證其他的appActor /// 都可以正常的獲取未處理的transaction /// 設定了一個7天的界限。即使有的appActor沒有使用(沒有建立userdefauls) /// 也會至多隻保留7天的transaction /// - Parameter appActors: app角色,比如healthnote ,widget /// - Returns: 日期(時間戳), 返回值為nil時會處理全部未處理的transaction func lastCommonTransactionTimestamp(in appActors: [AppActor]) -> Date? { // 七天前 let sevenDaysAgo = Date().addingTimeInterval(-604800) let lasttimestamps = appActors .compactMap { lastHistoryTransactionTimestamp(for: $0) } // 全部actor都沒有設定值 guard !lasttimestamps.isEmpty else {return nil} let minTimestamp = lasttimestamps.min()! // 檢查是否全部的actor都設定了值 guard lasttimestamps.count != appActors.count else { //返回最晚的時間戳 return minTimestamp } // 如果超過7天還沒有獲得全部actor的值,則返回七天,防止有的actor永遠不會被設定 if minTimestamp < sevenDaysAgo { return sevenDaysAgo } else { return nil } }

/// 獲取指定的appActor最後處理的transaction的時間戳
/// - Parameter appActore: app角色,比如healthnote ,widget
/// - Returns: 日期(時間戳), 返回值為nil時會處理全部未處理的transaction
func lastHistoryTransactionTimestamp(for appActor: AppActor) -> Date? {
    let key = "PersistentHistoryTracker.lastToken.\(appActor.rawValue)"
    return object(forKey: key) as? Date
}

/// 給指定的appActor設定最新的transaction時間戳
/// - Parameters:
///   - appActor: app角色,比如healthnote ,widget
///   - newDate: 日期(時間戳)
func updateLastHistoryTransactionTimestamp(for appActor: AppActor, to newDate: Date?) {
    let key = "PersistentHistoryTracker.lastToken.\(appActor.rawValue)"
    set(newDate, forKey: key)
}

} ```

由於App Group的成員每個都會儲存自己的lastHistoryTransactionTimestamp,因此為了保證Transaction能夠被所有成員都正確合併後,再被清除掉,lastCommonTransactionTimestamp將返回所有成員最晚的時間戳。lastCommonTransactionTimestamp在清除合併後的Transaction時,將被使用到。

有了這些基礎,上面的程式碼變可以修改為:

swift let fromDate = userDefaults.lastHistoryTransactionTimestamp(for: currentActor) ?? Date.distantPast NSPersistentHistoryChangeRequest.fetchHistory(after: fromDate)

通過時間戳,我們已經過濾了大量不必關心的Transaction了,但在剩下的Transaction中都是我們需要的嗎?答案是否定的,至少有兩種情況的Transaction我們是不需要關心的:

  • 由當前App本身上下文產生的Transaction

通常App會對自身通過上下文產生的資料變化做出即時的反饋,如果改變化已經體現在了檢視上下文中(主執行緒ManagedObjectContext),則我們可以無需理會這些Transaction。但如果資料是通過批量操作完成的,或者是在backgroudContext操作,且並沒有被合併到檢視上下文中,我們還是要處理這些Transaction的。

  • 由系統產生的Transaction

比如當你使用了PersistentCloudKitContainer時,所有的網路同步資料都將會產生Transaction,這些Transaction會由CoreData來處理,我們無需理會。

基於以上兩點,我們可以進一步縮小需要處理的Transaction範圍。最終fetcher的程式碼如下:

```swift extension PersistentHistoryTrackerManager { enum Error: String, Swift.Error { case historyTransactionConvertionFailed } // 獲取過濾後的Transaction func fetcher() throws -> [NSPersistentHistoryTransaction] { let fromDate = userDefaults.lastHistoryTransactionTimestamp(for: currentActor) ?? Date.distantPast NSPersistentHistoryChangeRequest.fetchHistory(after: fromDate)

    let historyFetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: fromDate)
    if let fetchRequest = NSPersistentHistoryTransaction.fetchRequest {
        var predicates: [NSPredicate] = []

        AppActor.allCases.forEach { appActor in
            if appActor == currentActor {
                // 本程式碼假設在App中,即使通過backgroud進行的操作也已經被即時合併到了ViewContext中
                // 因此對於當前appActor,只處理名稱為batchContext上下文產生的transaction
                let perdicate = NSPredicate(format: "%K = %@ AND %K = %@",
                                            #keyPath(NSPersistentHistoryTransaction.author),
                                            appActor.rawValue,
                                            #keyPath(NSPersistentHistoryTransaction.contextName),
                                            "batchContext")
                predicates.append(perdicate)
            } else {
                // 其他的appActor產生的transactions,全部都要進行處理
                let perdicate = NSPredicate(format: "%K = %@",
                                            #keyPath(NSPersistentHistoryTransaction.author),
                                            appActor.rawValue)
                predicates.append(perdicate)
            }
        }

        let compoundPredicate = NSCompoundPredicate(type: .or, subpredicates: predicates)
        fetchRequest.predicate = compoundPredicate
        historyFetchRequest.fetchRequest = fetchRequest
    }
    guard let historyResult = try backgroundContext.execute(historyFetchRequest) as? NSPersistentHistoryResult,
          let history = historyResult.result as? [NSPersistentHistoryTransaction]
    else {
        throw Error.historyTransactionConvertionFailed
    }
    return history
}

} ```

如果你的App比較單純(比如沒有使用PersistentCloudKitContainer),可以不需要上面更精細的predicate處理過程。總的來說,即使獲取的Transaction超出了需要的範圍,CoreData在合併時給系統造成的壓力也並不大。

由於fetcher是通過NSPersistentHistoryTransaction.authorNSPersistentHistoryTransaction.contextName來對Transaction進行進一步過濾的,因此請在你的程式碼中,明確的在NSManagedObjectContext中標記上身份:

swift // 標記程式碼中的上下文的author,例如 viewContext.transactionAuthor = AppActor.mainApp.rawValue // 如果用於批處理的操作,請標記name,例如 backgroundContext.name = "batchContext"

清楚地標記Transaction資訊,是使用Persistent History Tracking的基本要求

將Transaction合併到檢視上下文中

通過fetcher獲取到了需要處理的Transaction後,我們需要將這些Transaction合併到檢視上下文中。

合併的操作就很簡單了,在合併後將最後的時間戳儲存即可。

```swift extension PersistentHistoryTrackerManager { func merger(transaction: [NSPersistentHistoryTransaction]) { let viewContext = container.viewContext viewContext.perform { transaction.forEach { transaction in let userInfo = transaction.objectIDNotification().userInfo ?? [:] NSManagedObjectContext.mergeChanges(fromRemoteContextSave: userInfo, into: [viewContext]) } }

    // 更新最後的transaction時間戳
    guard let lastTimestamp = transaction.last?.timestamp else { return }
    userDefaults.updateLastHistoryTransactionTimestamp(for: currentActor, to: lastTimestamp)
}

} ```

可以根據自己的習慣選用合併程式碼,下面的程式碼和上面的NSManagedObjectContext.mergeChanges是等效的:

swift viewContext.perform { transaction.forEach { transaction in viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification()) } }

這些已經在資料庫中發生但尚未反映在檢視上下文中的Transaction,會在合併後立即體現在你的App UI上。

清理合並後的Transaction

所有的Transaction都被儲存在Sqlite檔案中,不僅會佔用空間,而且隨著記錄的增多也會影響Sqlite的訪問速度。我們需要制定明確的清理策略來刪除已經處理過的Transaction。

fetcher中使用open class func fetchHistory(after date: Date) -> Self類似,Persistent History Tracking同樣為我們準備了三個方法用來做清理工作:

swift open class func deleteHistory(before date: Date) -> Self open class func deleteHistory(before token: NSPersistentHistoryToken?) -> Self open class func deleteHistory(before transaction: NSPersistentHistoryTransaction?) -> Self

刪除指定時間點之前且滿足條件的Transaction

清理策略可以粗曠的也可以很精細的,例如在蘋果官方文件中便採取了一種比較粗曠的清理策略:

```swift let sevenDaysAgo = Date(timeIntervalSinceNow: TimeInterval(exactly: -604_800)!) let purgeHistoryRequest = NSPersistentHistoryChangeRequest.deleteHistory( before: sevenDaysAgo)

do { try persistentContainer.backgroundContext.execute(purgeHistoryRequest) } catch { fatalError("Could not purge history: (error)") } ```

刪除一切7天前的Transaction,無論其author是誰。事實上,這個看似粗曠的策略在實際使用中幾乎沒有任何問題。

在本文中,我們將同fetcher一樣,對清除策略做更精細的處理。

```swift import CoreData import Foundation

/// 刪除已經處理過的transaction public struct PersistentHistoryCleaner { /// NSPersistentCloudkitContainer let container: NSPersistentContainer /// app group userDefaults let userDefault = UserDefaults.appGroup /// 全部的appActor let appActors = AppActor.allCases

/// 清除已經處理過的persistent history transaction
public func clean() {
    guard let timestamp = userDefault.lastCommonTransactionTimestamp(in: appActors) else {
        return
    }

    // 獲取可以刪除的transaction的request
    let deleteHistoryRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: timestamp)

    // 只刪除由App Group的成員產生的Transaction
    if let fetchRequest = NSPersistentHistoryTransaction.fetchRequest {
        var predicates: [NSPredicate] = []

        appActors.forEach { appActor in
            // 清理App Group成員建立的Transaction
            let perdicate = NSPredicate(format: "%K = %@",
                                        #keyPath(NSPersistentHistoryTransaction.author),
                                        appActor.rawValue)
            predicates.append(perdicate)
        }

        let compoundPredicate = NSCompoundPredicate(type: .or, subpredicates: predicates)
        fetchRequest.predicate = compoundPredicate
        deleteHistoryRequest.fetchRequest = fetchRequest
    }

    container.performBackgroundTask { context in
        do {
            try context.execute(deleteHistoryRequest)
            // 重置全部appActor的時間戳
            appActors.forEach { actor in
                userDefault.updateLastHistoryTransactionTimestamp(for: actor, to: nil)
            }
        } catch {
            print(error)
        }
    }
}

} ```

之所以在我在fetcher和cleaner中設定瞭如此詳盡的predicate,是因為我自己是在PersistentCloudKitContainer中使用Persistent History Tracking功能的。Cloudkit同步會產生大量的Transaction,因此需要更精準的對操作物件進行過濾。

CoreData會自動處理和清除CloudKit同步產生的Transaction,但是如果我們不小心刪除了尚沒被CoreData處理的CloudKit Transaction,可能會導致資料庫同步錯誤,CoreData會清空當前的全部資料,嘗試從遠端重新載入資料。

因此,如果你是在PersistentCloudKitContainer上使用Persistent History Tracking,請務必僅對App Group成員產生的Transaction做清除操作。

如果僅是在PersistentContainer上使用Persistent History Tracking,fetcher和cleaner中都可以不用過濾的如此徹底。

在建立了PersistentHistoryCleaner後,我們可以根據自己的實際情況選擇呼叫時機。

如果採用PersistentContainer,可以嘗試比較積極的清除策略。在PersistentHistoryTrackingManager中新增如下程式碼:

```swift private func processor() { backgroundContext.performAndWait { ... }

    let cleaner = PersistentHistoryCleaner(container: container)
    cleaner.clean()
}

```

這樣在每次響應NSPersistentStoreRemoteChange通知後,都會嘗試清除已經合併過的Transaction。

不過我個人更推薦使用不那麼積極的清除策略。

swift @main struct PersistentTrackBlogApp: App { let persistenceController = PersistenceController.shared @Environment(\.scenePhase) var scenePhase var body: some Scene { WindowGroup { ContentView() .environment(\.managedObjectContext, persistenceController.container.viewContext) .onChange(of: scenePhase) { scenePhase in switch scenePhase { case .active: break case .background: let clean = PersistentHistoryCleaner(container: persistenceController.container) clean.clean() case .inactive: break @unknown default: break } } } } }

比如當app退到後臺時,進行清除工作。

總結

可以在Github下載本文的全部程式碼。

以下資料對於本文有著至關重要的作用:

Donny Wals的這本書是我最近一段時間非常喜歡的一本CoreData的書籍。其中有關於Persistent History Tracking的章節。另外他的Blog也經常會有關於CoreData的文章

Avanderlee的部落格也有大量關於CoreData的精彩文章,Persistent History Tracking in Core Data這篇文章同樣做了非常詳細的說明。本文的程式碼結構也受其影響。

蘋果構建了Persistent History Tracking,讓多個成員可以共享單個數據庫並保持UI的及時更新。無論你是構建一套應用程式,或者是想為你的App新增合適的Extension,亦或僅為了統一的響應批處理操作的資料,持久化歷史跟蹤都能為你提供良好的幫助。

Persistent History Tracking儘管可能會造成一點系統負擔,不過和它帶來的便利性相比是微不足道的。在實際使用中,我基本上感受不到因它而導致的效能損失。

本文原載於我的個人部落格肘子的Swift記事本

文章也一併釋出在微信公共號:肘子的Swift記事本

weixincode.png