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

Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例

幾乎每一個(gè)應(yīng)用開(kāi)發(fā)者都需要經(jīng)歷的就是將從 web service 獲取到的數(shù)據(jù)轉(zhuǎn)變到 Core Data 中。這篇文章闡述了如何去做。我們?cè)谶@里討論的每一個(gè)問(wèn)題在之前的文章中都已經(jīng)描述過(guò)了,并且 Apple 在他們的文檔中也提過(guò)。然而,從頭到尾回顧一遍對(duì)我們來(lái)說(shuō)還是很有益的。

程序所有的代碼都在 GitHub 上。

計(jì)劃

我們將會(huì)建立一個(gè)簡(jiǎn)單、只讀的應(yīng)用程序,用來(lái)顯示 CocoaPods 說(shuō)明的完整列表。這些說(shuō)明都顯示在 table view 中,所有 pod 的說(shuō)明都是以分頁(yè)的形式,從 web service 取得,并以 JSON 對(duì)象返回。

我們這樣來(lái)做

  1. 首先,我們創(chuàng)建一個(gè) PodsWebservice 類(lèi),用來(lái)從 web service 請(qǐng)求所有的說(shuō)明。
  2. 接著,創(chuàng)建一個(gè) Importer 對(duì)象取出說(shuō)明并將他們導(dǎo)入 Core Data。
  3. 最終,我們展示如何讓最重要的工作在后臺(tái)線(xiàn)程中運(yùn)行。

從 Web Service 取得對(duì)象

首先,創(chuàng)建一個(gè)單獨(dú)的類(lèi)從 web service 取得數(shù)據(jù)是很不錯(cuò)的。我們已經(jīng)寫(xiě)了一個(gè)簡(jiǎn)單的 web server 示例,用來(lái)獲取 CocoaPods 說(shuō)明并將它們生成 JSON;請(qǐng)求 /specs 這個(gè) URL 會(huì)返回一個(gè)按字母排序的 pod 說(shuō)明列表。web service 是分頁(yè)的,所以我們需要分開(kāi)請(qǐng)求每一頁(yè)。一個(gè)響應(yīng)的示例如下:

{ 
  "number_of_pages": 559,
  "result": [{
    "authors": { "Ash Furrow": "ash@ashfurrow.com" },
    "homepage": "https://github.com/500px/500px-iOS-api",
    "license": "MIT",
    "name": "500px-iOS-api",
  ...

我們想要?jiǎng)?chuàng)建只有一個(gè) fetchAllPods: 方法的類(lèi),它有一個(gè)回調(diào) block,這將會(huì)被每一個(gè)頁(yè)面調(diào)用。這也可以通過(guò)代理實(shí)現(xiàn);但為什么我們選擇用 block,你可以讀一讀這篇有關(guān)消息傳遞機(jī)制的文章。

@interface PodsWebservice : NSObject
- (void)fetchAllPods:(void (^)(NSArray *pods))callback;
@end

這個(gè)回調(diào)會(huì)被每個(gè)頁(yè)面調(diào)用。實(shí)現(xiàn)這個(gè)方法很簡(jiǎn)單。我們創(chuàng)建一個(gè)幫助方法,fetchAllPods:page:,它會(huì)為一個(gè)頁(yè)面取得所有的 pods,一旦加載完一頁(yè)就讓它再調(diào)用自己。注意一下,為了簡(jiǎn)潔,我們這里不考慮處理錯(cuò)誤,但是你可以在 GitHub 上完整的項(xiàng)目中看到。處理錯(cuò)誤總是很重要的,至少打印出錯(cuò)誤,這樣你可以很快檢查到哪些地方?jīng)]有像預(yù)期一樣工作:

- (void)fetchAllPods:(void (^)(NSArray *pods))callback page:(NSUInteger)page
{
    NSString *urlString = [NSString stringWithFormat:@"http://localhost:4567/specs?page=%d", page];
    NSURL *url = [NSURL URLWithString:urlString];
    [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:
      ^(NSData *data, NSURLResponse *response, NSError *error) {
        id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
        if ([result isKindOfClass:[NSDictionary class]]) {
            NSArray *pods = result[@"result"];
            callback(pods);
            NSNumber* numberOfPages = result[@"number_of_pages"];
            NSUInteger nextPage = page + 1;
            if (nextPage < numberOfPages.unsignedIntegerValue) {
                [self fetchAllPods:callback page:nextPage];
            }
        }
    }] resume];
}

要做的就是這些了。我們解析 JSON,做一些非常粗糙的檢查(驗(yàn)證結(jié)果是一個(gè)字典),然后調(diào)用回調(diào)函數(shù)。

將對(duì)象裝進(jìn) Core Data

現(xiàn)在我們可以將 JSON 裝進(jìn)我們的 Core Data store 中了。為了分清,我們創(chuàng)建一個(gè) Importer 對(duì)象來(lái)調(diào)用 web service,并且創(chuàng)建或者更新對(duì)象。將這些放到一個(gè)單獨(dú)的類(lèi)中很不錯(cuò),因?yàn)檫@樣我們的 web service 和 Core Data 部分完全解耦。如果我們想要給 store 提供一個(gè)不同的 web service 或者在別的某個(gè)地方重用 web service,我們現(xiàn)在并不需要手動(dòng)處理這兩種情況。同時(shí),不要在 view controller 中編寫(xiě)邏輯代碼,以后我們可以在別的 app 中更容易復(fù)用這些組件。

我們的 Importer 有兩個(gè)方法:

@interface Importer : NSObject
- (id)initWithContext:(NSManagedObjectContext *)context 
           webservice:(PodsWebservice *)webservice;
- (void)import;
@end

通過(guò)初始化方法將 context 注入到對(duì)象中是一個(gè)非常強(qiáng)有力的技巧。當(dāng)編寫(xiě)測(cè)試的時(shí)候,我們可以很容易的注入一個(gè)不同的 context。同樣適用于 web service:我們可以很容易的用一個(gè)不同的對(duì)象模擬 web service。

import 方法負(fù)責(zé)處理邏輯。我們調(diào)用 fetchAllPods: 方法,并且對(duì)于每一批 pod 說(shuō)明,我們都會(huì)將它們導(dǎo)入到 context 中。通過(guò)將邏輯代碼包裝到 performBlock:,context 會(huì)確保所有的事情都在正確的線(xiàn)程中執(zhí)行。然后我們迭代這些說(shuō)明,并且會(huì)為每一個(gè)說(shuō)明生成一個(gè)唯一標(biāo)識(shí)符(這些標(biāo)識(shí)符可以是任何獨(dú)一無(wú)二的,只要能確定到唯一一個(gè) model object,正如在 Drew 的文章中解釋那樣。然后我們?cè)囍业?model object,如果不存在則創(chuàng)建一個(gè)。loadFromDictionary: 方法需要一個(gè) JSON 字典,并根據(jù)字典中的值更新 model object:

- (void)import
{
    [self.webservice fetchAllPods:^(NSArray *pods)
    {
        [self.context performBlock:^
        {
            for(NSDictionary *podSpec in pods) {
                NSString *identifier = [podSpec[@"name"] stringByAppendingString:podSpec[@"version"]];
                Pod *pod = [Pod findOrCreatePodWithIdentifier:identifier inContext:self.context];
                [pod loadFromDictionary:podSpec];
            }
        }];
    }];
}

上面的代碼中有很多地方要注意。首先,查找或創(chuàng)建方法的效率是非常低下的。在生產(chǎn)環(huán)境的代碼中,你需要批量處理 pods 并且同時(shí)找到他們,正如在《導(dǎo)入大數(shù)據(jù)集》中「高效地導(dǎo)入數(shù)據(jù)」這一節(jié)中所解釋的那樣。

第二,我們直接在 Pod 類(lèi)(managed object 的子類(lèi))中創(chuàng)建 loadFromDictionary:。這意味著我們的 model object 知道 web service。在真實(shí)的代碼中,我們很有可能將這些放到一個(gè)類(lèi)別中,這樣這兩個(gè)很完美的分開(kāi)了。對(duì)于這個(gè)示例,這無(wú)關(guān)要緊。

創(chuàng)建一個(gè)獨(dú)立的后臺(tái)堆棧

在寫(xiě)上面的代碼時(shí),我們會(huì)先在在主 managed object context 中擁有一切需要的數(shù)據(jù)。我們的應(yīng)用在 table view 控制器中使用一個(gè) fetched results controller 來(lái)顯示所有的 pods。當(dāng) managed object context 中的數(shù)據(jù)改變時(shí),fetched results controller 自動(dòng)更新 data model。然而,在主 managed object context 中處理導(dǎo)入數(shù)據(jù)并不是最優(yōu)的。主線(xiàn)程可能被堵塞,UI 可能沒(méi)有反應(yīng)。大多數(shù)時(shí)候,在主線(xiàn)程中處理的工作應(yīng)該是最小限度的,并且造成的延遲應(yīng)當(dāng)難以察覺(jué)。如果你的情況正是這樣,那非常好。然而,如果我們想要做些額外的努力,我們可以在后臺(tái)線(xiàn)程中處理導(dǎo)入操作。

Apple 在 WWDC 會(huì)議以及官方的《Core Data 編程指南》文檔的「Concurrency with Core Data」 一節(jié)中,對(duì)于并發(fā)的 Core Data,推薦給開(kāi)發(fā)者兩種選擇。這兩種都需要獨(dú)立的 managed object contexts,它們要么共享同樣的 persistent store coordinator,要么不共享。在處理很多改變時(shí),擁有獨(dú)立的 persistent store coordinators 提供更出色的性能,因?yàn)閮H需要的鎖只是在 sqlite 級(jí)別。擁有共享的 persistent store coordinator 也就意味著擁有共享緩存,當(dāng)你沒(méi)有做出很多改變時(shí),這會(huì)很快。所以,根據(jù)你的情況而定,你需要衡量哪種方案更好,然后選擇是否需要一個(gè)共享的 persistent store coordinator。當(dāng)主 context 是只讀的情況下,根本不需要鎖,因?yàn)?iOS 7 中的 sqlite 有寫(xiě)前記錄功能并且支持多重讀取和單一寫(xiě)入。然而,對(duì)于我們的示范目的,我們會(huì)使用完全獨(dú)立堆棧的處理方式。我們使用下面的代碼設(shè)置一個(gè) managed object context:

- (NSManagedObjectContext *)setupManagedObjectContextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType
{
    NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:concurrencyType];
    managedObjectContext.persistentStoreCoordinator =
            [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
    NSError* error;
    [managedObjectContext.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType 
                                                                  configuration:nil 
                                                                            URL:self.storeURL 
                                                                        options:nil 
                                                                          error:&error];
    if (error) {
        NSLog(@"error: %@", error.localizedDescription);
    }
    return managedObjectContext;
}

然后我們調(diào)用這個(gè)方法兩次,一次是為主 managed object context,一次是為后臺(tái) managed object context:

self.managedObjectContext = [self setupManagedObjectContextWithConcurrencyType:NSMainQueueConcurrencyType];
self.backgroundManagedObjectContext = [self setupManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType];

注意傳遞的參數(shù) NSPrivateQueueConcurrencyType 告訴 Core Data 創(chuàng)建一個(gè)獨(dú)立隊(duì)列,這將確保后臺(tái) managed object context 的運(yùn)行發(fā)生在一個(gè)獨(dú)立的線(xiàn)程中。

現(xiàn)在就剩一步了:每當(dāng)后臺(tái) context 保存后,我們需要更新主線(xiàn)程。我們?cè)?a rel="nofollow" >之前第 2 期的這篇文章中描述了如何操作。我們注冊(cè)一下,當(dāng) context 保存時(shí)得到一個(gè)通知,如果是后臺(tái) context,調(diào)用 mergeChangesFromContextDidSaveNotification: 方法。這就是我們要做的所有事情:

[[NSNotificationCenter defaultCenter]
        addObserverForName:NSManagedObjectContextDidSaveNotification
                    object:nil
                     queue:nil
                usingBlock:^(NSNotification* note) {
    NSManagedObjectContext *moc = self.managedObjectContext;
    if (note.object != moc) {
        [moc performBlock:^(){
            [moc mergeChangesFromContextDidSaveNotification:note];
        }];
    }
 }];

這兒還有一個(gè)小忠告:mergeChangesFromContextDidSaveNotification: 是在 performBlock:中發(fā)生的。在我們這個(gè)情況下,moc 是主 managed object context,因此,這將會(huì)阻塞主線(xiàn)程。

注意你的 UI(即使是只讀的)必須有能力處理對(duì)象的改變,或者事件的刪除。Brent Simmons 最近寫(xiě)了兩篇文章,分別是 《Why Use a Custom Notification for Note Deletion》《Deleting Objects in Core Data》。這些文章解釋說(shuō)明了如何面對(duì)這些情況,如果你在你的 UI 中顯示一個(gè)對(duì)象,這個(gè)對(duì)象有可能會(huì)發(fā)生改變或者被刪除。

實(shí)現(xiàn)從 UI 進(jìn)行的寫(xiě)入

你可能覺(jué)得上面講的看起來(lái)非常簡(jiǎn)單,這是因?yàn)閮H有的寫(xiě)操作是在后臺(tái)線(xiàn)程進(jìn)行的。在我們當(dāng)前的應(yīng)用中,我們沒(méi)有處理其他方面的合并;并沒(méi)有來(lái)自主 managed object context 中的改變。為了增加這個(gè),你可以采用不少策略。Drew 的這篇文章很好的闡述了相關(guān)的方法。

根據(jù)你的需求,一個(gè)非常簡(jiǎn)單的模式或許是這樣:不管用戶(hù)何時(shí)改變 UI 中的某些東西,你并不改變 managed object context。相反,你去調(diào)用 web service。如果成功了,你可以從 web service 中得到改變,然后更新你的后臺(tái) context。這些改變隨后回被傳送到主 context。這樣做有兩個(gè)弊端:用戶(hù)可能需要一段時(shí)間才能看到 UI 的改變,并且如果用戶(hù)未聯(lián)網(wǎng),他將不能改變?nèi)魏螙|西。在 Florian 的文章中,描述了我們?nèi)绾问褂貌煌呗宰寫(xiě)?yīng)用在離線(xiàn)時(shí)也能工作。

如果你正在處理合并,你也需要定義一個(gè)合并原則。這又是根據(jù)特定使用情況而定的。如果合并失敗了你可能需要拋出一個(gè)錯(cuò)誤,或者總是給某一個(gè) managed object context 優(yōu)先權(quán)。NSMergePolicy 類(lèi)描述出了可能的選擇。

結(jié)論

我們已經(jīng)看到如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的只讀應(yīng)用,這個(gè)應(yīng)用能將從 web service 取得的大量數(shù)據(jù)導(dǎo)入到 Core Data。通過(guò)使用后臺(tái) managed object context,我們已經(jīng)建立了一個(gè)不會(huì)阻塞 UI(除非正在處理合并)的 Core Data 程序。