認識CoreData - 多線程

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> http://www.jianshu.com/p/283e67ba12a3git


CoreData使用相關的技術點已經講差很少了,我所掌握的也就這麼多了....

在本篇文章中主要講CoreData的多線程,其中會包括併發隊列類型、線程安全等技術點。我對多線程的理解可能不是太透徹,文章中出現的問題還請各位指出。在以後公司項目使用CoreData的過程當中,我會將其中遇到的多線程相關的問題更新到文章中。github

在文章的最後,會根據我對CoreData多線程的學習,以及在工做中的具體使用,給出一些關於多線程結構的設計建議,各位能夠當作參考。sql

文章中若有疏漏或錯誤,還請各位及時提出,謝謝!😊數據庫


博客配圖

MOC併發隊列類型

CoreDataMOC是支持多線程的,能夠在建立MOC對象時,指定其併發隊列的類型。當指定隊列類型後,系統會將操做都放在指定的隊列中執行,若是指定的是私有隊列,系統會建立一個新的隊列。但這都是系統內部的行爲,咱們並不能獲取這個隊列,隊列由系統所擁有,並由系統將任務派發到這個隊列中執行的緩存

NSManagedObjectContext併發隊列類型:
  • NSConfinementConcurrencyType : 若是使用init方法初始化上下文,默認就是這個併發類型。這個枚舉值是不支持多線程的,從名字上也體現出來了。
  • NSPrivateQueueConcurrencyType : 私有併發隊列類型,操做都是在子線程中完成的。
  • NSMainQueueConcurrencyType : 主併發隊列類型,若是涉及到UI相關的操做,應該考慮使用這個枚舉值初始化上下文。

其中NSConfinementConcurrencyType類型在iOS9以後已經被蘋果廢棄,不建議使用這個API。使用此類型建立的MOC,調用某些比較新的CoreDataAPI可能會致使崩潰。安全

MOC多線程調用方式

CoreDataMOC不是線程安全的,在多線程狀況下使用MOC時,不能簡單的將MOC從一個線程中傳遞到另外一個線程中使用,這並非CoreData的多線程,並且會出問題。對於MOC多線程的使用,蘋果給出了本身的解決方案。數據結構

在建立的MOC中使用多線程,不管是私有隊列仍是主隊列,都應該採用下面兩種多線程的使用方式,而不是本身手動建立線程。調用下面方法後,系統內部會將任務派發到不一樣的隊列中執行。能夠在不一樣的線程中調用MOC的這兩個方法,這個是容許的。多線程

- (void)performBlock:(void (^)())block            異步執行的block,調用以後會馬上返回。
- (void)performBlockAndWait:(void (^)())block     同步執行的block,調用以後會等待這個任務完成,纔會繼續向下執行。

下面是多線程調用的示例代碼,在多線程的環境下執行MOCsave方法,就是將save方法放在MOCblock體中異步執行,其餘方法的調用也是同樣的。併發

[context performBlock:^{
    [context save:nil];
}];

可是須要注意的是,這兩個block方法不能在NSConfinementConcurrencyType類型的MOC下調用,這個類型的MOC是不支持多線程的,只支持其餘兩種併發方式的MOC異步

多線程的使用

在業務比較複雜的狀況下,須要進行大量數據處理,而且還須要涉及到UI的操做。對於這種複雜需求,若是都放在主隊列中,對性能和界面流暢度都會有很大的影響,致使用戶體驗很是差,下降屏幕FPS。對於這種狀況,能夠採起多個MOC配合的方式。

CoreData多線程的發展中,在iOS5經歷了一次比較大的變化,以後能夠更方便的使用多線程。從iOS5開始,支持設置MOCparentContext屬性,經過這個屬性能夠設置MOC父MOC。下面會針對iOS5以前和以後,分別講解CoreData的多線程使用。

儘管如今的開發中早就不兼容iOS5以前的系統了,可是做爲了解這裏仍是要講一下,並且這種同步方式在iOS5以後也是能夠正常使用的,也有不少人還在使用這種同步方式,下面其餘章節也是同理。

iOS5以前使用多個MOC

iOS5以前實現MOC的多線程,能夠建立多個MOC,多個MOC使用同一個PSC,並讓多個MOC實現數據同步。經過這種方式不用擔憂PSC在調用過程當中的線程問題,MOC在使用PSC進行save操做時,會對PSC進行加鎖,等當前加鎖的MOC執行完操做以後,其餘MOC才能繼續執行操做。

每個PSC都對應着一個持久化存儲區,PSC知道存儲區中數據存儲的數據結構,而MOC須要使用這個PSC進行save操做的實現。

多線程結構

這樣作有一個問題,當一個MOC發生改變並持久化到本地時,系統並不會將其餘MOC緩存在內存中的NSManagedObject對象改變。因此這就須要咱們在MOC發生改變時,將其餘MOC數據更新。

根據上面的解釋,在下面例子中建立了一個主隊列的mainMOC,主要用於UI操做。一個私有隊列的backgroundMOC,用於除UI以外的耗時操做,兩個MOC使用的同一個PSC

// 獲取PSC實例對象
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

    // 建立託管對象模型,並指明加載Company模型文件
    NSURL *modelPath = [[NSBundle mainBundle] URLForResource:@"Company" withExtension:@"momd"];
    NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelPath];

       // 建立PSC對象,並將託管對象模型當作參數傳入,其餘MOC都是用這一個PSC。
    NSPersistentStoreCoordinator *PSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];

    // 根據指定的路徑,建立並關聯本地數據庫
    NSString *dataPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
    dataPath = [dataPath stringByAppendingFormat:@"/%@.sqlite", @"Company"];
    [PSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:nil error:nil];

    return PSC;
}

// 初始化用於本地存儲的全部MOC
- (void)createManagedObjectContext {

    // 建立PSC實例對象,其餘MOC都用這一個PSC。
    NSPersistentStoreCoordinator *PSC = self.persistentStoreCoordinator;

    // 建立主隊列MOC,用於執行UI操做
    NSManagedObjectContext *mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    mainMOC.persistentStoreCoordinator = PSC;

    // 建立私有隊列MOC,用於執行其餘耗時操做
    NSManagedObjectContext *backgroundMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    backgroundMOC.persistentStoreCoordinator = PSC;

    // 經過監聽NSManagedObjectContextDidSaveNotification通知,來獲取全部MOC的改變消息
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:nil];
}

// MOC改變後的通知回調
- (void)contextChanged:(NSNotification *)noti {
    NSManagedObjectContext *MOC = noti.object;
    // 這裏須要作判斷操做,判斷當前改變的MOC是否咱們將要作同步的MOC,若是就是當前MOC本身作的改變,那就不須要再同步本身了。
    // 因爲項目中可能存在多個PSC,因此下面還須要判斷PSC是否當前操做的PSC,若是不是當前PSC則不須要同步,不要去同步其餘本地存儲的數據。
    [MOC performBlock:^{
        // 直接調用系統提供的同步API,系統內部會完成同步的實現細節。
        [MOC mergeChangesFromContextDidSaveNotification:noti];
    }];
}

在上面的Demo中,建立了一個PSC,並將其餘MOC都關聯到這個PSC上,這樣全部的MOC執行本地持久化相關的操做時,都是經過同一個PSC進行操做的。並在下面添加了一個通知,這個通知是監聽全部MOC執行save操做後的通知,並在通知的回調方法中進行數據的合併

iOS5以後使用多個MOC

iOS5以後,MOC能夠設置parentContext,一個parentContext能夠擁有多個ChildContext。在ChildContext執行save操做後,會將操做pushparentContext,由parentContext去完成真正的save操做,而ChildContext全部的改變都會被parentContext所知曉,這解決了以前MOC手動同步數據的問題。

須要注意的是,在ChildContext調用save方法以後,此時並無將數據寫入存儲區,還須要調用parentContextsave方法。由於ChildContext並不擁有PSCChildContext也不須要設置PSC,因此須要parentContext調用PSC來執行真正的save操做。也就是隻有擁有PSCMOC執行save操做後,纔是真正的執行了寫入存儲區的操做。

- (void)createManagedObjectContext {
    // 建立PSC實例對象,仍是用上面Demo的實例化代碼
    NSPersistentStoreCoordinator *PSC = self.persistentStoreCoordinator;

    // 建立主隊列MOC,用於執行UI操做
    NSManagedObjectContext *mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    mainMOC.persistentStoreCoordinator = PSC;

    // 建立私有隊列MOC,用於執行其餘耗時操做,backgroundMOC並不須要設置PSC
    NSManagedObjectContext *backgroundMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    backgroundMOC.parentContext = mainMOC;

    // 私有隊列的MOC和主隊列的MOC,在執行save操做時,都應該調用performBlock:方法,在本身的隊列中執行save操做。
    // 私有隊列的MOC執行完本身的save操做後,還調用了主隊列MOC的save方法,來完成真正的持久化操做,不然不能持久化到本地
    [backgroundMOC performBlock:^{
        [backgroundMOC save:nil];
    
        [mainMOC performBlock:^{
            [mainMOC save:nil];
        }];
    }];
}

上面例子中建立一個主隊列的mainMOC,來完成UI相關的操做。建立私有隊列的backgroundMOC,處理複雜邏輯以及數據處理操做,在實際開發中能夠根據需求建立多個backgroundMOC。須要注意的是,在backgroundMOC執行完save方法後,又在mainMOC中執行了一次save方法,這步是很重要的。

iOS5以前進行數據同步

就像上面章節中講到的,在iOS5以前存在多個MOC的狀況下,一個MOC發生更改並提交存儲區後,其餘MOC並不知道這個改變,其餘MOC和本地存儲的數據是不一樣步的,因此就涉及到數據同步的問題。

進行數據同步時,會遇到多種複雜狀況。例如只有一個MOC數據發生了改變,其餘MOC更新時並無對相同的數據作改變,這樣不會形成衝突,能夠直接將其餘MOC更新。

若是在一個MOC數據發生改變後,其餘MOC對相同的數據作了改變,並且改變的結果不一樣,這樣在同步時就會形成衝突。下面將會按照這兩種狀況,分別講一下不一樣狀況下的衝突處理方式。

簡單狀況下的數據同步

簡單狀況下的數據同步,是針對於只有一個MOC的數據發生改變,並提交存儲區後,其餘MOC更新時並無對相同的數據作改變,只是單純的同步數據的狀況。

NSManagedObjectContext類中,根據不一樣操做定義了一些通知。在一個MOC發生改變時,其餘地方能夠經過MOC中定義的通知名,來獲取MOC發生的改變。在NSManagedObjectContext中定義了下面三個通知:

  • NSManagedObjectContextWillSaveNotification MOC將要向存儲區存儲數據時,調用這個通知。在這個通知中不能獲取發生改變相關的NSManagedObject對象。
  • NSManagedObjectContextDidSaveNotification MOC向存儲區存儲數據後,調用這個通知。在這個通知中能夠獲取改變、添加、刪除等信息,以及相關聯的NSManagedObject對象。
  • NSManagedObjectContextObjectsDidChangeNotificationMOC中任何一個託管對象發生改變時,調用這個通知。例如修改託管對象的屬性。

經過監聽NSManagedObjectContextDidSaveNotification通知,獲取全部MOCsave操做。

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(settingsContext:) name:NSManagedObjectContextDidSaveNotification object:nil];

不須要在通知的回調方法中,編寫代碼對比被修改的託管對象。MOC爲咱們提供了下面的方法,只須要將通知對象傳入,系統會自動同步數據

- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification;

下面是通知中的實現代碼,可是須要注意的是,因爲通知是同步執行的,在通知對應的回調方法中所處的線程,和發出通知的MOC執行操做時所處的線程是同一個線程,也就是系統performBlock:回調方法分配的線程。

因此其餘MOC在通知回調方法中,須要注意使用performBlock:方法,並在block體中執行操做。

- (void)settingsContext:(NSNotification *)noti {
    [context performBlock:^{
        // 調用須要同步的MOC對象的merge方法,直接將通知對象當作參數傳進去便可,系統會完成同步操做。
        [context mergeChangesFromContextDidSaveNotification:noti];
    }];
}

複雜狀況下的數據同步

在一個MOC對本地存儲區的數據發生改變,而其餘MOC也對一樣的數據作了改變,這樣後面執行save操做的MOC就會衝突,並致使後面的save操做失敗,這就是複雜狀況下的數據合併。

這是由於每次一個MOC執行一次fetch操做後,會保存一個本地持久化存儲的狀態,當下次執行save操做時會對比這個狀態和本地持久化狀態是否同樣。若是同樣,則表明本地沒有其餘MOC對存儲發生過改變;若是不同,則表明本地持久化存儲被其餘MOC改變過,這就是形成衝突的根本緣由。

對於這種衝突的狀況,能夠經過MOC對象指定解決衝突的方案,經過mergePolicy屬性來設置方案。mergePolicy屬性有下面幾種可選的策略,默認是NSErrorMergePolicy方式,這也是惟一一個有NSError返回值的選項。

  • NSErrorMergePolicy : 默認值,當出現合併衝突時,返回一個NSError對象來描述錯誤,而MOC和持久化存儲區不發生改變
  • NSMergeByPropertyStoreTrumpMergePolicy : 以本地存儲爲準,使用本地存儲來覆蓋衝突部分。
  • NSMergeByPropertyObjectTrumpMergePolicy : 以MOC的爲準,使用MOC來覆蓋本地存儲的衝突部分。
  • NSOverwriteMergePolicy : 以MOC爲準,用MOC的全部NSManagedObject對象覆蓋本地存儲的對應對象。
  • NSRollbackMergePolicy : 以本地存儲爲準,MOC全部的NSManagedObject對象被本地存儲的對應對象所覆蓋。

上面五種策略中,除了第一個NSErrorMergePolicy的策略,其餘四種中NSMergeByPropertyStoreTrumpMergePolicyNSRollbackMergePolicy,以及NSMergeByPropertyObjectTrumpMergePolicyNSOverwriteMergePolicy看起來是重複的。

其實它們並非衝突的,這四種策略的不一樣體如今,對沒有發生衝突的部分應該怎麼處理NSMergeByPropertyStoreTrumpMergePolicyNSMergeByPropertyObjectTrumpMergePolicy對沒有衝突的部分,未衝突部分數據並不會受到影響。而NSRollbackMergePolicyNSOverwriteMergePolicy則是不管是否衝突,直接所有替換。

題外話:
對於MOC的這種合併策略來看,有木有感受到CoreData解決衝突的方式,和SVN解決衝突的方式特別像。。。

線程安全

不管是MOC仍是託管對象,都不該該在其餘MOC的線程中執行操做,這兩個API都不是線程安全的。但MOC能夠在其餘MOC線程中調用performBlock:方法,切換到本身的線程執行操做。

若是其餘MOC想要拿到託管對象,並在本身的隊列中使用託管對象,這是不容許的,託管對象是不能直接傳遞到其餘MOC的線程的。可是能夠經過獲取NSManagedObjectNSManagedObjectID對象,在其餘MOC中經過NSManagedObjectID對象,從持久化存儲區中獲取NSManagedObject對象,這樣就是容許的。NSManagedObjectID是線程安全,而且能夠跨線程使用的。

能夠經過MOC獲取NSManagedObjectID對應的NSManagedObject對象,例以下面幾個MOCAPI

NSManagedObject *object = [context objectRegisteredForID:objectID];
NSManagedObject *object = [context objectWithID:objectID];

經過NSManagedObject對象的objectID屬性,獲取NSManagedObjectID類型的objectID對象。

NSManagedObjectID *objectID = object.objectID;

CoreData多線程結構設計

上面章節中寫的大多都是怎麼用CoreData多線程,在掌握多線程的使用後,就能夠根據公司業務需求,設計一套CoreData多線程結構了。對於多線程結構的設計,應該本着儘可能減小主線程壓力的角度去設計,將全部耗時操做都放在子線程中執行。

對於具體的設計我根據不一樣的業務需求,給出兩種設計方案的建議。

兩層設計方案

在項目中多線程操做比較簡單時,能夠建立一個主隊列mainMOC,和一個或多個私有隊列的backgroundMOC。將全部backgroundMOCparentContext設置爲mainMOC,採起這樣的兩層設計通常就可以知足大多數需求了。

兩層設計方案

將耗時操做都放在backgroundMOC中執行,mainMOC負責全部和UI相關的操做。全部和UI無關的工做都交給backgroundMOC,在backgroundMOC對數據發生改變後,調用save方法會將改變pushmainMOC中,再由mainMOC執行save方法將改變保存到存儲區。

代碼這裏就不寫了,和上面例子中設置parentContext代碼同樣,主要講一下設計思路。

三層設計方案

可是咱們發現,上面的save操做最後仍是由mainMOC去執行的,backgroundMOC只是負責處理數據。雖然mainMOC只執行save操做並不會很耗時,可是若是save涉及的數據比較多,這樣仍是會對性能形成影響的。

雖然客戶端不多涉及到大量數據處理的需求,可是假設有這樣的需求。能夠考慮在兩層結構之上,給mainMOC之上再添加一個parentMOC,這個parentMOC也是私有隊列的MOC,用於處理save操做。

三層設計方案

這樣CoreData存儲的結構就是三層了,最底層是backgroundMOC負責處理數據,中間層是mainMOC負責UI相關操做,最上層也是一個backgroundMOC負責執行save操做。這樣就將影響UI的全部耗時操做全都剝離到私有隊列中執行,使性能達到了很好的優化。

須要注意的是,執行MOC相關操做時,不要阻塞當前主線程。全部MOC的操做應該是異步的,不管是子線程仍是主線程,儘可能少的使用同步block方法。

MOC同步時機

設置MOCparentContext屬性以後,parent對於child的改變是知道的,可是child對於parent的改變是不知道的。蘋果這樣設計,應該是爲了更好的數據同步。

Employee *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:backgroundMOC];
emp.name = @"lxz";
emp.brithday = [NSDate date];
emp.height = @1.7f;

[backgroundMOC performBlock:^{
    [backgroundMOC save:nil];
    [mainMOC performBlock:^{
        [mainMOC save:nil];
    }];
}];

在上面這段代碼中,mainMOCbackgroundMOCparentContext。在backgroundMOC執行save方法前,backgroundMOCmainMOC都不能獲取到Employee的數據,在backgroundMOC執行完save方法後,自身上下文發生改變的同時,也將改變pushmainMOC中,mainMOC也具備了Employee對象。

因此在backgroundMOCsave方法執行時,是對內存中的上下文作了改變,當擁有PSCmainMOC執行save方法後,是對本地存儲區作了改變。


好多同窗都問我有Demo沒有,其實文章中貼出的代碼組合起來就是個Demo。後來想了想,仍是給本系列文章配了一個簡單的Demo,方便你們運行調試,後續會給全部博客的文章都加上Demo

Demo只是來輔助讀者更好的理解文章中的內容,應該博客結合Demo一塊兒學習,只看Demo仍是不能理解更深層的原理Demo中幾乎每一行代碼都會有註釋,各位能夠打斷點跟着Demo執行流程走一遍,看看各個階段變量的值。

Demo地址劉小壯的Github


這兩天更新了一下文章,將CoreData系列的六篇文章整合在一塊兒,作了一個PDF版的《CoreData Book》,放在我Github上了。PDF上有文章目錄,方便閱讀。

若是你以爲不錯,請把PDF幫忙轉到其餘羣裏,或者你的朋友,讓更多的人瞭解CoreData,衷心感謝!😁

相關文章
相關標籤/搜索