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

前言

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

在計劃中的【健康筆記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-7349058.png

蘋果并沒有公開這些表的具體結構,不過我們可以使用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-7299035.png

在App Groups中選擇或者創建group

image-20210726193200091-7299122.png

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

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

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

image-20210726193608259.png

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

創建可在Group中共享的UserDefaults

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中所有的成員共享,每個成員都可以對其進行讀寫操作

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

獲取Group Container URL

 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中啟用的例子:

   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,類似的代碼如下:

        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啟用該功能。

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

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事件

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

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

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

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

獲取需要處理的Transaction

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

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信息:

NSPersistentHistoryChangeRequest.fetchHistory(after: .distantPast)

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

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

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時,將被使用到。

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

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的代碼如下:

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中標記上身份:

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

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

將Transaction合并到視圖上下文中

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

合并的操作就很簡單了,在合并后將最后的時間戳保存即可。

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是等效的:

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同樣為我們準備了三個方法用來做清理工作:

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

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

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一樣,對清除策略做更精細的處理。

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中添加如下代碼:

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

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

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

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

@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下載本文的全部代碼。

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

  • Practical Core Data

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

  • SwiftLee

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

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

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

本文原載于我的個人博客肘子的Swift記事本

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容