Core Data by tutorials 笔记(八)

今天来学习一下多个 context 的情况,特别是在多线程环境下。第十章也是本书的最后一章,如果你对 core data 的其他内容感兴趣,可以去翻看之前的笔记,或直接购买 《Core Data by Tutorials》

Chapter 10: Multiple Managed Object Contexts

作者一开始介绍了几种使用多个 context 的情形,比如会阻塞 UI 的的任务,最好还是在后台线程单独使用一个 context,和主线程 context 分开。还有处理临时编辑的数据时,使用一个 child context 也会很有帮助。

一、Getting started

本章提供了一个冲浪评分的 APP 作为 Start Project,你可以添加冲浪地点的评价,还可以将所有记录导出为 CSV 文件。

与之前章节不同的是,这个 APP 的初始数据存放在 app bundle 中,我们看看在 Core Data stack 中如何获取:

// 1 找到并创建一个 URL 引用
let seededDatabaseURL = bundle .URLForResource("SurfJournalDatabase",  
    withExtension: "sqlite")
// 2 尝试拷贝 seeded database 文件到 document 目录,只会拷贝一次,存在就会失败。
var fileManagerError:NSError? = nil  
let didCopyDatabase = NSFileManager.defaultManager()  
    .copyItemAtURL(seededDatabaseURL!, toURL: storeURL, 
    error: &fileManagerError)
// 3 只有拷贝成功才会运行下面方法
if didCopyDatabase {  
    // 4 拷贝 smh(shared memory file)
    fileManagerError = nil 
    let seededSHMURL = bundle
        .URLForResource("SurfJournalDatabase", withExtension: "sqlite-shm")
    let shmURL = documentsURL.URLByAppendingPathComponent( 
        "SurfJournalDatabase.sqlite-shm")
    let didCopySHM = NSFileManager.defaultManager() 
        .copyItemAtURL(seededSHMURL!, toURL: shmURL,
        error: &fileManagerError) 
    if !didCopySHM {
        println("Error seeding Core Data: \(fileManagerError)")
        abort() 
    }
    // 5 拷贝wal(write-ahead logging file)
    fileManagerError = nil
    let walURL = documentsURL.URLByAppendingPathComponent(
        "SurfJournalDatabase.sqlite-wal") 
    let seededWALURL = bundle
        .URLForResource("SurfJournalDatabase", withExtension: "sqlite-wal")
    let didCopyWAL = NSFileManager.defaultManager() 
        .copyItemAtURL(seededWALURL!, toURL: walURL,
        error: &fileManagerError) 
    if !didCopyWAL {
        println("Error seeding Core Data: \(fileManagerError)")
        abort() 
    }
    println("Seeded Core Data")
}
// 6 指定 store URL 即可
var error: NSError? = nil  
let options = [NSInferMappingModelAutomaticallyOption:true,  
    NSMigratePersistentStoresAutomaticallyOption:true] 
store = psc.addPersistentStoreWithType(NSSQLiteStoreType,  
    configuration: nil, 
    URL: storeURL, 
    options: options, 
    error: &error)
// 7
if store == nil {  
    println("Error adding persistent store: \(error)") 
    abort()
}

上面的方法除了拷贝 sqlite 文件,还拷贝了 SHM (shared memory file) 和 WAL (write-ahead logging) files,这都是为了并行读写的需要。无论那个文件出错了都直接让程序终止 abort。

二、Doing work in the background

当我们导出数据时,会发现这个过程会阻塞 UI。传统的方法是使用 GCD 在后台执行 export 操作,但 Core data managed object contexts 并不是线程安全的,也就是说你不能简单的开启一个后台线程然后使用相同的 core data stack。

解决方法也很简单:针对 export 操作创建一个新的 context 放到一个私有线程中去执行,而不是在主线程里。

将数据导出为 csv,其实很多场景都能用到,具体来学习一下:

  • 先为实体 JournalEntry 子类添加一个 csv string 方法,将属性输出为字符串:

    func csv() -> String {
    let coalescedHeight = height ?? ""
    let coalescedPeriod = period ?? ""
    let coalescedWind = wind ?? ""
    let coalescedLocation = location ?? "" 
    var coalescedRating:String
    if let rating = rating?.intValue {
        coalescedRating = String(rating) 
    } else {
        coalescedRating = "" 
    }
    return "\(stringForDate()),\(coalescedHeight)," + 
        "\(coalescedPeriod),\(coalescedWind)," + 
        "\(coalescedLocation),\(coalescedRating)\n"
    }
    
  • 通过 fetch 得到所有的 jouranlEntry 实体,用 NSFileManager 在临时文件夹下创建一个 csv 文件并返回这个URL

    // 1
    var fetchRequestError: NSError? = nil
    let results = coreDataStack.context.executeFetchRequest(
    self.surfJournalFetchRequest(), error: &fetchRequestError)
    if results == nil {
        println("ERROR: \(fetchRequestError)")
    }
    // 2
    let exportFilePath = NSTemporaryDirectory() + "export.csv"
    let exportFileURL = NSURL(fileURLWithPath: exportFilePath)!
    NSFileManager.defaultManager().createFileAtPath( 
        exportFilePath, contents: NSData(), attributes: nil)
    
  • 用这个 URL 初始化一个 NSFileHandle,用 for-in 遍历取出每一个 journalEntry 实体,执行 csv() 将自身属性处理成字符串,然后用 UTF8-encoded 编码转换为 NSData 类型的 data,最后 NSFileHandle 将 data 写入 URL

    // 3
    var fileHandleError: NSError? = nil
    let fileHandle = NSFileHandle(forWritingToURL: exportFileURL,
        error: &fileHandleError)
    if let fileHandle = fileHandle {
    // 4
    for object in results! {
        let journalEntry = object as JournalEntry
        fileHandle.seekToEndOfFile()
        let csvData = journalEntry.csv().dataUsingEncoding(
            NSUTF8StringEncoding, allowLossyConversion: false)
            fileHandle.writeData(csvData!)
    }
    // 5
    fileHandle.closeFile()
    

学习完如何将数据导出为 csv,我们来进入本章真正的主题,创建一个私有的后台线程,把 export 操作放在这个后台线程中去执行。

// 1 创建一个使用私有线程的 context,与 main context 共用一个 persistentStoreCoordinator
let privateContext = NSManagedObjectContext(  
    concurrencyType: .PrivateQueueConcurrencyType)
privateContext.persistentStoreCoordinator =  
    coreDataStack.context.persistentStoreCoordinator
// 2 performBlock 这个方法会在 context 的线程上异步执行 block 里的内容
privateContext.performBlock { () -> Void in  
// 3 获取所有的 JournalEntry entities
    var fetchRequestError:NSError? = nil
    let results = privateContext.executeFetchRequest(
        self.surfJournalFetchRequest(), 
        error: &fetchRequestError)
    if results == nil {
        println("ERROR: \(fetchRequestError)")
    }
......

在后台执行 performBlock 的过程中,所有UI相关的操作还是要回到主线程中来执行。

// 4
    dispatch_async(dispatch_get_main_queue(), { () -> Void in
        self.navigationItem.leftBarButtonItem =
            self.exportBarButtonItem()
        println("Export Path: \(exportFilePath)")
        self.showExportFinishedAlertView(exportFilePath)
    })
    } else {
      dispatch_async(dispatch_get_main_queue(), { () -> Void in
        self.navigationItem.leftBarButtonItem = self.exportBarButtonItem()
        println("ERROR: \(fileHandleError)") })
    }
} // 5 closing brace for performBlock()

关于 managed object context 的 concurrency types 一共有三种类型:

  • ConfinementConcurrencyType 这种手动管理线程访问的基本不用
  • PrivateQueueConcurrencyType 指定 context 将在后台线程中使用
  • MainQueueConcurrencyType 指定 context 将在主线程中使用,任何 UI 相关的操作都要使用这一种,包括为 table view 创建一个 fetched results controller。

三、Editing on a scratchpad

本节介绍了另外一种情形,类似于便笺本,你在上面涂写,到最后你可以选择保存也可以选择丢弃掉。作者使用了一种 child managed object contexts 的方式来模拟这个便签本,要么发送这些 changes 到 parent context 保存,要么直接丢弃掉。

具体的技术细节是:所有的 managed object contexts 都有一个叫做 parent store(父母空间)的东西,用来检索和修改数据(具体数据都是 managed objects 形式)。进一步讲,the parent store 其实就是一个 persistent store coordinator,比如 main context,他的 parent store 就是由 CoreDataStack 提供的 persistent store coordinator。相对的,你可以将一个 context 设置为另一个 context 的 parent store,其中一个 context 就是 child context。而且当你保存这个 child context 时,这些 changes 只能到达 parent context,不会再向更高的 parent context 传递(除非 parent context save)。

关于这个冲浪APP还是有个小问题,当添加了一个新的 journal entry 后,就会创建新的 object1 添加到 context 中,如果这时候点击 Cancel 按钮,应用是不会保存到 context,但这个 object1 会仍然存在,这个时候,再增加另一个 object2 然后保存到 context,此时 object1 这个被取消的对象仍然会出现在 table view 中。

你可以在cancel的时候通过简单的删除操作来解决这个 issue,但是如果操作更加复杂还是使用一个临时的 child context 更加简单。

// 1
let childContext = NSManagedObjectContext(  
    concurrencyType: .MainQueueConcurrencyType)
childContext.parentContext = coreDataStack.context  
// 2
let childEntry = childContext.objectWithID(  
    surfJournalEntry.objectID) as JournalEntry
// 3
detailViewController.journalEntry = childEntry  
detailViewController.context = childContext  
detailViewController.delegate = self  

创建一个 childContext,parent store 设为 main context。这里使用了 objectID 来获取 journal entry。因为 managed objects 只特定于自己的 context 的,而 objectID 针对所有的 context 都是唯一的,所以 childContext 要使用 objectID 来获取 mainContext 中的 managed objects

最后一点要注意的是注释3,这里同时为 detailViewController 传递了 managed object(childEntry)和 managed object context(childContext),为什么不只传递 managed object 呢,他可以通过属性 managed object context 来得到 context 呀,原因就在于 managed object 对于 context 仅仅是 弱引用,如果不传递 context,ARC 就有可能将其移除,产生不可控结果。

历时一周终于写完了,通过对 Core Data 的系统学习还是收获不小的:)


-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!