你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 認識CoreData—多線程

認識CoreData—多線程

編輯:IOS開發基礎

QQ截圖20160803141824.jpg

投稿文章,作者:劉小壯(簡書)

導讀:

認識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操作的實現。

1470205214468279.png

多線程結構

這樣做有一個問題,當一個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,采取這樣的兩層設計一般就能夠滿足大多數需求了。

270478-43c4efa5c045e963.png

兩層設計方案

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

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

三層設計方案

但是我們發現,上面的save操作最後還是由mainMOC去執行的,backgroundMOC只是負責處理數據。雖然mainMOC只執行save操作並不會很耗時,但是如果save涉及的數據比較多,這樣還是會對性能造成影響的。

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

270478-e9801da2ca62cdd9.png

三層設計方案

這樣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方法後,是對本地存儲區做了改變。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved