Core Data可能是OS X和iOS中最容易被誤解的框架之一了。為了幫助大家理解,我們將快速研究Core Data,來看一下它是關于什么的。為了正確使用Core Data, 有必要理解其概念。幾乎所有Core Data引起的挫敗,都是因為不理解它能做什么和它是怎么工作的。讓我們開始吧。
Core Data是什么?
大概8年前,在2005年4月,Apple發布了OS X 10.4版本,第一次引入了Core Data框架。那時YouTube也剛發布。
Core Data是模型層的技術。Core Data幫助你構建代表程序狀態的模型層。Core Data也是一種持久化技術,它可以將模型的狀態持久化到磁盤。但它更重要的特點是:Core Data不只是一個加載和保存數據的框架,它也能處理內存中的數據。
如果你曾接觸過Object-relational mapping(O/RM),Core Data不僅是一種O/RM。如果你曾接觸過SQL wrappers, Core Data也不是一種SQL wrapper。它確實默認使用SQL,但是,它是一種更高層次的抽象概念。如果你需要一個O/RM或者SQL wrapper,那么Core Data并不適合你。
>Core Data提供的最強大的功能之一是它的對象圖形管理。為了更有效的使用Core Data, 你需要理解這一部分內容。
還有一點需要注意:Core Data完全獨立于任何UI層的框架。從設計的角度來說,它是完全的模型層的框架。在OS X中,甚至在一些后臺駐留程序中,Core Data也起著重要的意義。
堆棧The Stack
Core Data中有不少組件,它是一種非常靈活的技術。在大多數使用情況里,設置相對來說比較簡單。
當所有組件綁定在一起,我們把它們稱為Core Data Stack. 這種堆棧有兩個主要部分。一部分是關于對象圖管理,這是你需要掌握好的部分,也應該知道怎么使用。第二部分是關于持久化的,比如保存模型對象的狀態和再次恢復對象的狀態。
在這兩部分的中間,即堆棧中間,是持久化存儲協調器(Persistent Store Coordinator, PSC),也被朋友們戲稱做中心監視局。通過它將對象圖管理部分和持久化部分綁在一起。當這兩部分中的一部分需要和另一部分交互,將通過PSC來調節。
對象圖管理是你的應用中模型層邏輯存在的地方。模型層對象存在于一個context里。在大多數設置中,只有一個context,所有的對象都放在這個context中。Core Data支持多個context,但是是針對更高級的使用情況。需要注意的是,每個context和其他context區分都很清楚,我們將要來看一點這部分內容。有個重要的事需要記住,對象和他們的context綁定在一起。每一個被管理的對象都知道它屬于哪個context,每一個context也知道它管理著哪些對象。
堆棧的另一部分是持久化發生的地方,比如Core Data從文件系統讀或寫。在所有情況下,持久化存儲協調器(PSC)有一個屬于自己的的持久化存儲器(persistent store),這個store在文件系統和SQLite數據庫交互。為了支持更高級的設置,Core Data支持使用多個存儲器附屬于同一個持久化存儲協調器,并且除了SQL,還有一些別的存儲類型可以選擇。
一個常見的解決方案,看起來是這個樣子的:
組件如何一起工作
我們來快速看一個例子,來說明這些組件是如何協同工作的。在我們a full application using Core Data的文章里,我們正好有一個實體enity,即一種對象: 我們有一個Item實體對應一個title。每一個item可以有子items,因此我們有一個父子關系。
這是我們的數據模型。像我們在Data Models and Model Objects文章里提到的,在Core Data中有一個特別類型的對象叫做Entity。在這種情況下,我們只有一個entity:一個Item entity. 同樣的,我們有一個NSManagedObject子類叫Item。這個Item entity映射到Item類。在data models的文章里會詳細的談到這個。
我們的應用僅有一個根item。這里面沒有什么奇妙的地方。這只是個簡單的我們用在底層的item。這是一個我們永遠不會為其設置父類的item.
當app啟動,我們沒有任何item。我們需要做的第一件事是創建一個根item。你通過插入對象到context里來添加可管理的對象。
創建對象
可能看起來有點笨重。我們通過NSEntityDescription的方法來插入:
1 2 | + (id)insertNewObjectForEntityForName:(NSString *)entityName inManagedObjectContext:(NSManagedObjectContext *)context |
我們建議你添加兩個方便的方法到模型類中:
1 2 3 4 5 6 7 8 9 10 | + (NSString *)entityName { return @“Item”; } + (instancetype)insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)moc; { return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:moc]; } |
現在,我們可以插入我們的根對象:
1 | Item *rootItem = [Item insertNewObjectInManagedObjectContext:managedObjectContext]; |
現在在我們的managed object context里有了一個唯一的item. context知道這個新插入的被管理對象,這個被管理對象rootItem也知道這個context(它有 -managedObjectContext方法)。
保存改變
到目前為止,雖然我們還沒有碰到持久化存儲協調器或者持久化存儲器。這個新的模型對象,rootItem,只是在內存中。如果我們想保存我們模型對象的狀態(我們的情況里就是rootItem),我們需要這樣保存context:
1 2 3 4 | NSError *error = nil; if (! [managedObjectContext save:&error]) { // Uh, oh. An error happened. :( } |
現在,有很多事將要發生。首先,managed object context算出改變的內容。Context有責任去記錄你對context里任何被管理的對象做出的改變。在我們的例子里,我們迄今為止唯一的改變是往里插入了一個對象,我們的rootItem.
這個managed object context把這些變化傳遞給持久化存儲協調器,讓它把改變傳遞給store。持久化存儲協調器協調store(在我們的例子里,是一個SQL存儲器)把我們新插入的對象寫入磁盤中的SQL數據庫里。NSPersistentStore類管理著和SQLite的真正交互,并且生成需要被執行的SQL代碼。持久化存儲協調器的角色只是簡單的協調store和context之間的交互。在我們的例子里,這個角色相對簡單,但是更復雜的應用里可以有多個store和多個context.
更新關系
Core Data的重要能力是它可以管理關系。我們看一個簡單的例子,加第二個item,把它設為rootItem的子item。
1 2 3 | Item *item = [Item insertNewObjectInManagedObjectContext:managedObjectContext]; item.parent = rootItem; item.title = @ "foo" ; |
好了。再次注意,這些改變只是在managed object context里面。一旦我們保存了context,managed object context就會告訴持久化存儲協調器去把那個新建的對象添加到數據庫文件中,像我們的第一個對象一樣。但是它也同樣會更新從我們第二個item到第一個的關系,或從第一個對象到第二個的關系。記住一個Item實體是如何有了父子關系。同時他們也有相反的關系。因為我們把第一個item設為第二個的父類,第二個就會是第一個的子類。Managed object context記錄了這些關系,持久化存儲協調器和store持久化(比如保存)這些關系到磁盤。
弄清對象
假設我們已經使用了我們的app一段時間,并且已經添加了一些子items到根item,甚至一些子items到子items。我們再次啟動app,Core Data已經在數據庫文件中存儲了這些item之間的關系,對象圖已經存在了。現在我們需要取出我們的根item, 這樣我們可以顯示底層items列表。我們有兩種辦法來實現這個,我們先來看一個簡單的。
當根Item對象創建并保存后,我們可以獲取它的NSManagedObjectID。這是一個不透明的對象,只代表根Item對象。我們可以把它保存到NSUserDefaults, 像這樣:
1 2 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; |
現在,當我們的app運行中,我們可以像這樣取回這個對象:
1 2 3 4 5 | 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]; |
當然,在一個真正的app中,我們需要檢查NSUserDefaults是否返回了一個有效的值。
剛才發生的事情是,managed object context讓持久化存儲協調器去從數據庫里獲取指定的對象。那個root對象現在就被恢復到了context中。但是所有其他的item還不在內存中。
rootItem有一個關系叫子關系,但是那兒還什么也沒有。我們想顯示rootItem的所有子item,所以我們調用:
1 | NSOrderedSet *children = rootItem.children; |
現在發生的是,context注意到要獲取rootItem有關聯的children,會得到一個所謂的故障。Core Data已經標記了這個關系作為一件需要被解決的事。既然我們已經在這個時候訪問了它,context現在會自動和持久化存儲協調器去協調,來載入這些子item到context里。
這個聽起來可能是不重要的,但是事實上這個地方有很多事情發生。如果子對象中碰巧有一些已經在內存中了,Core Data需要保證它會重用這些對象。這就叫做唯一性。在context中,從來不會有多于一個對象對應于一個給定的item.
其次,持久化存儲協調器有它自己內部對象值的緩存。如果context需要一個指定的對象(比如一個子item),并且持久化存儲協調器已經在內存中有它需要的值了,這個對象可以被直接加到context,不需要通過store。這個很重要,因為使用store意味著執行SQL語句,這要比直接用內存中的值慢很多。
我們繼續從一個item到子item再到子item,我們慢慢的把整個對象圖加載到managed object context。但是一旦它們都在內存中,操作對象,或者獲取它們的關系都是很快的,因為我們只是在managed object context中工作。我們完全不需要和持久化存儲協調器打交道。這時候獲取我們的Item對象的title, parent, children屬性都很快也很方便。
理解在這些情況中數據是怎么獲取的很重要,因為它影響性能。在我們的這個例子里,它不太重要,因為我們沒有使用很多數據。但是一旦你開始使用,你就需要理解背后發生了什么。
當你獲取一種關系(比如我們的例子是父子關系),下面三種情況中的一個會發生: (1) 這個對象已經在context中,獲取基本上沒有開銷。(2)這個對象不在context中,但是因為你最近從store中獲取過這個對象,持久化存儲協調器緩存了它的值。這種情況適當便宜一些(雖然一些操作會被鎖?。i_銷最大的情況是:(3)當這個對象被context和持久化存儲協調器都第一次訪問,這樣它需要被store從SQLite數據庫中取出來。這種情況要比1和2開銷大很多。
如果你知道你需要從store中獲取對象(因為你還沒有它們),當你限制一次取回多少個對象時,將會產生很大不同。在我們的例子里,我們可能需要一次獲取所有的子item,而不是一個接一個。這可以通過一個特殊的NSFetchRequest來實現。但是我們一定要小心只是在我們需要的時候才執行一次取出請求。因為一個取出請求將會引起(3)發生,它總是需要通過SQLite數據庫來獲取。因此,如果性能很重要,檢查對象是否已經存在就很必要。你可以使用 -[NSManagedObjectContext objectRegisteredForID:]
來檢測一個對象是否已經存在。
改變對象的值
現在,比如我們要改變一個item對象的title:
1 | item.title = @ "New title" ; |
當我們這么做的時候,這個item的title就改變了。但是同時,managed object context會把這個item標記為已經改變,這樣當我們調用context的-save:,它將會通過持久化存儲協調器和相應的store保存起來。context的一個重要職責就是標記改變。
context知道從上次保存后,哪些對象已經被插入,改變和刪除。你可以通過-insertedObjects, -updateObjects, -deletedObject這些方法來獲取。同樣的,你也可以通過-changedValues方法問一個被管理的對象,它的哪些值變了。你可能從來都不需要這么做。但是這是Core Data可以把改變保存到支持的數據庫中的方式。
當我們插入一個新的Item對象,Core Data知道需要把這些改變存入store?,F在,當我們改變title,也會發生同樣的事情。
保存值需要和持久化存儲協調器還有持久化store依次訪問SQLite數據庫。當恢復對象和值時,使用store和數據庫比直接在內存中操作數據相對耗費資源。保存有一個固定的開銷,不管你要保存的變化有多少。并且每次變化都有成本,這只是SQLite的工作。當你改變很多值時,需要將變更打包,并批量更改。如果你每次改變都保存,需要付出昂貴的代價,因為你需要經常保存。如果你保存次數少一些,你將會有有一大批更改交由SQLite來處理。
需要注意的是保存是原子性的。它們都是事務。或者所有的改變都被提交到store/SQLite數據庫,或者任何改變都不會被保存。當你實現自定義的NSIncrementalStore子類的時候,這點很重要應該要記住。你可以保證保存永遠不會失?。ū热缫驗闆_突),或者你的store子類需要在保存失敗時恢復所有改變。否則,內存中的對象圖會和store中的不一致。
如果你只是使用簡單的設置,保存通常不會失敗。但是Core Data允許多個context對應一個持久化存儲協調器,所以你可能會在持久化存儲協調器中遇到沖突。改變是每一個context的,另一個context可能會引入有沖突的改變。Core Data甚至允許完全不同的堆棧都訪問同一個在磁盤中的SQLite數據庫文件。這顯然也可能引發沖突(比如,一個context想更新一個object的一個值,但是這個object已經被另一個context刪除了)。另一個導致保存失敗的原因可能是校驗。Core Data支持對對象的復雜校驗規則。這是個高級的話題。一個簡單的校驗可以是,一個Item的title長度一定不能超過300字符。但是Core Data也支持對屬性的復雜的校驗規則。
結束語
如果Core Data看起來讓人畏縮,可能主要因為它可以讓你通過復雜的方式來靈活使用。始終記?。罕M可能讓事情保持簡單。這會讓開發更容易,讓你和你的用戶免于麻煩。只在你確定會有幫助的情況下,才使用像background contexts這些更復雜的東西。
當你使用一個簡單的Core Data堆棧,并且你用我們在本文中提到的方法來使用managed objects,你會很快學會欣賞Core Data可以為你做的事情,和它怎么幫你縮短開發周期。
新聞熱點
疑難解答