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

同步案例學(xué)習(xí)

不久之前,我和 Chris 一起為一個(gè)大型青年運(yùn)動(dòng)組織開發(fā)企業(yè) iPad 應(yīng)用。我們選擇 Core Data 作為數(shù)據(jù)持久化工具,并根據(jù)需求定制了數(shù)據(jù)同步的解決方案。根據(jù) Drew 文章中提到的同步方式分類表格,我們使用的是異步客戶端-服務(wù)器(client-server)方式。?

本文將對(duì)我們決策和實(shí)現(xiàn)的過程進(jìn)行案例分析,以供大家學(xué)習(xí)如何定制自己的同步方案。我們的最終方案并不是完美或者普遍適用的,但是現(xiàn)階段它能夠滿足我們的需求。

在我們深入研究之前,如果你對(duì)數(shù)據(jù)同步方案感興趣(既然你在讀這篇文章,我覺得這應(yīng)該是肯定的),我強(qiáng)烈建議你去 Brent 的博客閱讀一下 Verper 應(yīng)用同步方案的系列文章。跟隨 Brent 的思路來分析 Vesper 的同步方案實(shí)現(xiàn)將會(huì)是一次絕妙的閱讀體驗(yàn)。

應(yīng)用場(chǎng)景

「iCloud 和 Core Data」或者 Dropbox 數(shù)據(jù)存儲(chǔ) API 中所示,現(xiàn)在大部分的同步方案都面向同一用戶多個(gè)設(shè)備之間數(shù)據(jù)同步的問題。不過我們面臨的需求略有不同,我們的應(yīng)用將會(huì)被部署在組織里的大約 50 個(gè)設(shè)備中,每個(gè)設(shè)備屬于不同的組織成員,大家都對(duì)同一個(gè)數(shù)據(jù)集進(jìn)行操作,我們需要在這些設(shè)備間進(jìn)行數(shù)據(jù)同步。

數(shù)據(jù)本身結(jié)構(gòu)復(fù)雜,包含大約一打?qū)嶓w和其間的各種關(guān)系。我們需要處理的數(shù)據(jù)量很大,真實(shí)情況下數(shù)據(jù)記錄的量將迅速達(dá)到十萬量級(jí)。

雖然大部分情況下組織成員都能夠連上 Wi-Fi,網(wǎng)絡(luò)連接的質(zhì)量其實(shí)是相當(dāng)差的。保證大部分情況下組織成員能夠使用應(yīng)用并訪問數(shù)據(jù)集是非常重要的,所以我們需要實(shí)現(xiàn)離線情況下的各種數(shù)據(jù)操作。

要求

將上一小節(jié)描述的應(yīng)用場(chǎng)景搞清楚之后,我們同步方案的需求其實(shí)已經(jīng)相當(dāng)清楚了:

  1. 無論有沒有網(wǎng)絡(luò)連接,每一臺(tái)設(shè)備都能夠訪問完整的數(shù)據(jù)集。
  2. 因?yàn)榫W(wǎng)絡(luò)連接不穩(wěn)定,數(shù)據(jù)同步時(shí)發(fā)起的請(qǐng)求數(shù)量要盡可能少。
  3. 數(shù)據(jù)更改必須基于最新的數(shù)據(jù),因?yàn)槿魏稳硕疾粦?yīng)該在不知曉其他人修改的情況下覆蓋那些改動(dòng)。

設(shè)計(jì)

API

由于數(shù)據(jù)模型的嵌套結(jié)構(gòu)和可能出現(xiàn)的高網(wǎng)絡(luò)延遲,傳統(tǒng)的 REST 風(fēng)格 API 并不是一個(gè)好選擇。例如為了在應(yīng)用中顯示一個(gè)儀表盤(dashboard)視圖,必須遍歷好幾個(gè)數(shù)據(jù)層級(jí)來獲取所需的所有數(shù)據(jù):隊(duì)伍、隊(duì)員聯(lián)盟、隊(duì)員、單屏顯示(screen)和單屏顯示元素(screen item)。如果我們分別獲取這幾類數(shù)據(jù),在數(shù)據(jù)更新完畢之前我們需要發(fā)起很多次請(qǐng)求。

實(shí)際中我們采用了更原子化的操作,在同步數(shù)據(jù)量不變的情況下發(fā)起請(qǐng)求數(shù)更少??蛻舳伺c服務(wù)器僅使用一個(gè) API 接口進(jìn)行交互:/sync。

為了實(shí)現(xiàn)這個(gè)方案,我們需要自定義客戶端與服務(wù)器交互的數(shù)據(jù)格式,從而在一次數(shù)據(jù)同步同求中包含所有需要的數(shù)據(jù)。

數(shù)據(jù)格式

客戶端與服務(wù)器之間通過自定義的 JSON 格式數(shù)據(jù)進(jìn)行交互。無論請(qǐng)求由誰發(fā)起,都采用同樣的格式,如下是一個(gè)簡(jiǎn)單的示例:

{
    "maxRevision": 17382,
    "changeSets: [
        ...
    ]
}

上例中的 JSON 數(shù)據(jù)頂層含有 maxRevisionchangeSets 兩個(gè) key。maxRevision 用來唯一標(biāo)識(shí)客戶端當(dāng)前數(shù)據(jù)版本的版本號(hào),changeSets 則是一個(gè)以數(shù)據(jù)修改集(change set)為元素的列表,如下所示:

{
    "types": [ "users" ],
    "users": {
        "create": [],
        "update": [ 1013 ],
        "delete": [],
        "attributes": {
            "1013": {
                "first_name": "Florian",
                "last_name": "Kugler",
                "date_of_birth": "1979-09-12 00:00:00.000+00"
                "revision": 355
            }
        }
    }
}

頂層的 types 對(duì)應(yīng)這個(gè)修改集中涉及的所有數(shù)據(jù)實(shí)體類型。每個(gè)類型又會(huì)對(duì)應(yīng)一個(gè)針對(duì)它自己的修改集合,包含創(chuàng)建(create)、更新(update)和刪除(delete)操作,每個(gè)操作對(duì)應(yīng)指定的記錄 ID。這些 ID 最后會(huì)對(duì)應(yīng)到針對(duì)這條記錄的哪些屬性(attributes)進(jìn)行了新建或更新操作。

這套數(shù)據(jù)結(jié)構(gòu)參考了之前 Web 端使用的數(shù)據(jù)結(jié)構(gòu),當(dāng)時(shí)采用的結(jié)構(gòu)有利于原有客戶端的數(shù)據(jù)處理,也同樣滿足現(xiàn)在的需求。

接下來我們看一個(gè)復(fù)雜一點(diǎn)的例子。假設(shè)我們?yōu)橐慌_(tái)設(shè)備上其中一名隊(duì)員添加了新的單屏顯示數(shù)據(jù),當(dāng)需要同步到服務(wù)器上時(shí),如下是請(qǐng)求中包含的數(shù)據(jù)結(jié)構(gòu):

{
    "maxRevision": 1000,
    "changeSets": [
        {
            "types": [ "screen_instances", "screen_instance_items" ],
            "screen_instances": {
                "create": [ -10 ],
                "update": [],
                "delete": [],
                "attributes": {
                    "-10": {
                        "screen_id": 749,
                        "date": "2014-02-01 13:15:23.487+01",
                        "comment": ""
                    }
                }
            },
            "screen_instance_items: {
                "create": [ -11, -12 ],
                "update": [],
                "delete": [],
                "attributes": {
                    "-11": {
                        "screen_instance_id": -10,
                        "numeric_value": 2
                    },
                    "-12": {
                        ...
                    }
                }
            }
        }
    ]
}

注意其中涉及的記錄 ID 是負(fù)數(shù),這是因?yàn)樗鼈兪切陆ǖ臈l目。新建的 screen_instance 條目 ID 是 -10,在后面的 screen_instance_items 條目中引用到了這個(gè) ID 作為外鍵。

當(dāng)服務(wù)器處理完這個(gè)請(qǐng)求后(假設(shè)沒有沖突或者權(quán)限問題),發(fā)回給客戶端的響應(yīng)請(qǐng)求中將包含如下 JSON 數(shù)據(jù):

{
    "maxRevision": 1001,
    "changeSets": [
        {
            "conflict": false,
            "types": [ "screen_instances", "screen_instance_items" ],
            "screen_instances": {
                "create": [ 321 ],
                "update": [],
                "delete": [],
                "attributes": {
                    "321": {
                        "__oldId__": -10
                        "revision": 1001
                        "screen_id": 749,
                        "date": "2014-02-01 13:15:23.487+01",
                        "comment": "",
                    }
                }
            },
            "screen_instance_items: {
                "create": [ 412, 413 ],
                "update": [],
                "delete": [],
                "attributes": {
                    "412": {
                        "__oldId__": -11,
                        "revision": 1001,
                        "screen_instance_id": 321,
                        "numeric_value": 2
                    },
                    "413": {
                        "__oldId__": -12,
                        "revision": 1001,
                        ...
                    }
                }
            }
        }
    ]
}

客戶端在請(qǐng)求中包含版本號(hào) 1000,而服務(wù)器返回版本號(hào) 1001,同時(shí)將 1001 這個(gè)版本賦予所有這次新建成功的記錄。(從服務(wù)器返回的版本號(hào)只增加了 1 我們可以知道客戶端的修改是基于最新數(shù)據(jù)進(jìn)行的。)

原來的負(fù)數(shù) ID 現(xiàn)在已經(jīng)被服務(wù)器用真實(shí)的 ID 替換。為了保留記錄間的聯(lián)系,負(fù)數(shù)外鍵也被相應(yīng)更新成了實(shí)際的 ID。但是客戶端仍然可以獲得臨時(shí)負(fù)數(shù) ID 與服務(wù)器返回的永久性正數(shù) ID 之間的關(guān)聯(lián)關(guān)系,因?yàn)榉?wù)器將臨時(shí)負(fù)數(shù) ID 作為記錄屬性的一部分返回了。

如果客戶端的修改不是基于最新的數(shù)據(jù)(假如客戶端的版本號(hào)是 995),服務(wù)器會(huì)返回多個(gè)修改集以將客戶端數(shù)據(jù)更新到最新版本。具體來說,服務(wù)器會(huì)將 995 版本到 1000 版本的更新操作與客戶端發(fā)送的 1001 版本一起返回。

解決沖突

如前所述,在這個(gè)所有人操作同一套數(shù)據(jù)的應(yīng)用場(chǎng)景下,任何人都不應(yīng)該在不知曉其他人修改的情況下覆蓋那些改動(dòng)。我們采取的方案就是只要你沒有看到其他人對(duì)數(shù)據(jù)的最新修改,你就不能直接對(duì)這些修改進(jìn)行覆蓋。

有了版本號(hào)的幫助,這個(gè)方案變得很容易實(shí)現(xiàn)??蛻舳税l(fā)送給服務(wù)器的任何修改,都包含有對(duì)應(yīng)數(shù)據(jù)條目的版本號(hào)。因?yàn)榭蛻舳藦膩聿恍薷囊延邪姹咎?hào),所以版本號(hào)反映了客戶端上一次與服務(wù)器交互的數(shù)據(jù)情況。服務(wù)器就可以根據(jù)版本號(hào)搜索對(duì)應(yīng)的條目,并屏蔽基于非最新數(shù)據(jù)的修改。

這個(gè)設(shè)計(jì)的優(yōu)雅之處在于采用的數(shù)據(jù)交互格式允許事務(wù)性修改。JSON 數(shù)據(jù)中的一個(gè)修改集可以包含對(duì)不同數(shù)據(jù)實(shí)體的多個(gè)修改。在服務(wù)器端,修改集中所有修改以事務(wù)方式進(jìn)行,如果其中任何操作導(dǎo)致沖突,則之前的操作全部回滾,然后服務(wù)器為該修改集加上沖突(conflict)標(biāo)識(shí)返回給客戶端。

問題在于客戶端在沖突發(fā)生后如何將數(shù)據(jù)恢復(fù)到正常狀態(tài)。因?yàn)樾薷目赡茉诳蛻舳颂幱陔x線狀態(tài)下進(jìn)行,一天之后才會(huì)上傳到服務(wù)器,所以必須保存一份精確的修改日志并存儲(chǔ)起來。這樣我們就可以在沖突發(fā)生時(shí)撤銷任何修改。

我們最終采用了一種不同的方案:因?yàn)橹挥蟹?wù)器能夠確定數(shù)據(jù)的正確狀態(tài),所以只需要在沖突發(fā)生時(shí)將正確的數(shù)據(jù)返回即可。服務(wù)器端針對(duì)此方案的實(shí)現(xiàn)非常簡(jiǎn)單,而客戶端則需要做很多工作來保證這一方案的正確。

例如客戶端刪除了一條不允許刪除的記錄,服務(wù)器就會(huì)返回一個(gè)有沖突(conflict)標(biāo)識(shí)的修改集,其中包含被錯(cuò)誤刪除的記錄,以及與該記錄相關(guān)聯(lián)的其他記錄。這樣客戶端就很容易恢復(fù)刪除的記錄,而不用在本地記錄每一次操作。

實(shí)現(xiàn)

之前我們已經(jīng)討論了數(shù)據(jù)同步的基本概念,現(xiàn)在來看一下具體實(shí)現(xiàn)的細(xì)節(jié)。

后端

后端是使用 node.js 寫的輕量級(jí)應(yīng)用,使用 PostgreSQL 存儲(chǔ)結(jié)構(gòu)化數(shù)據(jù),同時(shí)使用 Redis 緩存所有數(shù)據(jù)庫修改事務(wù)的修改集(換言之,每個(gè)修改集對(duì)應(yīng)從 x 版本到 x+1 版本的全部修改)。在客戶端發(fā)起同步請(qǐng)求時(shí),服務(wù)器可以從緩存的修改集中迅速找到客戶端缺失的最新修改并返回,而不用臨時(shí)去查詢數(shù)據(jù)庫。

后端實(shí)現(xiàn)的具體細(xì)節(jié)超出了本文的范圍,但是老實(shí)說這些細(xì)節(jié)中并沒有什么激動(dòng)人心的地方。服務(wù)器只是簡(jiǎn)單地為每一個(gè)接收到的修改集發(fā)起一個(gè)事務(wù),然后嘗試將這些操作寫入數(shù)據(jù)庫。如果發(fā)生沖突,事務(wù)就進(jìn)行回滾,然后構(gòu)造一個(gè)正確狀態(tài)的修改集。如果沒有錯(cuò)誤發(fā)生,服務(wù)器將以一個(gè)包含最新版本號(hào)的修改集確認(rèn)這次修改。

處理完客戶端發(fā)送過來的修改后,服務(wù)器會(huì)檢查客戶端的最新版本號(hào)是否落后于自己的,如果是的話就將上面構(gòu)造的正確修改集返回給客戶端以同步到最新狀態(tài)。

Core Data

在客戶端使用了 Core Data,所以我們需要在后臺(tái)記錄用戶的每一次修改并提交到服務(wù)器。同時(shí)我們也需要處理服務(wù)器發(fā)送過來的數(shù)據(jù)并與本地?cái)?shù)據(jù)進(jìn)行合并。

為了達(dá)到以上目的,我們使用了一個(gè)主隊(duì)列管理用戶界面相關(guān)的對(duì)象上下文(object context)(包括用戶輸入的所有數(shù)據(jù)),另一個(gè)獨(dú)立的私有隊(duì)列則用于管理服務(wù)器發(fā)送過來的數(shù)據(jù)。

當(dāng)用戶修改數(shù)據(jù)的時(shí)候,主上下文(main context)會(huì)被保存,而我們監(jiān)聽了保存事件的通知。從通知中我們可以獲取用戶操作中插入、更新和刪除的數(shù)據(jù)對(duì)象,從而構(gòu)造一個(gè)修改集加入到隊(duì)列中等候最終發(fā)送給服務(wù)器。這個(gè)隊(duì)列是持久化的(隊(duì)列本身和其中的對(duì)象使用 NSCoding 協(xié)議),所以即使應(yīng)用在與服務(wù)器同步前退出了我們也不會(huì)丟失任何修改。

當(dāng)客戶端與服務(wù)器建立好連接之后,就從隊(duì)列中拿出所有的修改集并轉(zhuǎn)換為上文提及的 JSON 格式,帶上當(dāng)前最新的版本號(hào)發(fā)送給服務(wù)器。

當(dāng)服務(wù)器的響應(yīng)返回時(shí),客戶端查看收到的所有修改集,然后更新私有隊(duì)列中相應(yīng)的本地?cái)?shù)據(jù)。只有當(dāng)這次更新成功完成時(shí),客戶端才會(huì)將服務(wù)器發(fā)送過來的最新版本號(hào)存儲(chǔ)到 Core Data 中指定的數(shù)據(jù)實(shí)體中。

最后很重要的一點(diǎn)是私有隊(duì)列中的修改會(huì)合并到主上下文中,所以用戶界面會(huì)相應(yīng)更新。

當(dāng)以上所有操作完成后,我們就可以接著處理隊(duì)列中新出現(xiàn)的修改以發(fā)起下一次同步請(qǐng)求。

合并策略

我們必須妥善處理用于存儲(chǔ)服務(wù)器返回?cái)?shù)據(jù)的私有隊(duì)列與主上下文之間的沖突。用戶再修改主上下文時(shí)很有可能后臺(tái)正在接收服務(wù)器的數(shù)據(jù)。

因?yàn)榉?wù)器返回的數(shù)據(jù)才是絕對(duì)正確的,在將私有隊(duì)列中的數(shù)據(jù)合并到主上下文的時(shí)候,我們的策略就是持久化存儲(chǔ)的數(shù)據(jù)相比內(nèi)存中的數(shù)據(jù)更優(yōu)先采用。

當(dāng)用戶修改一個(gè)服務(wù)器已經(jīng)更新(比如已經(jīng)刪除)的對(duì)象時(shí),這種合并策略會(huì)遇到一些問題。當(dāng)這種修改在私有隊(duì)列中保存但還未合并到主上下文時(shí)可以發(fā)送一個(gè)自定義的通知,這樣用戶界面可以針對(duì)此作出反應(yīng)。

初始數(shù)據(jù)導(dǎo)入

由于我們需要為移動(dòng)設(shè)備處理大量的數(shù)據(jù)條目(十萬級(jí)),將所有的數(shù)據(jù)從服務(wù)器下載并導(dǎo)入到 iOS 設(shè)備中將花費(fèi)很長(zhǎng)時(shí)間。因此,我們會(huì)在應(yīng)用中附帶一份數(shù)據(jù)集的最新快照。這些快照使用經(jīng)過特殊設(shè)置的模擬器運(yùn)行生成,該設(shè)置可以在本地?cái)?shù)據(jù)不是最新的情況下從服務(wù)器獲取所需的數(shù)據(jù)。

然后我們對(duì)生成的 SQLite 數(shù)據(jù)庫文件運(yùn)行如下兩個(gè)命令:

sqlite> PRAGMA wal_checkpoint;
sqlite> VACUUM;

第一條命令確保日志中記錄的所有之前的修改同步到主 .sqlite 文件中,第二條命令確保文件不會(huì)過大。

當(dāng)應(yīng)用第一次啟動(dòng)時(shí),數(shù)據(jù)文件從應(yīng)用中被拷貝到最終的位置。想對(duì)這個(gè)過程以及其他導(dǎo)入數(shù)據(jù)到 Core Data 中的方法有更多了解,可以參考這篇文章。

因?yàn)?Core Data 數(shù)據(jù)模型中含有一個(gè)存儲(chǔ)版本號(hào)的特殊數(shù)據(jù)實(shí)體,應(yīng)用中包含的數(shù)據(jù)文件會(huì)自動(dòng)將正確的版本號(hào)寫入該實(shí)體中作為初始版本號(hào)。

壓縮

因?yàn)?JSON 格式數(shù)據(jù)體積相對(duì)較大,使用 gzip 格式壓縮發(fā)送給服務(wù)器的數(shù)據(jù)就變得非常重要。在請(qǐng)求中加入 Accept-Encoding: gzip 頭信息能讓服務(wù)器同樣使用 gzip 壓縮返回的數(shù)據(jù)。不過這只對(duì)服務(wù)器返回的數(shù)據(jù)有效,并不會(huì)在發(fā)送時(shí)啟用壓縮。

客戶端包含 Accept-Encoding 頭信息僅僅是為了告訴服務(wù)器自己能夠支持 gzip 格式的數(shù)據(jù),所以服務(wù)器應(yīng)該在支持 gzip 壓縮的情況下返回壓縮過的數(shù)據(jù)。一般情況下客戶端并不知道發(fā)送請(qǐng)求時(shí)服務(wù)器本身是否支持壓縮,所以默認(rèn)情況下客戶端發(fā)送的數(shù)據(jù)不能進(jìn)行壓縮。

在我們的這個(gè)案例中,因?yàn)榉?wù)器也是我們能夠控制的,所以我們可以保證能夠支持 gzip 壓縮。所以我們可以在發(fā)送數(shù)據(jù)到服務(wù)器時(shí)加上 Content-Encoding: gzip 頭信息并壓縮數(shù)據(jù),因?yàn)槲覀冎婪?wù)器肯定能夠處理??梢詤⒖?a rel="nofollow" >這個(gè) NSData category 獲取一個(gè) gzip 壓縮的案例。

臨時(shí) ID 和永久 ID

創(chuàng)建新的記錄時(shí),客戶端會(huì)為這些記錄分配臨時(shí) ID,這樣就可以記錄它們之間的關(guān)系并發(fā)送給服務(wù)器。我們使用了負(fù)數(shù)作為臨時(shí) ID,從 -1 開始依次遞減。當(dāng)前最新的臨時(shí) ID 會(huì)持久化存儲(chǔ)在標(biāo)準(zhǔn)的用戶預(yù)設(shè)值(standard user defaults)中。

由于我們采用了這種策略,一次只處理一個(gè)同步請(qǐng)求是非常重要的,同時(shí)我們也需要維護(hù)臨時(shí) ID 與服務(wù)器返回的真實(shí) ID 之間的映射關(guān)系。

發(fā)送同步請(qǐng)求之前,我們會(huì)檢查是否已經(jīng)收到對(duì)應(yīng)待提交修改的永久 ID。如果有的話,我們將這些待提交修改的臨時(shí) ID 換成永久 ID,并更新相應(yīng)的外鍵。如果我們不這樣做或者一次發(fā)送多個(gè)同步請(qǐng)求,可能會(huì)導(dǎo)致多次創(chuàng)建同一條記錄而不是在已有基礎(chǔ)上進(jìn)行更新,因?yàn)槲覀兪褂门R時(shí) ID 將這條記錄多次發(fā)送給了服務(wù)器。

因?yàn)樗接嘘?duì)列(在導(dǎo)入修改時(shí))和主上下文一樣也需要訪問這種映射關(guān)系,為了線程安全我們將對(duì)其的訪問封裝在了一個(gè)順序隊(duì)列中。

結(jié)論

構(gòu)建自己的數(shù)據(jù)同步方案并不是一個(gè)簡(jiǎn)單的任務(wù),很可能將花費(fèi)超出你想象的時(shí)間。至少處理本文中提及的各種同步系統(tǒng)邊界情況就會(huì)占用你很多時(shí)間。不過相應(yīng)的你也能得到靈活性和控制權(quán),比如同一套后端既為 Web 接口提供數(shù)據(jù),又在后臺(tái)做數(shù)據(jù)分析。

如果你面對(duì)的是一個(gè)罕見的同步場(chǎng)景(比如文章提到的例子中,我們需要在很多人的設(shè)備之間相互同步),你也許只能自己定制解決方案。也許這將是一個(gè)痛苦的過程,因?yàn)槟阈枰谀X子里不停地考慮各種邊界情況,不過這也意味著這是一個(gè)值得做的有趣項(xiàng)目。