Core Data by tutorials 筆記(八)

2018-02-24 15:55 更新

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

今天來學(xué)習(xí)一下多個(gè)context的情況,特別是在多線程環(huán)境下。第十章也是本書的最后一章,如果你對(duì)core data的其他內(nèi)容感興趣,可以去翻看之前的筆記,或直接購買《Core Data by Tutorials》

Chapter 10: Multiple Managed Object Contexts

作者一開始介紹了幾種使用多個(gè)context的情形,比如會(huì)阻塞UI的的任務(wù),最好還是在后臺(tái)線程單獨(dú)使用一個(gè)context,和主線程context分開。還有處理臨時(shí)編輯的數(shù)據(jù)時(shí),使用一個(gè)child context也會(huì)很有幫助。

一、Getting started

本章提供了一個(gè)沖浪評(píng)分的APP作為Start Project,你可以添加沖浪地點(diǎn)的評(píng)價(jià),還可以將所有記錄導(dǎo)出為CSV文件。

與之前章節(jié)不同的是,這個(gè)APP的初始數(shù)據(jù)存放在app bundle中,我們看看在Core Data stack中如何獲取:

// 1 找到并創(chuàng)建一個(gè)URL引用
let seededDatabaseURL = bundle .URLForResource("SurfJournalDatabase",
    withExtension: "sqlite")
// 2 嘗試拷貝seeded database文件到document目錄,只會(huì)拷貝一次,存在就會(huì)失敗。
var fileManagerError:NSError? = nil
let didCopyDatabase = NSFileManager.defaultManager()
    .copyItemAtURL(seededDatabaseURL!, toURL: storeURL, 
    error: &fileManagerError)
// 3 只有拷貝成功才會(huì)運(yùn)行下面方法
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,這都是為了并行讀寫的需要。無論那個(gè)文件出錯(cuò)了都直接讓程序終止abort。

二、Doing work in the background

當(dāng)我們導(dǎo)出數(shù)據(jù)時(shí),會(huì)發(fā)現(xiàn)這個(gè)過程會(huì)阻塞UI。傳統(tǒng)的方法是使用GCD在后臺(tái)執(zhí)行export操作,但Core data managed object contexts并不是線程安全的,也就是說你不能簡單的開啟一個(gè)后臺(tái)線程然后使用相同的core data stack。

解決方法也很簡單:針對(duì)export操作創(chuàng)建一個(gè)新的context放到一個(gè)私有線程中去執(zhí)行,而不是在主線程里。

將數(shù)據(jù)導(dǎo)出為csv,其實(shí)很多場景都能用到,具體來學(xué)習(xí)一下:

  • 先為實(shí)體JournalEntry子類添加一個(gè)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實(shí)體,用NSFileManager在臨時(shí)文件夾下創(chuàng)建一個(gè)csv文件并返回這個(gè)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)
  • 用這個(gè)URL初始化一個(gè)NSFileHandle,用for-in遍歷取出每一個(gè)journalEntry實(shí)體,執(zhí)行csv()將自身屬性處理成字符串,然后用UTF8-encoded編碼轉(zhuǎn)換為NSData類型的data,最后NSFileHandle將data寫入U(xiǎn)RL

    // 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()

學(xué)習(xí)完如何將數(shù)據(jù)導(dǎo)出為csv,我們來進(jìn)入本章真正的主題,創(chuàng)建一個(gè)私有的后臺(tái)線程,把export操作放在這個(gè)后臺(tái)線程中去執(zhí)行。

// 1 創(chuàng)建一個(gè)使用私有線程的context,與main context共用一個(gè)persistentStoreCoordinator
let privateContext = NSManagedObjectContext( 
    concurrencyType: .PrivateQueueConcurrencyType)
privateContext.persistentStoreCoordinator =
    coreDataStack.context.persistentStoreCoordinator
// 2 performBlock這個(gè)方法會(huì)在context的線程上異步執(zhí)行block里的內(nèi)容
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)")
    }
......

在后臺(tái)執(zhí)行performBlock的過程中,所有UI相關(guān)的操作還是要回到主線程中來執(zhí)行。

// 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()

關(guān)于managed object context的concurrency types一共有三種類型:

  • ConfinementConcurrencyType 這種手動(dòng)管理線程訪問的基本不用
  • PrivateQueueConcurrencyType 指定context將在后臺(tái)線程中使用
  • MainQueueConcurrencyType 指定context將在主線程中使用,任何UI相關(guān)的操作都要使用這一種,包括為table view創(chuàng)建一個(gè)fetched results controller。

三、Editing on a scratchpad

本節(jié)介紹了另外一種情形,類似于便箋本,你在上面涂寫,到最后你可以選擇保存也可以選擇丟棄掉。作者使用了一種child managed object contexts的方式來模擬這個(gè)便簽本,要么發(fā)送這些changes到parent context保存,要么直接丟棄掉。

具體的技術(shù)細(xì)節(jié)是:所有的managed object contexts都有一個(gè)叫做parent store(父母空間)的東西,用來檢索和修改數(shù)據(jù)(具體數(shù)據(jù)都是managed objects形式)。進(jìn)一步講,the parent store其實(shí)就是一個(gè)persistent store coordinator,比如main context,他的parent store就是由CoreDataStack提供的persistent store coordinator。相對(duì)的,你可以將一個(gè)context設(shè)置為另一個(gè)context的parent store,其中一個(gè)context就是child context。而且當(dāng)你保存這個(gè)child context時(shí),這些changes只能到達(dá)parent context,不會(huì)再向更高的parent context傳遞(除非parent context save)。

關(guān)于這個(gè)沖浪APP還是有個(gè)小問題,當(dāng)添加了一個(gè)新的journal entry后,就會(huì)創(chuàng)建新的object1添加到context中,如果這時(shí)候點(diǎn)擊Cancel按鈕,應(yīng)用是不會(huì)保存到context,但這個(gè)object1會(huì)仍然存在,這個(gè)時(shí)候,再增加另一個(gè)object2然后保存到context,此時(shí)object1這個(gè)被取消的對(duì)象仍然會(huì)出現(xiàn)在table view中。

你可以在cancel的時(shí)候通過簡單的刪除操作來解決這個(gè)issue,但是如果操作更加復(fù)雜還是使用一個(gè)臨時(shí)的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

創(chuàng)建一個(gè)childContext,parent store設(shè)為main context。這里使用了objectID來獲取journal entry。因?yàn)閙anaged objects只特定于自己的context的,而objectID針對(duì)所有的context都是唯一的,所以childContext要使用objectID來獲取mainContext中的managed objects。

最后一點(diǎn)要注意的是注釋3,這里同時(shí)為detailViewController傳遞了managed object(childEntry)和managed object context(childContext),為什么不只傳遞managed object呢,他可以通過屬性managed object context來得到context呀,原因就在于managed object對(duì)于context僅僅是弱引用,如果不傳遞context,ARC就有可能將其移除,產(chǎn)生不可控結(jié)果。

歷時(shí)一周終于寫完了,通過對(duì)Core Data的系統(tǒng)學(xué)習(xí)還是收獲不小的:)

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)