<簡書 — 劉小壯> http://www.jianshu.com/p/283e67ba12a3git
CoreData
使用相關的技術點已經講差很少了,我所掌握的也就這麼多了....在本篇文章中主要講
CoreData
的多線程,其中會包括併發隊列類型、線程安全等技術點。我對多線程的理解可能不是太透徹,文章中出現的問題還請各位指出。在以後公司項目使用CoreData
的過程當中,我會將其中遇到的多線程相關的問題更新到文章中。github在文章的最後,會根據我對
CoreData
多線程的學習,以及在工做中的具體使用,給出一些關於多線程結構的設計建議,各位能夠當作參考。sql文章中若有疏漏或錯誤,還請各位及時提出,謝謝!😊數據庫
在CoreData
中MOC
是支持多線程的,能夠在建立MOC
對象時,指定其併發隊列的類型。當指定隊列類型後,系統會將操做都放在指定的隊列中執行,若是指定的是私有隊列,系統會建立一個新的隊列。但這都是系統內部的行爲,咱們並不能獲取這個隊列,隊列由系統所擁有,並由系統將任務派發到這個隊列中執行的。緩存
init
方法初始化上下文,默認就是這個併發類型。這個枚舉值是不支持多線程的,從名字上也體現出來了。UI
相關的操做,應該考慮使用這個枚舉值初始化上下文。其中NSConfinementConcurrencyType
類型在iOS9
以後已經被蘋果廢棄,不建議使用這個API
。使用此類型建立的MOC
,調用某些比較新的CoreData
的API
可能會致使崩潰。安全
在CoreData
中MOC不是線程安全的,在多線程狀況下使用MOC
時,不能簡單的將MOC
從一個線程中傳遞到另外一個線程中使用,這並非CoreData
的多線程,並且會出問題。對於MOC
多線程的使用,蘋果給出了本身的解決方案。數據結構
在建立的MOC
中使用多線程,不管是私有隊列仍是主隊列,都應該採用下面兩種多線程的使用方式,而不是本身手動建立線程。調用下面方法後,系統內部會將任務派發到不一樣的隊列中執行。能夠在不一樣的線程中調用MOC
的這兩個方法,這個是容許的。多線程
- (void)performBlock:(void (^)())block 異步執行的block,調用以後會馬上返回。 - (void)performBlockAndWait:(void (^)())block 同步執行的block,調用以後會等待這個任務完成,纔會繼續向下執行。
下面是多線程調用的示例代碼,在多線程的環境下執行MOC
的save
方法,就是將save
方法放在MOC
的block
體中異步執行,其餘方法的調用也是同樣的。併發
[context performBlock:^{ [context save:nil]; }];
可是須要注意的是,這兩個block
方法不能在NSConfinementConcurrencyType
類型的MOC
下調用,這個類型的MOC
是不支持多線程的,只支持其餘兩種併發方式的MOC
。異步
在業務比較複雜的狀況下,須要進行大量數據處理,而且還須要涉及到UI
的操做。對於這種複雜需求,若是都放在主隊列中,對性能和界面流暢度都會有很大的影響,致使用戶體驗很是差,下降屏幕FPS
。對於這種狀況,能夠採起多個MOC
配合的方式。
CoreData
多線程的發展中,在iOS5
經歷了一次比較大的變化,以後能夠更方便的使用多線程。從iOS5
開始,支持設置MOC
的parentContext
屬性,經過這個屬性能夠設置MOC
的父MOC
。下面會針對iOS5
以前和以後,分別講解CoreData
的多線程使用。
儘管如今的開發中早就不兼容iOS5
以前的系統了,可是做爲了解這裏仍是要講一下,並且這種同步方式在iOS5
以後也是能夠正常使用的,也有不少人還在使用這種同步方式,下面其餘章節也是同理。
在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
能夠設置parentContext
,一個parentContext
能夠擁有多個ChildContext
。在ChildContext
執行save
操做後,會將操做push
到parentContext
,由parentContext
去完成真正的save
操做,而ChildContext
全部的改變都會被parentContext
所知曉,這解決了以前MOC
手動同步數據的問題。
須要注意的是,在ChildContext
調用save
方法以後,此時並無將數據寫入存儲區,還須要調用parentContext
的save
方法。由於ChildContext
並不擁有PSC
,ChildContext
也不須要設置PSC
,因此須要parentContext
調用PSC
來執行真正的save
操做。也就是隻有擁有PSC
的MOC
執行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
以前存在多個MOC
的狀況下,一個MOC
發生更改並提交存儲區後,其餘MOC
並不知道這個改變,其餘MOC
和本地存儲的數據是不一樣步的,因此就涉及到數據同步的問題。
進行數據同步時,會遇到多種複雜狀況。例如只有一個MOC
數據發生了改變,其餘MOC
更新時並無對相同的數據作改變,這樣不會形成衝突,能夠直接將其餘MOC
更新。
若是在一個MOC
數據發生改變後,其餘MOC
對相同的數據作了改變,並且改變的結果不一樣,這樣在同步時就會形成衝突。下面將會按照這兩種狀況,分別講一下不一樣狀況下的衝突處理方式。
簡單狀況下的數據同步,是針對於只有一個MOC
的數據發生改變,並提交存儲區後,其餘MOC
更新時並無對相同的數據作改變,只是單純的同步數據的狀況。
在NSManagedObjectContext
類中,根據不一樣操做定義了一些通知。在一個MOC
發生改變時,其餘地方能夠經過MOC
中定義的通知名,來獲取MOC
發生的改變。在NSManagedObjectContext
中定義了下面三個通知:
MOC
將要向存儲區存儲數據時,調用這個通知。在這個通知中不能獲取發生改變相關的NSManagedObject
對象。MOC
向存儲區存儲數據後,調用這個通知。在這個通知中能夠獲取改變、添加、刪除等信息,以及相關聯的NSManagedObject
對象。MOC
中任何一個託管對象發生改變時,調用這個通知。例如修改託管對象的屬性。經過監聽NSManagedObjectContextDidSaveNotification
通知,獲取全部MOC
的save
操做。
[[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
返回值的選項。
NSError
對象來描述錯誤,而MOC
和持久化存儲區不發生改變。MOC
的爲準,使用MOC
來覆蓋本地存儲的衝突部分。MOC
爲準,用MOC
的全部NSManagedObject
對象覆蓋本地存儲的對應對象。MOC
全部的NSManagedObject
對象被本地存儲的對應對象所覆蓋。上面五種策略中,除了第一個NSErrorMergePolicy
的策略,其餘四種中NSMergeByPropertyStoreTrumpMergePolicy
和NSRollbackMergePolicy
,以及NSMergeByPropertyObjectTrumpMergePolicy
和NSOverwriteMergePolicy
看起來是重複的。
其實它們並非衝突的,這四種策略的不一樣體如今,對沒有發生衝突的部分應該怎麼處理。NSMergeByPropertyStoreTrumpMergePolicy
和NSMergeByPropertyObjectTrumpMergePolicy
對沒有衝突的部分,未衝突部分數據並不會受到影響。而NSRollbackMergePolicy
和NSOverwriteMergePolicy
則是不管是否衝突,直接所有替換。
題外話:
對於MOC
的這種合併策略來看,有木有感受到CoreData
解決衝突的方式,和SVN
解決衝突的方式特別像。。。
不管是MOC
仍是託管對象,都不該該在其餘MOC
的線程中執行操做,這兩個API都不是線程安全的。但MOC
能夠在其餘MOC
線程中調用performBlock:
方法,切換到本身的線程執行操做。
若是其餘MOC
想要拿到託管對象,並在本身的隊列中使用託管對象,這是不容許的,託管對象是不能直接傳遞到其餘MOC
的線程的。可是能夠經過獲取NSManagedObject
的NSManagedObjectID
對象,在其餘MOC
中經過NSManagedObjectID
對象,從持久化存儲區中獲取NSManagedObject
對象,這樣就是容許的。NSManagedObjectID
是線程安全,而且能夠跨線程使用的。
能夠經過MOC
獲取NSManagedObjectID
對應的NSManagedObject
對象,例以下面幾個MOC
的API
。
NSManagedObject *object = [context objectRegisteredForID:objectID]; NSManagedObject *object = [context objectWithID:objectID];
經過NSManagedObject
對象的objectID
屬性,獲取NSManagedObjectID
類型的objectID
對象。
NSManagedObjectID *objectID = object.objectID;
上面章節中寫的大多都是怎麼用CoreData
多線程,在掌握多線程的使用後,就能夠根據公司業務需求,設計一套CoreData
多線程結構了。對於多線程結構的設計,應該本着儘可能減小主線程壓力的角度去設計,將全部耗時操做都放在子線程中執行。
對於具體的設計我根據不一樣的業務需求,給出兩種設計方案的建議。
在項目中多線程操做比較簡單時,能夠建立一個主隊列mainMOC
,和一個或多個私有隊列的backgroundMOC
。將全部backgroundMOC
的parentContext
設置爲mainMOC
,採起這樣的兩層設計通常就可以知足大多數需求了。
將耗時操做都放在backgroundMOC
中執行,mainMOC
負責全部和UI
相關的操做。全部和UI
無關的工做都交給backgroundMOC
,在backgroundMOC
對數據發生改變後,調用save
方法會將改變push
到mainMOC
中,再由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
的parentContext
屬性以後,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]; }]; }];
在上面這段代碼中,mainMOC
是backgroundMOC
的parentContext
。在backgroundMOC
執行save
方法前,backgroundMOC
和mainMOC
都不能獲取到Employee
的數據,在backgroundMOC
執行完save
方法後,自身上下文發生改變的同時,也將改變push
到mainMOC
中,mainMOC
也具備了Employee
對象。
因此在backgroundMOC
的save
方法執行時,是對內存中的上下文作了改變,當擁有PSC
的mainMOC
執行save
方法後,是對本地存儲區作了改變。
好多同窗都問我有Demo
沒有,其實文章中貼出的代碼組合起來就是個Demo
。後來想了想,仍是給本系列文章配了一個簡單的Demo
,方便你們運行調試,後續會給全部博客的文章都加上Demo
。
Demo
只是來輔助讀者更好的理解文章中的內容,應該博客結合Demo
一塊兒學習,只看Demo
仍是不能理解更深層的原理。Demo
中幾乎每一行代碼都會有註釋,各位能夠打斷點跟着Demo
執行流程走一遍,看看各個階段變量的值。
Demo地址:劉小壯的Github
這兩天更新了一下文章,將CoreData
系列的六篇文章整合在一塊兒,作了一個PDF
版的《CoreData Book》,放在我Github上了。PDF
上有文章目錄,方便閱讀。
若是你以爲不錯,請把PDF幫忙轉到其餘羣裏,或者你的朋友,讓更多的人瞭解CoreData,衷心感謝!😁