CoreData底層架構實現 概述

相關擴展原文 :http://objccn.io/issue-4-2/   http://objccn.io/issue-4-3/


Core Data 可能是 OS X 和 iOS 裏面最容易被誤解的框架之一,爲了幫助大家理解,我們將快速的研究 Core Data,讓大家對它有一個初步的瞭解,對於想要正確使用 Core Data 的同學來說,理解它的概念是非常必要的。幾乎所有對 Core Data 感到失望的原因都是因爲對它工作機制的錯誤理解。讓我們開始吧:

Core Data 是什麼?

大概八年前,2005年的四月份,Apple 發佈了 OS X 10.4,正是在這個版本中 Core Data 框架發佈了。那個時候 YouTube 也剛發佈。

Core Data 是一個模型層的技術。Core Data 幫助你建立代表程序狀態的模型層。Core Data 也是一種持久化技術,它能將模型對象的狀態持久化到磁盤,但它最重要的特點是:Core Data 不僅是一個加載、保存數據的框架,它還能和內存中的數據很好的共事。

如果你之前曾經接觸過 Object-relational maping (O/RM):Core Data不是一個 O/RM,但它比 O/RM 能做的更多。如果你之前曾經接觸過 SQL wrappers:Core Data 不是一個 SQL wrapper。它默認使用 SQL,但是,它是一種更高級的抽象概念。如果你需要的是一個 O/RM 或者 SQL wrapper,那麼 Core Data 並不適合你。

對象圖管理(object graph management)是 Core Data 最強大的功能之一。爲了更好利用 Core Data,這是你需要理解的一塊內容。

還有一點要注意:Core Data 是完全獨立於任何 UI 層級的框架。它是作爲模型層框架被設計出來的。在 OS X 中,甚至在一些後臺駐留程序中,Core Data 都起着非常重要的意義。

堆棧

Core Data 有相當多可用的組件。這是一個非常靈活的技術。在大多數的使用情況下,設置都相當簡單。

當所有的組件都捆綁到一起的時候,我們把它稱作 Core Data 堆棧,這個堆棧有兩個主要部分。一部分是關於對象圖管理,這正是你需要很好掌握的那一部分,並且知道怎麼使用。第二部分是關於持久化,比如,保存你模型對象的狀態,然後再恢復模型對象的狀態。

在兩個部分之間,即堆棧中間,是持久化存儲協調器(persistent store coordinator),也被稱爲中間審查者。它將對象圖管理部分和持久化部分捆綁在一起,當它們兩者中的任何一部分需要和另一部分交流時,這便需要持久化存儲協調器來調節了。

對象圖管理是你程序模型層的邏輯存在的地方。模型層的對象存在於一個 context 內。在大多數的設置中,存在一個 context ,並且所有的對象存在於那個 context 中。Core Data 支持多個 contexts,不過對於更高級的使用情況才用。注意每個 context 和其他 context 都是完全獨立的,一會兒我們將會談到。需要記住的是,對象和它們的 context 是相關聯的。每個被管理的對象都知道自己屬於哪個 context,並且每個 context 都知道自己管理着哪些對象。

堆棧的另一部分就是持久了,即 Core Data 從文件系統中讀或寫數據。每個持久化存儲協調器(persistent store coordinator)都有一個屬於自己的持久化存儲(persistent store),並且這個 store 在文件系統中與 SQLite 數據庫交互。爲了支持更高級的設置,Core Data 可以將多個 stores 附屬於同一個持久化存儲協調器,並且除了存儲 SQL 格式外,還有很多存儲類型可供選擇。

最常見的解決方案如下圖所示:

組件如何一起工作

讓我們快速的看一個例子,看看組件是如何協同工作的。在我們的文章《一個完成的 Core Data 應用》中,正好有一個實體,即一種對象:我們有一個 Item 實體對應一個 title。每一個 item 可以擁有子 items,因此,我們有一個父子關係

這是我們的數據模型。正如我們在《數據模型和模型對象》一文中提到的一樣,在 Core Data 中有一特別的對象——實體。在這種情況下,我們只有一個實體:Item 實體。同樣的,我們有一個 NSManagedObject 的子類,叫做 Item。這個 Item 實體映射到Item 類上。在數據模型的文章中會詳細的談到這個。

我們的程序僅有一個 Item。這並沒有什麼奇妙的地方。它是一個我們用來顯示底層 item 等級的 item。它是一個我們永遠不會爲其設置父類的 Item。

當程序運行時,我們像上面圖片描繪的一樣設置我們的堆棧,一個存儲,一個 managed object context,以及一個持久化存儲協調器來將它們關聯起來。

在第一次運行時,我們並沒有任何 items。我們需要做的第一件事就是創建 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];
}

現在,我們可以像這樣插入我們的根對象了:

Item *rootItem = [Item insertNewObjectInManagedObjectContext:managedObjectContext];

現在,在我們的 managed object context 中有一個唯一的 item。Context 知道這是一個新插入進來需要被管理的對象,並且被管理的對象 rootItem 知道這個 Context(它有一個 -managedObjectContext 方法)。

保存改變

雖然我們已經談到這了,可是我們還是沒有接觸到持久化存儲協調器或持久化存儲。新的模型對象—rootItem,僅僅在內存中。如果我們想要保存模型對象的狀態(在這種情況下只是一個對象),我們需要保存 context:

NSError *error = nil;
if (! [managedObjectContext save:&error]) {
    // 啊,哦. 有錯誤發生了 :(
}

這個時候,很多事情將要發生。首先是 managed object context 計算出改變的內容。這是 context 的職責,追蹤出任何你在 context 管理對象中做出的改變。在我們的例子中,我們到現在做出的唯一改變就是插入一個對象,即我們的 rootItem

Managed object context 將這些改變傳給持久化存儲協調器,讓它將這些改變傳給 store。持久化存儲協調器會協調 store(在我們的例子中,store 是一個 SQL 數據庫)來將我們插入的對象寫入到磁盤上的 SQL 數據庫。NSPersistentStore 類管理着和 SQLite 的實際交互,並且產生需要被執行的 SQL 代碼。持久化存儲協調器的角色就是簡化調整 store 和 context 之間的交互過程。在我們的例子中,這個角色相當簡單,但是,複雜的設置可以有多個 stores 和多個 contexts。

更新關係

Core Data 的優勢在於管理關係。讓我們着眼於簡單的情況:增加我們第二個 item,並且使它成爲 rootItem 的子 item:

Item *item = [Item insertNewObjectInManagedObjectContext:managedObjectContext];
item.parent = rootItem;
item.title = @"foo";

好了。同樣的,這些改變僅僅存在於 managed object context 中。一旦我們保存了 context,managed object context 將會通知持久化存儲協調器,像增加第一個對象一樣增加新創建的對象到數據庫文件中。但這也將會更新第二個 item 與第一個 item 之間的關係。記住 Item 實體是如何有一個父子關係的。它們之間有相反的關係。因爲我們設置第一個 item 爲第二個 item 的父親(parent)時,第二個 item 將會變成第一個 item 的兒子(child)。Managed object context 追蹤這些關係,持久化存儲協調器和 store 保存這些關係到磁盤。

獲取對象

我們已經使用我們的程序一會兒了,並且已經爲 rootItem 增加了一些子 items,甚至增加子 items 到子 items。然而,我們再次啓動我們的程序。Core Data 已經將這些 items 之間的關係保存到了數據庫文件。對象圖是持久化的。我們現在需要取出 item,所以我們可以顯示底層 items 的列表。有兩種方法可以達到這個效果。我們先看簡單點的方法。

當 rootItem 對象創建並保存之後我們可以向它請求它的 NSManagedObjectID。這是一個不透明的對象,可以唯一代表 rootItem。我們可以保存這個對象到 NSUSerDefaults,像這樣:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setURL:rootItem.objectID.URIRepresentation forKey:@"rootItem"];

現在,當程序重新運行時,我們可以像這樣返回得到這個對象:

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 要求持久化存儲協調器從數據庫取得指定的對象。根對象現在被恢復到 context 中。然而,其他所有的 items 仍然不在內存中。

rootItem 有一個子關係叫做 children。但現在那兒還沒有什麼。我們想要顯示 rootItem 的子 item,因此我們需要調用:

NSOrderedSet *children = rootItem.children;

現在發生的是,context 標註這個 rootItem 的子 item 爲所謂的故障。Core Data 已經標註這個關係爲仍需要被解決。既然我們已經在這個時候訪問了它,context 將會自動配合持久化存儲協調器來將這些子 items 載入到 context 中。

這聽起來可能非常不重要,但是在這個時候真正發生了很多事情。如果任何子對象偶然發生在內存中,Core Data 保證會複用那些對象。這是Core Data 獨一無二的功能。在 context 內,從不會存在第二個相同的單一對象來代表一個給定的 item。

其次,持久化存儲協調器有它自己內部對象值的緩存。如果 context 需要一個指定的對象(比如一個子 item),並且持久化存儲協調器在緩存中已經有需要的值,那麼,對象(即這個 item)可以不通過 store 而被直接加到 context。這很重要,因爲訪問 store 就意味着執行 SQL 代碼,這比使用內存中存在的值要慢很多。

隨着我們遍歷 item 的子 item,以及子 item 的子 item,我們慢慢地把整個對象圖引用到了 managed object context。而這些對象都在內存中之後,操作對象以及傳遞關係就會變得非常快,因爲我們只是在 managed object context 裏操作。我們跟本不需要訪問持久化存儲協調器。在我們的 Item 對象上訪問 titleparent 和 children 是非常快而且高效的。

由於它會影響性能,所以瞭解數據在這些情況下怎麼取出來是非常重要的。在我們特定的情況下,由於我們並沒接觸到太多的數據,所以這並不算什麼,但是一旦你需要處理的數據量較大,你將需要了解在背後發生了什麼。

當你遍歷一個關係時(比如在我們例子中的 parent 或 children 關係)下面三種情況將有一種會發生:(1)對象已經在 context 中,這種操作基本上是沒有任何代價的。(2)對象不在 context 中,但是因爲你最近從 store 中取出過對象,所以持久化存儲協調器緩存了對象的值。這個操作還算廉價(但是,一些操作會被鎖住)。操作耗費最昂貴的情況是(3),當 context 和持久化存儲協調器都是第一次訪問這個對象,這種情況必須通過 store 從 SQLite 數據庫取回。最後一種情況比(1)和(2)需要付出更多代價。

如果你知道你必須從 store 取回對象(比如你已經知道沒有這些對象),當你限制一次取回多少個對象時,將會產生很大的不同。在我們的例子中,我們希望一次性取出所有子 items,而不是一個接一個。我們可以通過一個特別的技巧 NSFetchRequest。但是我們要注意,當我們需要做這個操作時,我們只需要執行一次取出請求,因爲一次取出請求將會造成(3)發生;這將總是獨佔 SQLite 數據庫的訪問。因此,當需要顯著提升性能時,檢查對象是否已經存在將變得非常有意義。你可以使用-[NSManagedObjectContext objectRegisteredForID:]來檢測一個對象是否已經存在。

改變對象的值

現在,我們可以說,我們已經改變我們一個 Item 對象的 title

item.title = @"New title";

當我們這樣做時,item 的 title 改變了。此外,managed object context 會標註這個對象(item)已經被改變,這樣當我們在 context 中調用 -save: 時,這個對象將會通過持久化存儲協調器和附屬的 store 保存起來。context最關鍵的職責之一就是跟蹤改變

從最後一次保存開始,context 知道哪些對象被插入,改變以及刪除。你可以通過 -insertedObjects-updatedObjects, 以及 –deletedObjects 方法來達到這樣的效果。同樣的,你可以通過 -changedValues 方法來詢問一個被管理的對象哪些值被改變了。這個方法正是 Core Data 能夠將你做出的改變推入到數據庫的原因。

當我們插入一個新的 Item 對象時,Core Data 知道需要將這些改變存入 store。那麼,將你改變對象的 title 時,也會發生同樣的事情。

保存 values 需要協調持久化存儲協調器和持久化 store 依次訪問 SQLite 數據庫。和在內存中操作對象比起來,取出對象和值,訪問 store 和數據庫是非常耗費資源的。不管你保存了多少更改,一次保存的代價是固定的。並且每個變化都有成本。這是 SQLite 的工作方式。當你做很多更改的時候,需要將更改打包,並批量更改。如果你保存每一次更改,將要付出很高的代價,因爲你需要經常做保存操作。如果你很少做保存,那麼你將會有一大批更改交給 SQLite 處理。

同樣需要注意的是保存操作是原子性的,要麼所有的更改會被提交給 store/SQLite 數據庫,要麼任何更改都不被保存。當實現自定義 NSIncrementalStore 基類時,這一點一定要牢記在心。要麼確保保存永遠不會失敗(比如說不會發生衝突),要麼當保存失敗時,你 store 的基類需要恢復所有的改變。否則,在內存中的對象圖最終和保存在 store 中的對象不一致。

如果你使用一個簡單的設置,保存操作通常不會失敗。但是 Core Data 允許每個持久化存儲協調器有多個 context,所以你可能陷入持久化存儲協調器層級的衝突之中。改變是對於每個 context 的,另一個 context 的更改可能導致衝突。Core Data 甚至允許完全不同的堆棧訪問磁盤上相同的 SQLite 數據庫。這明顯也會導致衝突(比如,一個 context 想要更新一個對象的值,而另一個 context 想要刪除這個對象)。另一個導致保存失敗的原因可能是驗證。Core Data 支持複雜的對象驗證策略。這是一個高級話題。一個簡單的驗證規則可能是: Item 的 title 不能超過300個字符。但是 Core Data 也支持通過屬性進行復雜的驗證策略。

結束語

如果 Core Data 看起來讓人害怕,這最有可能是因爲它的靈活性允許你可以通過非常複雜的方法使用它。始終記住:儘可能保持簡單。它會讓開發變得更容易,並且把你和你的用戶從麻煩中拯救出來。除非你確信它會帶來幫助,纔去使用更復雜的東西,比如說是 background contexts。

當你開始使用一個簡單的 Core Data 堆棧,並且使用我們在這篇文章中講到的知識吧,你將很快會真正體會到 Core Data 能爲你做什麼,並且學到它是怎麼縮短你開發週期的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章