投稿文章,作者:劉小壯(簡書)
導讀:
認識CoreData—初識CoreData
認識CoreData—基礎使用
認識CoreData—使用進階
認識CoreData—高級用法
正文:
CoreData使用相關的技術點已經講差不多了,我所掌握的也就這麼多了....
在本篇文章中主要講CoreData的多線程,其中會包括並發隊列類型、線程安全等技術點。我對多線程的理解可能不是太透徹,文章中出現的問題還請各位指出。在之後公司項目使用CoreData的過程中,我會將其中遇到的多線程相關的問題更新到文章中。
在文章的最後,會根據我對CoreData多線程的學習,以及在工作中的具體使用,給出一些關於多線程結構的設計建議,各位可以當做參考。
文章中如有疏漏或錯誤,還請各位及時提出,謝謝!
MOC並發隊列類型
在CoreData中MOC是支持多線程的,可以在創建MOC對象時,指定其並發隊列的類型。當指定隊列類型後,系統會將操作都放在指定的隊列中執行,如果指定的是私有隊列,系統會創建一個新的隊列。但這都是系統內部的行為,我們並不能獲取這個隊列,隊列由系統所擁有,並由系統將任務派發到這個隊列中執行的。
NSManagedObjectContext並發隊列類型:
NSConfinementConcurrencyType : 如果使用init方法初始化上下文,默認就是這個並發類型。這個枚舉值是不支持多線程的,從名字上也體現出來了。
NSPrivateQueueConcurrencyType : 私有並發隊列類型,操作都是在子線程中完成的。
NSMainQueueConcurrencyType : 主並發隊列類型,如果涉及到UI相關的操作,應該考慮使用這個枚舉值初始化上下文。
其中NSConfinementConcurrencyType類型在iOS9之後已經被蘋果廢棄,不建議使用這個API。使用此類型創建的MOC,調用某些比較新的CoreData的API可能會導致崩潰。
MOC多線程調用方式
在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之後也是可以正常使用的,也有很多人還在使用這種同步方式,下面其他章節也是同理。
iOS 5之前使用多個MOC
在iOS 5之前實現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操作後,會將操作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之前進行數據同步
就像上面章節中講到的,在iOS5之前存在多個MOC的情況下,一個MOC發生更改並提交存儲區後,其他MOC並不知道這個改變,其他MOC和本地存儲的數據是不同步的,所以就涉及到數據同步的問題。
進行數據同步時,會遇到多種復雜情況。例如只有一個MOC數據發生了改變,其他MOC更新時並沒有對相同的數據做改變,這樣不會造成沖突,可以直接將其他MOC更新。
如果在一個MOC數據發生改變後,其他MOC對相同的數據做了改變,而且改變的結果不同,這樣在同步時就會造成沖突。下面將會按照這兩種情況,分別講一下不同情況下的沖突處理方式。
簡單情況下的數據同步
簡單情況下的數據同步,是針對於只有一個MOC的數據發生改變,並提交存儲區後,其他MOC更新時並沒有對相同的數據做改變,只是單純的同步數據的情況。
在NSManagedObjectContext類中,根據不同操作定義了一些通知。在一個MOC發生改變時,其他地方可以通過MOC中定義的通知名,來獲取MOC發生的改變。在NSManagedObjectContext中定義了下面三個通知:
NSManagedObjectContextWillSaveNotification:MOC將要向存儲區存儲數據時,調用這個通知。在這個通知中不能獲取發生改變相關的NSManagedObject對象。
NSManagedObjectContextDidSaveNotification:MOC向存儲區存儲數據後,調用這個通知。在這個通知中可以獲取改變、添加、刪除等信息,以及相關聯的NSManagedObject對象。
NSManagedObjectContextObjectsDidChangeNotification:在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返回值的選項。
NSErrorMergePolicy : 默認值,當出現合並沖突時,返回一個NSError對象來描述錯誤,而MOC和持久化存儲區不發生改變。
NSMergeByPropertyStoreTrumpMergePolicy : 以本地存儲為准,使用本地存儲來覆蓋沖突部分。
NSMergeByPropertyObjectTrumpMergePolicy : 以MOC的為准,使用MOC來覆蓋本地存儲的沖突部分。
NSOverwriteMergePolicy : 以MOC為准,用MOC的所有NSManagedObject對象覆蓋本地存儲的對應對象。
NSRollbackMergePolicy : 以本地存儲為准,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多線程,在掌握多線程的使用後,就可以根據公司業務需求,設計一套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同步時機
設置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方法後,是對本地存儲區做了改變。