Core Data by tutorials 笔记(八)
原文出处: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的系统学习还是收获不小的:)
更多建议: