Core Data 可能是 OS X 和 iOS 里面最容易被誤解的框架之一,為了幫助大家理解,我們將快速的研究 Core Data,讓大家對它有一個初步的了解,對于想要正確使用 Core Data 的同學來說,理解它的概念是非常必要的。幾乎所有對 Core Data 感到失望的原因都是因為對它工作機制的錯誤理解。讓我們開始吧:
大概八年前,2005年的四月份,Apple 發(fā)布了 OS X 10.4,正是在這個版本中 Core Data 框架發(fā)布了。那個時候 YouTube 也剛發(fā)布。
Core Data 是一個模型層的技術(shù)。Core Data 幫助你建立代表程序狀態(tài)的模型層。Core Data 也是一種持久化技術(shù),它能將模型對象的狀態(tài)持久化到磁盤,但它最重要的特點是:Core Data 不僅是一個加載、保存數(shù)據(jù)的框架,它還能和內(nèi)存中的數(shù)據(jù)很好的共事。
如果你之前曾經(jīng)接觸過 Object-relational maping (O/RM):Core Data不是一個 O/RM,但它比 O/RM 能做的更多。如果你之前曾經(jīng)接觸過 SQL wrappers:Core Data 不是一個 SQL wrapper。它默認使用 SQL,但是,它是一種更高級的抽象概念。如果你需要的是一個 O/RM 或者 SQL wrapper,那么 Core Data 并不適合你。
對象圖管理(object graph management)是 Core Data 最強大的功能之一。為了更好利用 Core Data,這是你需要理解的一塊內(nèi)容。
還有一點要注意:Core Data 是完全獨立于任何 UI 層級的框架。它是作為模型層框架被設(shè)計出來的。在 OS X 中,甚至在一些后臺駐留程序中,Core Data 都起著非常重要的意義。
Core Data 有相當多可用的組件。這是一個非常靈活的技術(shù)。在大多數(shù)的使用情況下,設(shè)置都相當簡單。
當所有的組件都捆綁到一起的時候,我們把它稱作 Core Data 堆棧,這個堆棧有兩個主要部分。一部分是關(guān)于對象圖管理,這正是你需要很好掌握的那一部分,并且知道怎么使用。第二部分是關(guān)于持久化,比如,保存你模型對象的狀態(tài),然后再恢復模型對象的狀態(tài)。
在兩個部分之間,即堆棧中間,是持久化存儲協(xié)調(diào)器(persistent store coordinator),也被稱為中間審查者。它將對象圖管理部分和持久化部分捆綁在一起,當它們兩者中的任何一部分需要和另一部分交流時,這便需要持久化存儲協(xié)調(diào)器來調(diào)節(jié)了。
http://wiki.jikexueyuan.com/project/objc/images/4-1.png" alt="" />
對象圖管理是你程序模型層的邏輯存在的地方。模型層的對象存在于一個 context 內(nèi)。在大多數(shù)的設(shè)置中,存在一個 context ,并且所有的對象存在于那個 context 中。Core Data 支持多個 contexts,不過對于更高級的使用情況才用。注意每個 context 和其他 context 都是完全獨立的,一會兒我們將會談到。需要記住的是,對象和它們的 context 是相關(guān)聯(lián)的。每個被管理的對象都知道自己屬于哪個 context,并且每個 context 都知道自己管理著哪些對象。
堆棧的另一部分就是持久了,即 Core Data 從文件系統(tǒng)中讀或?qū)憯?shù)據(jù)。每個持久化存儲協(xié)調(diào)器(persistent store coordinator)都有一個屬于自己的持久化存儲(persistent store),并且這個 store 在文件系統(tǒng)中與 SQLite 數(shù)據(jù)庫交互。為了支持更高級的設(shè)置,Core Data 可以將多個 stores 附屬于同一個持久化存儲協(xié)調(diào)器,并且除了存儲 SQL 格式外,還有很多存儲類型可供選擇。
最常見的解決方案如下圖所示:
http://wiki.jikexueyuan.com/project/objc/images/4-2.png" alt="" />
讓我們快速的看一個例子,看看組件是如何協(xié)同工作的。在我們的文章《一個完成的 Core Data 應(yīng)用》中,正好有一個實體,即一種對象:我們有一個 Item 實體對應(yīng)一個 title。每一個 item 可以擁有子 items,因此,我們有一個父子關(guān)系。
這是我們的數(shù)據(jù)模型。正如我們在《數(shù)據(jù)模型和模型對象》一文中提到的一樣,在 Core Data 中有一種特別的對象——實體。在這種情況下,我們只有一個實體:Item 實體。同樣的,我們有一個 NSManagedObject
的子類,叫做 Item
。這個 Item 實體映射到 Item
類上。在數(shù)據(jù)模型的文章中會詳細的談到這個。
我們的程序僅有一個根 Item。這并沒有什么奇妙的地方。它是一個我們用來顯示底層 item 等級的 item。它是一個我們永遠不會為其設(shè)置父類的 Item。
當程序運行時,我們像上面圖片描繪的一樣設(shè)置我們的堆棧,一個存儲,一個 managed object context,以及一個持久化存儲協(xié)調(diào)器來將它們關(guān)聯(lián)起來。
在第一次運行時,我們并沒有任何 items。我們需要做的第一件事就是創(chuàng)建根 item。你通過將它們插入 context 來增加管理對象。
插入對象的方法似乎很笨重,我們通過 NSEntityDescription
的方法來插入:
+ (id)insertNewObjectForEntityForName:(NSString *)entityName
inManagedObjectContext:(NSManagedObjectContext *)context
我們建議你增加兩個方便的方法到你的模型類中:
+ (NSString *)entityName
{
return @“Item”;
}
+ (instancetype)insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)moc;
{
return [NSEntityDescription insertNewObjectForEntityForName:[self entityName]
inManagedObjectContext:moc];
}
現(xiàn)在,我們可以像這樣插入我們的根對象了:
Item *rootItem = [Item insertNewObjectInManagedObjectContext:managedObjectContext];
現(xiàn)在,在我們的 managed object context 中有一個唯一的 item。Context 知道這是一個新插入進來需要被管理的對象,并且被管理的對象 rootItem
知道這個 Context(它有一個 -managedObjectContext
方法)。
雖然我們已經(jīng)談到這了,可是我們還是沒有接觸到持久化存儲協(xié)調(diào)器或持久化存儲。新的模型對象—rootItem
,僅僅在內(nèi)存中。如果我們想要保存模型對象的狀態(tài)(在這種情況下只是一個對象),我們需要保存 context:
NSError *error = nil;
if (! [managedObjectContext save:&error]) {
// 啊,哦. 有錯誤發(fā)生了 :(
}
這個時候,很多事情將要發(fā)生。首先是 managed object context 計算出改變的內(nèi)容。這是 context 的職責,追蹤出任何你在 context 管理對象中做出的改變。在我們的例子中,我們到現(xiàn)在做出的唯一改變就是插入一個對象,即我們的 rootItem
。
Managed object context 將這些改變傳給持久化存儲協(xié)調(diào)器,讓它將這些改變傳給 store。持久化存儲協(xié)調(diào)器會協(xié)調(diào) store(在我們的例子中,store 是一個 SQL 數(shù)據(jù)庫)來將我們插入的對象寫入到磁盤上的 SQL 數(shù)據(jù)庫。NSPersistentStore
類管理著和 SQLite 的實際交互,并且產(chǎn)生需要被執(zhí)行的 SQL 代碼。持久化存儲協(xié)調(diào)器的角色就是簡化調(diào)整 store 和 context 之間的交互過程。在我們的例子中,這個角色相當簡單,但是,復雜的設(shè)置可以有多個 stores 和多個 contexts。
Core Data 的優(yōu)勢在于管理關(guān)系。讓我們著眼于簡單的情況:增加我們第二個 item,并且使它成為 rootItem
的子 item:
Item *item = [Item insertNewObjectInManagedObjectContext:managedObjectContext];
item.parent = rootItem;
item.title = @"foo";
好了。同樣的,這些改變僅僅存在于 managed object context 中。一旦我們保存了 context,managed object context 將會通知持久化存儲協(xié)調(diào)器,像增加第一個對象一樣增加新創(chuàng)建的對象到數(shù)據(jù)庫文件中。但這也將會更新第二個 item 與第一個 item 之間的關(guān)系。記住 Item 實體是如何有一個父子關(guān)系的。它們之間有相反的關(guān)系。因為我們設(shè)置第一個 item 為第二個 item 的父親(parent)時,第二個 item 將會變成第一個 item 的兒子(child)。Managed object context 追蹤這些關(guān)系,持久化存儲協(xié)調(diào)器和 store 保存這些關(guān)系到磁盤。
我們已經(jīng)使用我們的程序一會兒了,并且已經(jīng)為 rootItem 增加了一些子 items,甚至增加子 items 到子 items。然而,我們再次啟動我們的程序。Core Data 已經(jīng)將這些 items 之間的關(guān)系保存到了數(shù)據(jù)庫文件。對象圖是持久化的。我們現(xiàn)在需要取出根 item,所以我們可以顯示底層 items 的列表。有兩種方法可以達到這個效果。我們先看簡單點的方法。
當 rootItem
對象創(chuàng)建并保存之后我們可以向它請求它的 NSManagedObjectID
。這是一個不透明的對象,可以唯一代表 rootItem
。我們可以保存這個對象到 NSUSerDefaults
,像這樣:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setURL:rootItem.objectID.URIRepresentation forKey:@"rootItem"];
現(xiàn)在,當程序重新運行時,我們可以像這樣返回得到這個對象:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSURL *uri = [defaults URLForKey:@"rootItem"];
NSManagedObjectID *moid = [managedObjectContext.persistentStoreCoordinator managedObjectIDForURIRepresentation:uri];
NSError *error = nil;
Item *rootItem = (id) [managedObjectContext existingObjectWithID:moid error:&error];
很明顯,在一個真正的程序中,我們需要檢查 NSUserDefaults
是否真正返回一個有效值。
剛才的操作是 managed object context 要求持久化存儲協(xié)調(diào)器從數(shù)據(jù)庫取得指定的對象。根對象現(xiàn)在被恢復到 context 中。然而,其他所有的 items 仍然不在內(nèi)存中。
rootItem
有一個子關(guān)系叫做 children
。但現(xiàn)在那兒還沒有什么。我們想要顯示 rootItem 的子 item,因此我們需要調(diào)用:
NSOrderedSet *children = rootItem.children;
現(xiàn)在發(fā)生的是,context 標注這個 rootItem 的子 item 為所謂的故障。Core Data 已經(jīng)標注這個關(guān)系為仍需要被解決。既然我們已經(jīng)在這個時候訪問了它,context 將會自動配合持久化存儲協(xié)調(diào)器來將這些子 items 載入到 context 中。
這聽起來可能非常不重要,但是在這個時候真正發(fā)生了很多事情。如果任何子對象偶然發(fā)生在內(nèi)存中,Core Data 保證會復用那些對象。這是Core Data 獨一無二的功能。在 context 內(nèi),從不會存在第二個相同的單一對象來代表一個給定的 item。
其次,持久化存儲協(xié)調(diào)器有它自己內(nèi)部對象值的緩存。如果 context 需要一個指定的對象(比如一個子 item),并且持久化存儲協(xié)調(diào)器在緩存中已經(jīng)有需要的值,那么,對象(即這個 item)可以不通過 store 而被直接加到 context。這很重要,因為訪問 store 就意味著執(zhí)行 SQL 代碼,這比使用內(nèi)存中存在的值要慢很多。
隨著我們遍歷 item 的子 item,以及子 item 的子 item,我們慢慢地把整個對象圖引用到了 managed object context。而這些對象都在內(nèi)存中之后,操作對象以及傳遞關(guān)系就會變得非???,因為我們只是在 managed object context 里操作。我們跟本不需要訪問持久化存儲協(xié)調(diào)器。在我們的 Item 對象上訪問 title
,parent
和 children
是非常快而且高效的。
由于它會影響性能,所以了解數(shù)據(jù)在這些情況下怎么取出來是非常重要的。在我們特定的情況下,由于我們并沒接觸到太多的數(shù)據(jù),所以這并不算什么,但是一旦你需要處理的數(shù)據(jù)量較大,你將需要了解在背后發(fā)生了什么。
當你遍歷一個關(guān)系時(比如在我們例子中的 parent
或 children
關(guān)系)下面三種情況將有一種會發(fā)生:(1)對象已經(jīng)在 context 中,這種操作基本上是沒有任何代價的。(2)對象不在 context 中,但是因為你最近從 store 中取出過對象,所以持久化存儲協(xié)調(diào)器緩存了對象的值。這個操作還算廉價(但是,一些操作會被鎖?。?。操作耗費最昂貴的情況是(3),當 context 和持久化存儲協(xié)調(diào)器都是第一次訪問這個對象,這種情況必須通過 store 從 SQLite 數(shù)據(jù)庫取回。最后一種情況比(1)和(2)需要付出更多代價。
如果你知道你必須從 store 取回對象(比如你已經(jīng)知道沒有這些對象),當你限制一次取回多少個對象時,將會產(chǎn)生很大的不同。在我們的例子中,我們希望一次性取出所有子 items,而不是一個接一個。我們可以通過一個特別的技巧 NSFetchRequest
。但是我們要注意,當我們需要做這個操作時,我們只需要執(zhí)行一次取出請求,因為一次取出請求將會造成(3)發(fā)生;這將總是獨占 SQLite 數(shù)據(jù)庫的訪問。因此,當需要顯著提升性能時,檢查對象是否已經(jīng)存在將變得非常有意義。你可以使用-[NSManagedObjectContext objectRegisteredForID:]
來檢測一個對象是否已經(jīng)存在。
現(xiàn)在,我們可以說,我們已經(jīng)改變我們一個 Item
對象的 title
:
item.title = @"New title";
當我們這樣做時,item 的 title 改變了。此外,managed object context 會標注這個對象(item
)已經(jīng)被改變,這樣當我們在 context 中調(diào)用 -save:
時,這個對象將會通過持久化存儲協(xié)調(diào)器和附屬的 store 保存起來。context最關(guān)鍵的職責之一就是跟蹤改變。
從最后一次保存開始,context 知道哪些對象被插入,改變以及刪除。你可以通過 -insertedObjects
, -updatedObjects
, 以及 –deletedObjects
方法來達到這樣的效果。同樣的,你可以通過 -changedValues
方法來詢問一個被管理的對象哪些值被改變了。這個方法正是 Core Data 能夠?qū)⒛阕龀龅母淖兺迫氲綌?shù)據(jù)庫的原因。
當我們插入一個新的 Item
對象時,Core Data 知道需要將這些改變存入 store。那么,將你改變對象的 title
時,也會發(fā)生同樣的事情。
保存 values 需要協(xié)調(diào)持久化存儲協(xié)調(diào)器和持久化 store 依次訪問 SQLite 數(shù)據(jù)庫。和在內(nèi)存中操作對象比起來,取出對象和值,訪問 store 和數(shù)據(jù)庫是非常耗費資源的。不管你保存了多少更改,一次保存的代價是固定的。并且每個變化都有成本。這是 SQLite 的工作方式。當你做很多更改的時候,需要將更改打包,并批量更改。如果你保存每一次更改,將要付出很高的代價,因為你需要經(jīng)常做保存操作。如果你很少做保存,那么你將會有一大批更改交給 SQLite 處理。
同樣需要注意的是保存操作是原子性的,要么所有的更改會被提交給 store/SQLite 數(shù)據(jù)庫,要么任何更改都不被保存。當實現(xiàn)自定義 NSIncrementalStore
基類時,這一點一定要牢記在心。要么確保保存永遠不會失?。ū热缯f不會發(fā)生沖突),要么當保存失敗時,你 store 的基類需要恢復所有的改變。否則,在內(nèi)存中的對象圖最終和保存在 store 中的對象不一致。
如果你使用一個簡單的設(shè)置,保存操作通常不會失敗。但是 Core Data 允許每個持久化存儲協(xié)調(diào)器有多個 context,所以你可能陷入持久化存儲協(xié)調(diào)器層級的沖突之中。改變是對于每個 context 的,另一個 context 的更改可能導致沖突。Core Data 甚至允許完全不同的堆棧訪問磁盤上相同的 SQLite 數(shù)據(jù)庫。這明顯也會導致沖突(比如,一個 context 想要更新一個對象的值,而另一個 context 想要刪除這個對象)。另一個導致保存失敗的原因可能是驗證。Core Data 支持復雜的對象驗證策略。這是一個高級話題。一個簡單的驗證規(guī)則可能是: Item
的 title
不能超過300個字符。但是 Core Data 也支持通過屬性進行復雜的驗證策略。
如果 Core Data 看起來讓人害怕,這最有可能是因為它的靈活性允許你可以通過非常復雜的方法使用它。始終記?。罕M可能保持簡單。它會讓開發(fā)變得更容易,并且把你和你的用戶從麻煩中拯救出來。除非你確信它會帶來幫助,才去使用更復雜的東西,比如說是 background contexts。
當你開始使用一個簡單的 Core Data 堆棧,并且使用我們在這篇文章中講到的知識吧,你將很快會真正體會到 Core Data 能為你做什么,并且學到它是怎么縮短你開發(fā)周期的。