Core Data: 多線程大量數據同步

 

http://www.jianshu.com/p/37ab8f336f76javascript

 

前言:本文是我幾個月前的這篇《iOS 面試基礎題目》 其中的一個問題的回答,這幾天整理博客,更新內容,自覺有能力回答這個問題了。這篇單獨拿出來首先是由於這個問題很不錯,值得單獨寫一篇;其次爲了便於檢索,由於簡書目前不支持標籤,只能經過文集來分類,有點不方便,折騰個優美的基於 Github 的博客又嫌麻煩,暫時仍是在這裏寫吧,等有精力了遷移;最後是由於這個回答寫得太長了,本來打算寫個大綱型的,但因爲回答是定位於基礎,因此加了不少基礎知識的介紹和補充,這樣一來,原文就更長了。我這裏的不少文章都寫得太長,羅裏吧嗦的,我已經難以忍受,但目前我還沒太多能力進行精簡。
老實說,當時寫的東西大部分只是搬運而已,是個人博客裏很水的一篇,但倒是我這麼多文章裏最受歡迎的一篇。能說明什麼,你們都很水唄,水沒關係,日拱一卒,共勉。html

第一步:搭建 Core Data 多線程環境
這個問題首先要解決的是搭建 Core Data 多線程環境。Core Data 對併發模式的支持很是完備,NSManagedObjectContext 的指定初始化方法中就指定了併發模式:java

init(concurrencyType ct: NSManagedObjectContextConcurrencyType)

有三種模式:
1.NSConfinementConcurrencyType
這種模式是用於向後兼容的,使用這種模式時你應該保證只能在建立的線程裏使用 context,然而這不容易獲得保證。關於此模式的最新消息是 iOS 9 中它將被廢棄,不推薦使用。
2.NSPrivateQueueConcurrencyType
在一個私有隊列中建立並管理 context。
3.NSMainQueueConcurrencyType
其實這種模式與第2種模式比較類似,只不過 context 與主隊列綁定,同時也所以與應用的 event loop 緊密相連。當 context 與 UI 更新相關的話就使用這種模式。ios

從 iOS 9 開始就剩下後面兩種模式了,那麼搭建多線程 Core Data 環境的方案通常以下,建立一個 NSMainQueueConcurrencyType 的 context 用於響應 UI 事件,其餘涉及大量數據操做可能會阻塞 UI 的,就使用 NSPrivateQueueConcurrencyType 的 context。git

let mainContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType) let backgroundContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)

但須要注意的是「Core Data 使用線程或者序列化的隊列來保護 managed objects 和 managed object context,所以 context 假設它的默認擁有者是它初始化時分配的線程或隊列,你不能在某個線程中初始化一個 context 後傳遞給另一個線程來使用它。」這段蹩腳的話是我從NSManagedObjectContext的文檔翻譯來的,意思就是說 managed object context 並不是線程安全的,你不能隨便地開啓一個後臺線程訪問 managed object context 進行數據操做就管這叫支持多線程了,那麼應該怎麼作呢?官方文檔《Using a Private Queue to Support Concurrency》爲咱們作了示範,在 private queue 的 context 中進行操做時,應該使用如下方法:github

func performBlock(_ block: () -> Void)//在私有隊列中異步地執行 Blcok func performBlockAndWait(_ block: () -> Void)//在私有隊列中執行 Block 直至操做結束才返回

要在不一樣線程中使用 managed object context 時,不須要咱們建立後臺線程而後訪問 managed object context 進行操做,而是交給 context 自身綁定的私有隊列去處理,咱們只須要在上述兩個方法的 Block 中執行操做便可。事實上,你也能夠在其餘線程中來使用 context,可是要保證以上兩個方法。並且,在 NSMainQueueConcurrencyType 的 context 中也應該使用這種方法執行操做,這樣能夠確保 context 自己在主線程中進行操做。面試

題外話,在構建多線程 context 時,常常會出現這樣的局面:Multi-contexts vs Concurrency。前者可能有更加複雜的狀況,在 iOS 5以後,context 能夠指定父 context,persistent store coordinator 再也不是其與 persistent store 聯繫的惟一選擇。須要注意的是,子 context 的 fetch 和 save 操做都會交給父 context 來完成,對於子 context 的 save 操做,只會到達上一層的父 context 裏,只有父 context 執行了 save 操做,子 context 中的變化纔會提交到 persistent store 保存。這種子 context 適合在後臺執行長時間操做,好比在後臺裏在子 context 裏導入大量數據,在主線程的父 context 裏更新進度。另一種是平行的多 context。Concurrency 的特性也是在 iOS 5 後開始支持,這個特性減小了平行的多 context 的需求。關於這個話題,能夠看這篇文章:《Concurrent Core Data Stacks – Performance Shootout》swift

第二步:數據的同步操做
總的來講,在多 context 環境的下,context 的生命週期裏有兩個階段須要處理數據同步的問題。當某個 context 裏的狀態發生了變化並執行保存來更新 persistent store後,對於其餘 context 來講有兩個選擇:1. persistent store 更新後,此時其餘 context 與 persistent store 進行同步;2. persistent store 更新後,其餘 context 並不當即同步,而在自身進行保存時與 persistent store 進行同步,二者有差別時須要解決衝突問題。前者採起的是「一處修改到處同步」的策略,全部的 context 中同步爲一個版本,後者採起的是「多版本共存協商處理」的策略。如下討論都基於多個 context,假設應用配置了兩個 managed object context,mainContext,在主線程運行 ,另一個 backgroundContext 用於後臺處理。安全

多 context 單數據版本

在 context 中執行保存時,應用並不會主動告知其餘 context。那麼如何在多個 context 間進行通訊呢?Core Data 提供了通知機制,context 執行保存時,會發出如下通知:
1.NSManagedObjectContextWillSaveNotification
Managed object context 即將執行保存操做時發出此通知,沒有附帶任何信息。
2.NSManagedObjectContextDidSaveNotification
Managed object context 的保存操做完成以後由該 context 自動發出此通知,包含了全部的新增、更新和刪除的對象的信息。注意在通知裏的 managed objects 只能在 context 所在的線程裏使用。因爲 context 也只能在自身的線程裏執行操做,因此無法直接使用通知裏的 managed objects,這時候應該經過 managed object 的 objectID 以及 context 的objectWithID(_ objectID: ) 來獲取。多線程

合併其餘 context 的數據能夠經過mergeChangesFromContextDidSaveNotification(_ notification:)來完成。在這個方法裏,context 會更新發出通知的 context 裏變化的任何一樣的對象,引入新增的對象並處於 faults 狀態,並刪除在發出通知的 context 裏已經刪除的對象。

代碼示例,在 backgroundContext 中編輯後使 mainContext 與之同步:

NSNotificationCenter.defaultCenter().addObserver(self, selector: "backgroundContextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: backgroundContext) func backgroundContextDidSave(notification: NSNotification){ mainContext.performBlock(){ mainContext.mergeChangesFromContextDidSaveNotification(notification) } }
多 context 多數據版本

在這種方案下,backgroundContext 並不針對 mainContext 的保存作出反應。在 mainContext 和 backgroundContext 中 fetch 了同類的 managed objects,兩個 context 都發生了變化而且變化不同,此時讓 backgroundContext 與 mainContext 先後腳分別執行保存的話,就會發生衝突致使後者保存失敗。

在 managed object context 中執行 fetch 操做時,會對 persistent store 裏的狀態進行快照,當 context 執行保存時,會使用快照與 persistent store 進行對比,若是狀態不一致,說明 persistent store 在其餘地方被更改了,而這個變化並非當前 context 形成的,這樣就形成了當前 context 狀態的不連續,此時保存就會產生衝突。這裏須要介紹 managed object context 的屬性mergePolicy,這個屬性指定了 context 的合併策略,決定了保存時合併數據發生衝突時如何應對,該屬性有如下幾種值:
1.NSErrorMergePolicy
默認策略,有衝突時保存失敗,persistent store 和 context 都維持原樣,並返回錯誤信息,是惟一反饋錯誤信息的合併策略。
2.NSMergeByPropertyStoreTrumpMergePolicy
當 persistent store 和 context 裏的版本有衝突,persistent store 裏的版本有優先權, context 裏使用 persistent store 裏的版本替換。
3.NSMergeByPropertyObjectTrumpMergePolicy
與上面相反,context 裏的版本有優先權,persistent store 裏使用 context 裏的版本替換。
4.NSOverwriteMergePolicy
用 context 裏的版本強制覆蓋 persistent store 裏的版本。
5.NSRollbackMergePolicy
放棄 context 中的全部變化並使用 persistent store 中的版本進行替換。

除了默認的 NSErrorMergePolicy在發生衝突時返回錯誤等待下一步處理外,其餘的合併策略直接根據自身的規則來處理合並衝突,所以在選擇時要謹慎處理。從上面的解釋來看,彷佛NSMergeByPropertyStoreTrumpMergePolicyNSRollbackMergePolicy沒什麼區別,NSMergeByPropertyObjectTrumpMergePolicyNSOverwriteMergePolicy也沒有什麼區別。區別在於怎麼對待被覆蓋的一方中沒有衝突的變化(解釋見此處)NSMergeByPropertyStoreTrumpMergePolicyNSMergeByPropertyObjectTrumpMergePolicy採起的是局部替換,前者 context 中沒有衝突的變化不會受到影響,後者 persistent store 中沒有衝突的變化不受影響;NSOverwriteMergePolicyNSRollbackMergePolicy 採起的是全局替換,persistent store 和 context 中只有一方的狀態得以保留。

回到本節開始的場景,mainContext 和 backgroundContext 中的版本不一致,會產生合併衝突,解決方案有如下兩種選擇:
1.無論 mainContext 中是否發生改變,與 backgroundContext 中狀態同步;

//此時 mainContext 和 backgroundContext 都採用默認合併策略便可。 mainContext.performBlock(){ do{ try mainContext.save() } catch { /* 清空 mainContext,其中全部的 managed objects 消失。 若是引用了其中的 managed objects 的話,注意在 reset 前取消對這些對象的引用。 */ mainContext.reset() //從新 fetch let fetchRequest = ... let updatedFetchedResults = try mainContext.executeFetchRequest(fetchRequest) } }

又或者,mainContext 的合併策略採用NSMergeByPropertyStoreTrumpMergePolicyNSRollbackMergePolicy,這樣就省去了 reset 操做。實際上,採用這種方案不如上一個策略來得方便。
2.無論其餘 context 中發生什麼變化,當前 context 進行保存時直接覆蓋 persistent store 裏的版本,這種方案下 context 的合併策略須要採用NSOverwriteMergePolicyTypeNSMergeByPropertyObjectTrumpMergePolicy,並且執行保存時不會返回錯誤,不須要後續的處理。

小結

同步多個 context 是個比較複雜的事情,須要根據具體的須要來設定 context 的合併策略以及選擇同步的時機,不只僅限於以上的兩種策略,融合兩種策略也能夠,固然那樣可能會大大增長複雜度,更容易致使 Bug。另外,還有一種使用 child context 的方法,就是將其餘 context 做爲 context 的 parentContext,這種方法沒有研究,本身有興趣能夠試試。
1.同步問題第一原則:不要跨線程使用 managed object,而應該經過其對應的 objectID,在其餘線程裏的 context 裏來獲取對象。
2.NSManagedObjectContext的合併方法mergeChangesFromContextDidSaveNotification(_ notification:)能夠替徹底複製另外一個 context 的狀態;若是你不想徹底複製,可使用更精確的方法refreshAllObjects(),這是 iOS 9 中推出的新方法;或者手動處理,固然,不推薦這麼作。
3.利用NSMergePolicy來處理同步相對而言危險一點,你得明確知道你在作什麼。

最後一站:大量數據操做

從上面的內容能夠得知,在多線程環境下同步數據基本上不須要咱們手動去處理 managed objects 的同步,所以處理大量數據的同步,關注的重點更多在於內存佔用和性能。寫代碼要記住如下幾點:
1.涉及大量數據的操做盡可能要放在後臺線程裏處理,防止阻塞主線程;對於多 context 的結構,能夠參考這篇文章《Concurrent Core Data Stacks – Performance Shootout》,做者經過驗證,證實了「設置一個 persistent store coordinator 和兩個獨立的 contexts 被證實了是在後臺處理 Core Data 的好方法」。
2.可以保持 faults 狀態的 managed objects 儘可能不要觸發 fire,下降內存佔用,同時也能提高響應速度。
3.fetch 大量數據時注意技巧,能夠經過利用 predicate 來篩選實際須要的數據,限制 fetch 的總數量,設定合適的批量獲取數量來下降 IO 的頻次,這些須要在實際環境中尋找平衡點。
4.儘可能讓 context 中的 entity 類別少一些,下降對同步的需求。
(從 iOS 8 開始,Core Data 在性能方面有了較大的提高,儘可能合理利用。)
5.使用異步請求 Asynchronous Fetch,儘管能夠將 fetch 大量數據的操做放在後臺線程裏,可是這樣依然會阻塞那個線程,使用異步請求,則依然能夠在後臺線程裏進行其餘操做,而且還有方便的進度提示和取消功能。
6.使用批量更新 Batch Update,有效下降內存佔用並大幅提高保存的速度。以往在NSManagedObjectContext中進行保存時,只能將其中包含的變化進行保存,而 Batch Update 則是直接對 persistent store 進行更新而不須要將 managed objects 讀入內存,能夠大幅下降內存佔用並且更新速度提高很多。但須要注意的是,使用批量更新並不會提醒 context,須要咱們對 context 手動進行更新,並且沒有進行有效驗證,也須要開發者來保證有效性。
7.使用批量刪除 Batch Delete,與批量更新相似,直接對 persistent store 進行操做,效率很是高,也有着和批量更新相似的問題。
8.使用 iOS 9 新增的NSManagedObjectContext的新 API:refreshAllObjects(),該方法會對 context 中註冊的全部對象進行刷新,尚未保存的變化也會獲得保留,這樣就能夠解放 6 和 7 中的手動更新工做。

Core Data

△Ο✕口

讚揚支持
登陸 後發表評論
 
浮海_2015
3樓 · 2015.11.18 10:57

有一個疑問,好比一個backgroundContext 刪除了幾條數據 ,而對應的mainContext對這幾天數據進行了操做;若是對mainContext進行了save,更新了到了PSC(persistent store),此時對backgroundContext進行了refreshAllObjects(),那刪除的數據還在不?應對這個conflict, mergeChangesFromContextDidSaveNotification和refreshAllObjects(),有差異嗎?仍是說,就是徹底還原了數據,而後再次進行數據操做...還有

if #available(iOS 9, *) {
//隻影響 mainContext 中註冊的 managed objects,但不引入 backgroundContext 新添加的對象。
mainContext.refreshAllObjects()
}

這段代碼,若是說refreshAllObjects徹底還原了數據,後面沒了數據操做,仍是沒有保存成功,這還有意義嗎

seedante@fory2015 refreshAllObjects() 會保留還沒有保存的變化,那麼mainContext 裏刪除了東西保存後,在 backgroundContext 裏 refreshAllObjects() 後還會留下那些被刪除的對象,但 PSC裏面已經刪除了,因此這裏我犯了錯,不該該這樣處理。我修改下文章。

2015.11.18 15:54 回覆
華落
4樓 · 2015.12.11 17:16

博主,您好,這段時間在研究CoreData,關於多線程我看到了 設置層級上下文的策略,還有今天看到您的文章,多上下文同步數據,我很是的不明白,我親自試驗了一下,直接使用一個NSPrivateQueueConcurrencyType的context。調用performBolck方法,執行,贈,刪,改,查,徹底是可行的,而後調用dispath mianqueue,刷新界面,刷新數據,徹底是能夠的啊,爲何要使用NSMainQueueConcurrencyType的context呢,還要同步2個上下文的數據,還請博主解答,多謝多謝 :sob:

_backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];

[_backgroundContext performBlockAndWait:^{
[_backgroundContext setPersistentStoreCoordinator:_coordinator];
[_backgroundContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
}];
//得到數據
[self.cdh.backgroundContext performBlock:^{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
NSArray *ary = [self.cdh.backgroundContext executeFetchRequest:request error:nil];
[weakSelf.array addObjectsFromArray:ary];
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.tableView reloadData];
});
}];

//修改數據

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];

Person *p = self.array[indexPath.row];
p.nName = @"saaa";
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
NSManagedObjectContext *context = self.cdh.backgroundContext;
[context performBlock:^{
[context save:nil];
}];
}

seedante@華落 你在這個場景很簡單的例子裏這麼作沒有問題,其實這個例子裏就不須要多線程 context,還有同步的麻煩。多線程 context 的例子能夠看這裏:http://objccn.io/issue-2-2/

2015.12.11 20:45 回覆

華落@seedante 感謝博主的回覆,我大概看了一下代碼,做者好像使用了NSFetchedResultsController,這個是內嵌的一些通知來更新tableview,並且NSFetchedResultsController好像只能接受 mainqueue形式的context,額。經過合併數據到主context,我以爲NSFetchedResultsController仍是會在主線程執行一次查詢操做,而後刷新表格,其實在拉動的時候仍是很卡,不過已經很好了吧,我仍是有點不理解,
NSArray *ary = [self.cdh.backgroundContext executeFetchRequest:request error:nil];
[weakSelf.array addObjectsFromArray:ary];
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.tableView reloadData];
});
我這句可能跨線程訪問了把- ,-

2015.12.12 10:01 回覆

seedante@華落 主線程只有一個,因此你在 backgroundContext 的私有隊列裏切換到主隊列裏刷新 UI 是沒有問題的。你說『NSFetchedResultsController好像只能接受 mainqueue形式的context』,刷新 UI 只能在主線程裏進行,若是 NSFetchedResultsController 發現綁定的 context 發生變化而刷新 UI 時不在主線程,一定有延遲和卡頓,你大概就是你說的好像只能接受mainqueue形式的context。

2015.12.12 10:18 回覆
添加新評論 還有2條評論, 展開查看
做者:seedante 連接:http://www.jianshu.com/p/37ab8f336f76 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索