🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
> 原文出处: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的系统学习还是收获不小的:)