coredata的并發(fā)處理
“我想要高可響應(yīng)性的app账锹,它允許我即使離線時(shí)候也能瀏覽數(shù)據(jù)”-我們常常聽(tīng)到有人這么說(shuō)授段。
諷刺的是,當(dāng)用coredata處理數(shù)據(jù)的時(shí)候恕齐,它會(huì)成為你的應(yīng)用的核心。除了管理你的app內(nèi)存中持有數(shù)據(jù)的方式巨缘,coredata還處理高級(jí)數(shù)據(jù)查詢(NSPredicate)娄周,懶加載(faults)端三,undo和redo支持谨履,表遷移害捕,UI整合(NSFetchedResultsController)躬它,合并策略以及其他相關(guān)事情。
Coredata可以是你的朋友菇夸,也可以是你的敵人鞠眉,這取決于你使用它的方式:如果在主線程處理,coredata會(huì)顯著的抑制程序性能。但是副渴,如果你想異步的使用coredata佑颇,你需要知道幾種模式宰闰。
Coredata設(shè)計(jì)
盡管有人說(shuō)coredata有比較陡的學(xué)習(xí)曲線胶背,不過(guò)一旦你理解了這個(gè)技術(shù)它就會(huì)變得很容易理解奔誓。這個(gè)技術(shù)就是關(guān)于如何管理你的模型的對(duì)象流和措。Coredata的結(jié)構(gòu)見(jiàn)下圖:
通常SQLite數(shù)據(jù)庫(kù)是用作備份數(shù)據(jù)存儲(chǔ)派阱,但是你也能指定“原子性”或者“僅內(nèi)存”這兩種儲(chǔ)存類型贫母。要注意的是盡管在OSX上coredata支持XML數(shù)據(jù)存儲(chǔ)腺劣,但是在iOS上XML存儲(chǔ)并不可用橘原。
大致上,一個(gè)NSManagedObject實(shí)例可以被看做數(shù)據(jù)庫(kù)里表里的一條記錄吓懈。這個(gè)表的元數(shù)據(jù)被存儲(chǔ)在NSEntityDescriptor對(duì)象中耻警。NSManagedObjectContext是NSManagedObject對(duì)象們的數(shù)據(jù)池。它負(fù)責(zé)NSManagedObject的生命周期温兼,以及從底層數(shù)據(jù)存儲(chǔ)獲取數(shù)據(jù)對(duì)象募判,持久化,object faulting, undo/redo支持释液。
一個(gè)NSManagedObjectContext必須工作在一個(gè)隊(duì)列上误债∏薜福可通過(guò)兩種方式來(lái)達(dá)成箫老。第一種方式就是簡(jiǎn)單的只在創(chuàng)建NSManagedObjectContext的隊(duì)列中來(lái)使用它。這被稱作線程約束界斜。
dispatch_async(queue, ^{
NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] init];
// moc usage
});
第二種方式是將NSManagedObjectContext初始化為concurrency類型各薇,然后通過(guò)調(diào)用performBlock:^ 或者 performBlockAndWait:^來(lái)使用它峭判。如下:
NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateConcurrencyType];
[moc performBlock:^{
// use moc in this block
}];
一共有三種異步類型可用:
- NSConfinementConcurrencyType - 盡管很多人使用,但是這個(gè)類型已經(jīng)被蘋(píng)果官方淘汰了疗认。這個(gè)類型的意思是:你必須手動(dòng)保證MOC在創(chuàng)建它的隊(duì)列內(nèi)使用横漏。這種情況下缎浇,你應(yīng)該使用init方法來(lái)做初始化素跺,而不是initWithConcurrencyType方法刊愚。
- NSPrivateQueueConcurrencyType - 意味著[moc performBlock:]將在后臺(tái)隊(duì)列中執(zhí)行。
- NSMainQueueConcurrencyType - 意味著[moc performBlock:]將在主隊(duì)列中執(zhí)行仑乌。
NSManagedObject不會(huì)被記錄到持久化存儲(chǔ)中百拓,直到NSManagedObjectContext的save方法被調(diào)用。
NSPersistentStoreCoordinator的作用是綁定持久化存儲(chǔ)和NSManagedObjectContext晰甚。通常衙传,NSPersistentStoreCoordinator有一個(gè)持久化存儲(chǔ)和多個(gè)NSManagedObjectContext,不過(guò)厕九,它其實(shí)也可以同時(shí)處理多個(gè)持久化存儲(chǔ)蓖捶。你可以認(rèn)為NSPersistentStoreCoordinator是持久化存儲(chǔ)與NSManagedObject之間的中間層。
要清楚的是扁远,UI更新是在主線程中發(fā)生俊鱼,所以在主線程中的coredata操作都會(huì)產(chǎn)生影響,并且可能導(dǎo)致視覺(jué)問(wèn)題或者性能問(wèn)題。原因是所有UI處理,繪制和用戶事件(點(diǎn)擊,手勢(shì))都在主線程中進(jìn)行冕广。運(yùn)行任何其他事情在主線程上都會(huì)對(duì)app性能產(chǎn)生影響。 If you want to know more about why it is a “failed dream” to create multithreaded UI on any platform, read “Multithreaded toolkits: A failed dream?”.
處理coredata有多重不同的方式。下面是幾種實(shí)踐中比較通用的處理方案。
方案 #1
就是把所有東西都放在主線程中處理。
第一種方法是相當(dāng)簡(jiǎn)單的一個(gè):把所有事情放在主線程處理渣玲。你需要Xcode生成的默認(rèn)的CoreData堆棧。下圖是這個(gè)堆棧的樣子:
【圖片】
這個(gè)唯一的NSManagedObjectContext是被用來(lái)UI處理和記錄數(shù)據(jù)到數(shù)據(jù)庫(kù)中星掰。使用范例如下:
1.從服務(wù)器獲取數(shù)據(jù)
2.只要數(shù)據(jù)返回威始,就展現(xiàn)某種等待提示器
3.存儲(chǔ)獲得的數(shù)據(jù)到數(shù)據(jù)庫(kù)中(插入,更新,刪除)
4.隱藏等待提示器并展示新數(shù)據(jù)燎猛。
然而作為一個(gè)非常簡(jiǎn)單的解決方案愤钾,負(fù)面影響就是在數(shù)據(jù)庫(kù)記錄數(shù)據(jù)的時(shí)候,由于它使用主線程在數(shù)據(jù)庫(kù)記錄新數(shù)據(jù)占业,似的app不可用。幸運(yùn)的是该默,知名的MBProgressHUD庫(kù)以一種聰明的方式實(shí)現(xiàn)音榜,它能在主線程被coredata任務(wù)使用的時(shí)候在主線程展示動(dòng)畫(huà)霜第。
方案 #2
將后臺(tái)寫(xiě)任務(wù)從主線程中分離双仍。
【圖片】
這里翎卓,一個(gè)工作context被引入,用來(lái)存儲(chǔ)從服務(wù)端收到的數(shù)據(jù)矩肩。一旦數(shù)據(jù)存儲(chǔ)完成,主context可以以下面兩種方式與工作context進(jìn)行合并:
1.一旦工作context完成操作肛跌,它將通知主context,這之后工作線程可以通過(guò)調(diào)用reset方法來(lái)進(jìn)行重置稳捆。
2.工作context觸發(fā)mergeChangesFromContextDidSaveNotification通知后,主context將進(jìn)行合并块请。工作context運(yùn)行save方法后會(huì)觸發(fā)上述通知最岗。
這種方法的負(fù)面影響是開(kāi)發(fā)者需要手動(dòng)進(jìn)行context間的合并。
方案 #3
利用NSManagedObjectContext間的父子關(guān)系驮樊。
在iOS5上薇正,Apple在NSManagedObjectContext間引入了父子關(guān)系片酝。使用這個(gè)可以幫助同步context之間的數(shù)據(jù)。下面是一個(gè)可能的方案:
【圖片】
這個(gè)方案擁有在后臺(tái)隊(duì)列創(chuàng)建管理對(duì)象(NSManagedObject)的好處挖腰。但是在調(diào)用save方法后雕沿,所有數(shù)據(jù)對(duì)象將傳給父context(主隊(duì)列),而且真實(shí)的寫(xiě)數(shù)據(jù)庫(kù)將依舊在主線程中發(fā)生猴仑。所以盡管本方案比方案1略強(qiáng)审轮,但是它仍然對(duì)主線程有影響。
方案 #4
與方案3反向的管理對(duì)象上下文宁脊。
【圖片】
流行庫(kù)Restkit就是使用的這種結(jié)構(gòu)断国,這此方案下,一個(gè)私有管理對(duì)象上下文被用于存儲(chǔ)從服務(wù)端獲取的數(shù)據(jù)榆苞。一旦完成,主context通過(guò)NSManagedObjectContextDidSaveNotification消息得到通知霞捡,于是主context更新到了它的父context(工作context)的數(shù)據(jù)坐漏。
在主context用于存儲(chǔ)數(shù)據(jù)時(shí)候(可以處理較小量的數(shù)據(jù)),由于在主context與工作context間的父子關(guān)系碧信,更改將被自動(dòng)合并到私有context赊琳。
方案 #5
擴(kuò)展父子關(guān)系的使用
【圖片】
這個(gè)方案很有趣。作為對(duì)于方案4的補(bǔ)充砰碴,本方案引入了一個(gè)新的并發(fā)context躏筏,這個(gè)context是主context的子context。接下來(lái)我們來(lái)看看對(duì)應(yīng)的數(shù)據(jù)流:
1.數(shù)據(jù)是異步獲取的呈枉,當(dāng)coredata在進(jìn)行處理時(shí)候用戶依然可以使用app趁尼。
2.服務(wù)端數(shù)據(jù)到達(dá)后,工作context被用來(lái)在數(shù)據(jù)庫(kù)中存儲(chǔ)數(shù)據(jù)猖辫。既然存儲(chǔ)發(fā)生在私有隊(duì)列酥泞,所以用戶依然可以和之前一樣使用app。
3.在工作context調(diào)用save方法后啃憎,數(shù)據(jù)就會(huì)被合并到主context≈ザ冢現(xiàn)在這個(gè)合并是在主隊(duì)列發(fā)生的,但是由于合并是在內(nèi)存中完成的辛萍,所以這是并不會(huì)產(chǎn)生嚴(yán)重的性能問(wèn)題悯姊。
4.最終,數(shù)據(jù)被傳遞到另一個(gè)工作context贩毕,由這個(gè)工作context來(lái)真實(shí)的把數(shù)據(jù)寫(xiě)到存儲(chǔ)里悯许。
這種方法的負(fù)面影響是,由于有3個(gè)context進(jìn)行合并耳幢,所有這些合并工作對(duì)app性能有一個(gè)可察覺(jué)的負(fù)面影響岸晦。
方案 #6
每個(gè)controller里都有一個(gè)管理對(duì)象context
如果你有很多工作context或者他們存儲(chǔ)了大量的數(shù)據(jù)欧啤,主context與這些工作context間的數(shù)據(jù)合并就發(fā)生在主線程,這可能導(dǎo)致app不響應(yīng)启上。
另外你辣,主context在持有很多數(shù)據(jù)時(shí)候會(huì)變得很重。隨著時(shí)間繼續(xù)尚揣,對(duì)象會(huì)不停的加入context中站玄。如果你想清除它,你必須在主context中調(diào)用reset方法包券。但是纫谅,如果你有多個(gè)視圖控制器在屏幕上課件,這種情況在iPad上比較常見(jiàn)溅固,你就需要對(duì)這個(gè)reset操作小心謹(jǐn)慎了付秕,以免某個(gè)視圖控制器獲得到已經(jīng)被重置的數(shù)據(jù)。
【圖片】
這個(gè)解決方案就是你簡(jiǎn)單的不合并任何事情到主context中侍郭。你不一定只能有一個(gè)主context询吴!與所有view controller只有一個(gè)主context相反,你可以嘗試這種方法:每個(gè)view controller都有自己的NSManagedObjectContext亮元。方法如下:
1.當(dāng)數(shù)據(jù)從服務(wù)器獲得后猛计,使用工作context來(lái)存儲(chǔ)數(shù)據(jù)。
2.一旦存儲(chǔ)完成爆捞,就觸發(fā)一個(gè)通知奉瘤,比如類似MyAppUsersSavedNotification(假設(shè)你正在存儲(chǔ)user列表數(shù)據(jù)到數(shù)據(jù)庫(kù)中)。
3.顯示user列表的控制器MyAppUsersViewController收到消息并執(zhí)行下述代碼:
[self.managedObjectContext reset];
self.fetchedResultsController = nil;
[self.tableView reloadData];
這樣煮甥,這個(gè)view controller的NSFetchedResultsController將會(huì)被重新創(chuàng)建(很多例子中盗温,假設(shè)你也有懶獲取器),它會(huì)從coredata獲取最新的數(shù)據(jù)苛秕。盡管這個(gè)操作是在主隊(duì)列中進(jìn)行的肌访,但這應(yīng)該是相當(dāng)快速的,只要?jiǎng)e忘記設(shè)置NSFetchRequest上fetchBatchSize艇劫。另外吼驶,也要考慮你真實(shí)需要的數(shù)據(jù),如果你不需要所有字段店煞,那么你應(yīng)該只使用NSFetchRequest中的propertiesToFetch字段來(lái)獲取你需要的字段蟹演。
哪個(gè)更好?
也許你希望找到一種適用所有情況的解決方案顷蟀。不幸的是酒请,通常來(lái)說(shuō),最佳方案取決于你的需求鸣个。對(duì)于簡(jiǎn)單的使用場(chǎng)景來(lái)說(shuō)你使用方案1即可羞反,但是對(duì)于一些更重的使用場(chǎng)景布朦,我們建議你使用RestKit和那些采用了方案4的解決方案。