鍍金池/ 教程/ iOS/ 導入大數(shù)據(jù)集
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應用數(shù)據(jù)腳本化
一個完整的 Core Data 應用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡應用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應式 Android 應用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學習
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學習的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應式視圖
Square Register 中的擴張
DTrace
基礎集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設計的藝術(shù)
導航應用
線程安全類的設計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標簽
同步案例學習
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

導入大數(shù)據(jù)集

往 Core Data 應用中導入大數(shù)據(jù)集是個很常見的問題。鑒于數(shù)據(jù)的特點你可以采用以下幾種方法:

  1. 從 web 服務器上下載數(shù)據(jù) (例如 JSON 數(shù)據(jù)),然后插入到 Core Data 中。
  2. 從 web 服務器上下載預先生成的 Core Data SQLite 數(shù)據(jù)庫文件。
  3. 把一個預先生成好的 Core Data SQLite 數(shù)據(jù)庫文件傳到應用程序包中。

對某些應用場景后兩種選擇作為可行的方案經(jīng)常被忽視了。因此,在本文中我們將進一步的了解他們,并總結(jié)一下如何高效地把web服務上的數(shù)據(jù)導入到一個動態(tài)的應用中。

傳輸預先生成的 SQLite 文件

當用大量數(shù)據(jù)來填充 Core Data 時,通過傳輸或下載預先生成的 SQLite 文件是一個可行的方案,并且比在客戶端創(chuàng)建數(shù)據(jù)更加高效。如果源數(shù)據(jù)庫包含靜態(tài)數(shù)據(jù),并且能夠相對獨立地與潛在的用戶產(chǎn)生的數(shù)據(jù)共存,這就是該技術(shù)的使用場景。

Core Data 框架在 iOS 和 OS X 間是共用的,因此,我們可以創(chuàng)建 OS X 上的命令行工具來產(chǎn)生 SQLite 數(shù)據(jù)庫文件,并且將該文件用在 iOS 應用中。

在我們的例子中 (你可以在 Github 上找到),我們創(chuàng)建了一個命令行工具,它接受兩個柏林城市的數(shù)據(jù)集文件作為輸入,并把它們插入到 Core Data SQLite 數(shù)據(jù)庫中。這個數(shù)據(jù)集包含大約 13,000 逗留記錄及三百萬逗留時間記錄。

對于該技術(shù)最重要的是,命令行工具和客戶端應用使用了相同的數(shù)據(jù)模型。如果數(shù)據(jù)模型隨著時間發(fā)生了改變,當你更新應用并傳輸新的源數(shù)據(jù)時,你要仔細地管理數(shù)據(jù)模型的版本。有一個好的建議就是不要復制.xcdatamodel文件,而是從命令行工具項目中把它鏈接到客戶端應用項目。

另一個有用的步驟是在產(chǎn)生的 SQLite 文件上執(zhí)行 VACUUM 命令。它會減小文件大小,因此根據(jù)你傳輸文件方式的不同,應用程序包的尺寸,或是要下載的數(shù)據(jù)庫的尺寸也會相應減小。

除了這些,對于該過程真的沒有別的方法了;在我們的案例項目中你也看到了,它就是些簡單的標準 Core Data 代碼。既然生成 SQLite 文件不是性能關(guān)鍵的任務,你也沒必要花大力氣去優(yōu)化它的性能。如果你想讓它更快,后面針對高效地導入大數(shù)據(jù)集到動態(tài)應用中所作的總結(jié)規(guī)則同樣適用。

用戶產(chǎn)生的數(shù)據(jù)

我們經(jīng)常會有這樣的場景,希望有一個可用的大的源數(shù)據(jù)集,但是也想能存儲和修改一些用戶產(chǎn)生的數(shù)據(jù)。同樣,有幾種方法來解決這個問題。

首先要考慮的是,用戶產(chǎn)生的數(shù)據(jù)是否真的需要用 Core Data 來存儲。如果我們能把這些數(shù)據(jù)存儲到 plist 文件中,就不要亂動已建好的 Core Data 數(shù)據(jù)庫。

如果我們想用 Core Data 來存儲,另一個需要考慮的問題是,在將來是否需要通過傳輸更新的預先建好的 SQLite 文件來更新源數(shù)據(jù)集。如果這種情況不會發(fā)生,我們可以安全地把用戶生產(chǎn)的數(shù)據(jù)包含到相同的數(shù)據(jù)模型和配置中。然而,如果我們想傳輸一個新源數(shù)據(jù)庫,我們必須要分離源數(shù)據(jù)與用戶產(chǎn)生的數(shù)據(jù)。

這個完全可以通過建立第二個完全獨立的,使用自己的數(shù)據(jù)模型的 Core Data 來實現(xiàn),或者通過在兩個持久性存儲間分發(fā)相同有數(shù)據(jù)模型的數(shù)據(jù)。對此,我們需要在同一個數(shù)據(jù)模型中創(chuàng)建第二個配置,它保存用戶產(chǎn)生的數(shù)據(jù)的實體。當配置 Core Data 棧時,我們將實例化兩個持久化存儲,其中一個包含 URL 和源數(shù)據(jù)庫的配置,另一個包含 URL 和用戶產(chǎn)生數(shù)據(jù)的數(shù)據(jù)庫的配置。

使用兩個獨立的 Core Data 棧是一種更簡單明了的方法。如果這個方法恰好能解決你的問題的話,我們強烈推薦使用它。然而,如果你想在用戶產(chǎn)生的數(shù)據(jù)與源數(shù)據(jù)間建立關(guān)系,Core Data 不能幫你實現(xiàn)。即使你把所有的東西包含在一個擴展到兩個持久化存儲的數(shù)據(jù)模型中,你依然不能像通常那樣在這些實體間定義關(guān)系,但是當獲取某一特定屬性時,你可以用 Core Data 中的 fetched properties 從不同的存儲中自動獲取對象。

應用 Bundle 中的 SQLite 文件

如果我們想往應用程序里傳輸一個預先生成的 SQLite 文件,我們必須檢測出最新更新的應用是否是第一次打開,并把程序外部的數(shù)據(jù)庫文件復制到目標目錄:

NSFileManager* fileManager = [NSFileManager defaultManager];
NSError *error;

if([fileManager fileExistsAtPath:self.storeURL.path]) {
    NSURL *storeDirectory = [self.storeURL URLByDeletingLastPathComponent];
    NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:storeDirectory
                                          includingPropertiesForKeys:nil
                                                             options:0
                                                        errorHandler:NULL];
    NSString *storeName = [self.storeURL.lastPathComponent stringByDeletingPathExtension];
    for (NSURL *url in enumerator) {
        if (![url.lastPathComponent hasPrefix:storeName]) continue;
        [fileManager removeItemAtURL:url error:&error];
    }
    // 處理錯誤
}

NSString* bundleDbPath = [[NSBundle mainBundle] pathForResource:@"seed" ofType:@"sqlite"];
[fileManager copyItemAtPath:bundleDbPath toPath:self.storeURL.path error:&error];

注意我們首先要刪除之前的數(shù)據(jù)庫文件。這不像你想的那樣簡單明了,因為可能會存在不同的附屬文件(如日志或?qū)懬叭罩疚募┡c主要的 .sqlite 文件相關(guān)。因此我們必須遍歷目錄里的每一項,刪除所有的與存儲文件名字匹配不帶擴展名的文件。

然而,我們也需要一個方法確保這件事我們只做了一次。一個很明顯的方法就是從程序中把源數(shù)據(jù)庫刪除。雖然在模擬器上管用,但是因為權(quán)限的問題,在真機上會失敗。有很多方案來解決這個問題,如在 user defaults 中設置一個 key,它包含了最新導入的數(shù)據(jù)的版本信息:

NSString* bundleVersion = [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey];
NSString *seedVersion = [[NSUserDefaults standardUserDefaults] objectForKey@"SeedVersion"];
if (![seedVersion isEqualToString:bundleVersion]) {
    // 復制源數(shù)據(jù)庫
}

// ... 導入成功后
NSDictionary *infoDictionary = [NSBundle mainBundle].infoDictionary;
[[NSUserDefaults standardUserDefaults] setObject:bundleVersion forKey:@"SeedVersion"];

或者舉個例子,我們也可以復制存在的數(shù)據(jù)庫到一個包含源版本的路徑來檢測它是否存在, 從而避免做兩個相同的導入。有很多可行的方法供你選擇,這取決于你的應用場景最重要的是什么。

下載預先生成的 SQLite 文件

如果出于某些原因我們不想把源數(shù)據(jù)庫包放在應用程序中(如,它會導致程序大小超過手機下載的閾值),我們可以從 web 服務器上下載。過程與我們把數(shù)據(jù)庫文件放在設備上是一樣的。但是得保證,服務器提供的數(shù)據(jù)庫版本要與客戶端的數(shù)據(jù)模型兼容,因為不同的應用版本數(shù)據(jù)模型可能會改變。

這不僅僅是通過下載來替換應用程序中的一個文件,這個方案也使得填充更多的數(shù)據(jù)而不導致在客戶端動態(tài)地導入數(shù)據(jù)引發(fā)的性能與電量損耗成為可能。

為了產(chǎn)生馬上可用的 SQLite 文件,我們可以像前面那樣在 (OS X) 服務器上運行類似的命令行導入程序。無可否認地,鑒于數(shù)據(jù)集的大小及要服務的請求數(shù),對每一個請求該操作所需的計算資源可能不允許。一個可行的替代方案是定期地生成SQLite文件,給客戶端發(fā)送這些現(xiàn)成的文件。

為了提供 SQLite 下載的 API,在服務器端及客戶端當然需要額外的邏輯,SQLite 的下載可以為自上次源文件生成后已經(jīng)發(fā)生改變的客戶端提供數(shù)據(jù)。整個過程有點復雜,但是可以讓你更容易的用任意大小的動態(tài)數(shù)據(jù)來填充 Core Data,而且沒有性能問題(除了帶寬限制)。

從 Web 服務導入數(shù)據(jù)

最后,讓我們看看如何從 web 服務器上導入大量的數(shù)據(jù),如 JSON 格式的數(shù)據(jù)。

如果我們要導入有關(guān)系的不同對象類型,我們需要在處理它們間的關(guān)系前先獨立地導入所有的對象。如果我們能在 server 端保證客戶端是以正確的順序收到的對象,我們可以馬上處理它們間的關(guān)系,而且不用為此擔心。但大部分情況這是不可能的。

為在不影響用戶界面響應前提下進行導入操作,我們必須在后臺線程中執(zhí)行導入操作。在第二期中,Chris寫了一篇在后臺使用 Core Data 的簡單方式。如果做的正確,多核設備可以在不影響用戶界面響應的情況下在后臺執(zhí)行導入操作。注意,并發(fā)地使用 Core Data 也有可能在不同的托管對象的上下文間產(chǎn)生沖突。你需要提出一種策略來預防或處理這些情況。

在本文中,理解 Core Data 的并發(fā)工作是很重要的。因為我們已經(jīng)在兩個線程上建立了兩個被管理對象上下文,這并不表示它們兩個會同時去訪問數(shù)據(jù)庫。從托管對象上下文發(fā)出的每個請求會對上下文的對象及 SQLite 文件加上鎖。例如,如果你在主上下文的一個子上下文中觸發(fā)了一個讀請求,為了執(zhí)行這個請求,主上下文,持久化存儲協(xié)調(diào)器,持久化存儲,以及 SQLite 文件都會被加鎖(盡管加在 SQLite 文件上的鎖比其他對象要去除的快)。在此期間,其他在 Core Data 棧上每個對象會被阻塞等著這個請求的完成。

在后臺上下文中大量導入數(shù)據(jù)的例子中,這意味著導入操作的保存請求會不斷地在持久化存儲協(xié)調(diào)器上加鎖。在此期間,像為了更新用戶界面而進行的讀取請求,是不能在主上下文中執(zhí)行的,而必須等待保存請求完成。因為 Core Data 的 API 是同步的,因此主線程會被阻塞,用戶界面的響應會受影響。

如果在你的應用場景中這是個問題,你應該考慮為后臺上下文使用帶有自己的持久化存儲協(xié)調(diào)器的獨立 Core Data 棧。在這種情況下,在后臺上下文與主上下文間唯一共享的資源就是 SQLite 文件,鎖競爭會比之前有所減少。特別地,當 SQLite 文件以 write-ahead loggin 的方式執(zhí)行 (在 iOS7 和 OS X 10.9 是默認的) 時,即使在 SQLite 文件級別,你也會得到真正并發(fā)。多個讀和一個寫可以同時來訪問數(shù)據(jù)庫(看這里 WWDC 2013 session "What's New in Core Data and iCloud" )

最后,在大量導入數(shù)據(jù)時,實時地把修改通知合并到主上下文中一般不會是個好的做法。如果用戶界面對這些變化自動響應的話(通過使用NSFetchResultsController),應用界面會陷入停頓。其實,我們可以在整個導入完成時發(fā)送一個自定義通知,讓用戶界面重新加載數(shù)據(jù)。

如果應用場景是想在導入數(shù)據(jù)期間就實時的更新UI界面,我們可以考慮過濾掉特定實體類型的保存通知,把它們按批聚集起來,或是其他減少界面更新頻率的方式,來確保界面可以響應。然而,在大多數(shù)情況下并不值得這么做,因為對界面的頻繁更新會讓用戶覺得更加迷惑,而非更有幫助。

在通過實際的導入例子講述了設置方法和操作手法后,我們再來看一些讓它盡可能高效的特殊方法。

高效地導入

為了高效導入數(shù)據(jù),我們的第一個建議就是通讀 Apple 關(guān)于這個主題的指導。我們也會強調(diào)該文檔中經(jīng)常容易被忘記的幾個方面。

首先,你要在用于導入的上下文中把 undoManager 置為 nil。盡管這個只適用于 OS X,因為在 iOS 上,上下文默認沒有 undo manager。把 undoManager 屬性置空會帶來重大的性能提升。

其次,訪問具有相互引用關(guān)系的對象會產(chǎn)生引用環(huán)。如果你使用了設計良好的自動釋放池后,還是看到在導入過程中內(nèi)存使用不斷增加,那就應該注意導入部分代碼中的陷阱了。蘋果在這里描述了如何使用refreshObject:mergeChanges:來去掉這些環(huán)。

當你導入可能已經(jīng)在數(shù)據(jù)庫中存在的數(shù)據(jù)時,你需要實現(xiàn)一些查找及創(chuàng)建的算法,以防止產(chǎn)生重復。對每一個對象執(zhí)行讀取請求效率很低,因為每個讀取請求都需要 Core Data 到硬盤上從存儲文件里讀取數(shù)據(jù)。然而,通過按批導入數(shù)據(jù)并使用在上面提到的文檔中 Apple 提供的高效查找創(chuàng)建算法,可以很容易避免這個問題。

當建立新導入的對象間的關(guān)系時,類似的問題也經(jīng)常產(chǎn)生。用一個讀取請求獨立地獲得每一個相關(guān)的對象是非常低效的。有兩種可能的解決方法:一是像按批導入數(shù)據(jù)那樣按批處理它們間的關(guān)系,二是緩存已經(jīng)導入的對象的ID。

按批處理關(guān)系可以使我們大大地減少一次獲取大量相關(guān)對象的讀取請求次數(shù)。不用擔心可能很長的查詢語句,如:

[NSPredicate predicateWithFormat:@"identifier IN %@", identifiersOfRelatedObjects];

處理一個在IN (...)從句中帶有很多標識符的查詢語句,總是比去硬盤上單獨地讀取每個對象更高效。

然而,也有一種可以完全避免讀取請求的方法,(前提是你只需要在剛導入的對象間建立關(guān)系)。如果你緩存導入的所有對象的 IDs (實際上在大多數(shù)情況下數(shù)據(jù)量也不大),之后你可以用 objectWithID: 方法為相關(guān)的對象建立關(guān)系。

// 在一堆對象已經(jīng)被導入并保存之后
for (MyManagedObject *object in importedObjects) {
    objectIDCache[object.identifier] = object.objectID;
}

// ... 之后在解決關(guān)系時
NSManagedObjectID objectID = objectIDCache[object.foreignKey];
MyManagedObject *relatedObject = [context objectWithID:objectId];
object.toOneRelation = relatedObject;

注意,這個例子假設 identifier 屬性在所有的實體類型中是唯一的,否則,我們就得為我們多緩存的不同類型的對象 IDs 創(chuàng)建重復的標識符。

結(jié)論

當你遇到需要導入大量數(shù)據(jù)到 Core Data 中時,在做大量 JSON 數(shù)據(jù)的實時導入前,盡量先不要按常規(guī)來思考。特別是如果你能控制客戶端和服務器端,經(jīng)常會有很多解決該問題的高效方法。但是如果你不得不忍痛做大量后臺導入工作,保證盡可能與主線程一樣獨立高效地進行。

上一篇:KVC 和 KVO下一篇:訪談