在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ iOS/ 同步案例學習
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 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ù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導航應(yīng)用
線程安全類的設(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 的多任務(wù)
自定義 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 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標簽
同步案例學習
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

同步案例學習

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

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

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

應(yīng)用場景

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

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

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

要求

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

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

設(shè)計

API

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

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

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

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

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

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

上例中的 JSON 數(shù)據(jù)頂層含有 maxRevisionchangeSets 兩個 key。maxRevision 用來唯一標識客戶端當前數(shù)據(jù)版本的版本號,changeSets 則是一個以數(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 對應(yīng)這個修改集中涉及的所有數(shù)據(jù)實體類型。每個類型又會對應(yīng)一個針對它自己的修改集合,包含創(chuàng)建(create)、更新(update)和刪除(delete)操作,每個操作對應(yīng)指定的記錄 ID。這些 ID 最后會對應(yīng)到針對這條記錄的哪些屬性(attributes)進行了新建或更新操作。

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

接下來我們看一個復(fù)雜一點的例子。假設(shè)我們?yōu)橐慌_設(shè)備上其中一名隊員添加了新的單屏顯示數(shù)據(jù),當需要同步到服務(wù)器上時,如下是請求中包含的數(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 是負數(shù),這是因為它們是新建的條目。新建的 screen_instance 條目 ID 是 -10,在后面的 screen_instance_items 條目中引用到了這個 ID 作為外鍵。

當服務(wù)器處理完這個請求后(假設(shè)沒有沖突或者權(quán)限問題),發(fā)回給客戶端的響應(yī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,
                        ...
                    }
                }
            }
        }
    ]
}

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

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

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

解決沖突

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

有了版本號的幫助,這個方案變得很容易實現(xiàn)??蛻舳税l(fā)送給服務(wù)器的任何修改,都包含有對應(yīng)數(shù)據(jù)條目的版本號。因為客戶端從來不修改已有版本號,所以版本號反映了客戶端上一次與服務(wù)器交互的數(shù)據(jù)情況。服務(wù)器就可以根據(jù)版本號搜索對應(yīng)的條目,并屏蔽基于非最新數(shù)據(jù)的修改。

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

問題在于客戶端在沖突發(fā)生后如何將數(shù)據(jù)恢復(fù)到正常狀態(tài)。因為修改可能在客戶端處于離線狀態(tài)下進行,一天之后才會上傳到服務(wù)器,所以必須保存一份精確的修改日志并存儲起來。這樣我們就可以在沖突發(fā)生時撤銷任何修改。

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

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

實現(xiàn)

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

后端

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

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

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

Core Data

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

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

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

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

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

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

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

合并策略

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

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

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

初始數(shù)據(jù)導入

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

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

sqlite> PRAGMA wal_checkpoint;
sqlite> VACUUM;

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

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

因為 Core Data 數(shù)據(jù)模型中含有一個存儲版本號的特殊數(shù)據(jù)實體,應(yīng)用中包含的數(shù)據(jù)文件會自動將正確的版本號寫入該實體中作為初始版本號。

壓縮

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

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

在我們的這個案例中,因為服務(wù)器也是我們能夠控制的,所以我們可以保證能夠支持 gzip 壓縮。所以我們可以在發(fā)送數(shù)據(jù)到服務(wù)器時加上 Content-Encoding: gzip 頭信息并壓縮數(shù)據(jù),因為我們知道服務(wù)器肯定能夠處理。可以參考這個 NSData category 獲取一個 gzip 壓縮的案例。

臨時 ID 和永久 ID

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

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

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

因為私有隊列(在導入修改時)和主上下文一樣也需要訪問這種映射關(guān)系,為了線程安全我們將對其的訪問封裝在了一個順序隊列中。

結(jié)論

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

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