> 原文出处:http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-ba
今天来学习一下多个context的情况,特别是在多线程环境下。第十章也是本书的最后一章,如果你对core data的其他内容感兴趣,可以去翻看之前的笔记,或直接购买[《Core Data by Tutorials》](http://www.raywenderlich.com/store/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)。
![](https://box.kancloud.cn/2015-08-21_55d6ecd7b6080.jpg)
关于这个冲浪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的系统学习还是收获不小的:)