Core Data by tutorials 笔记(八)

2018-02-24 15:55 更新

原文出处:http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-ba

今天来学习一下多个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的系统学习还是收获不小的:)

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号