# 跨平臺移動應用中偶爾連接的數據
**[Kevin Ashley](https://msdn.microsoft.com/zh-cn/magazine/ee532098.aspx?sdmr=KevinAshley&sdmi=authors)**
**[下載代碼示例](http://bit.ly/11yZyhN)**
大多數移動應用需要在某段時間內處于離線狀態,且移動用戶希望應用在連接和斷開連接的狀態下都可以正常工作。數以億計的移動設備用戶可能沒有意識到應用對于在線和離線狀態的需求;他們只希望應用可以在任何條件下運行。在本文中,我將向您展示一些方法,以了解如何使移動應用在兩種狀態下運行,并借助 Windows、iOS 和 Android 中的云通過跨平臺 Xamarin 工具和 Microsoft Azure 移動服務來輕松地同步數據。
作為一名移動應用開發人員,我之前遇到過需要同步離線數據的需求。對于我的 Winter Sports 滑雪應用 ([winter-sports.co](http://winter-sports.co/)) 和 Active Fitness 活動跟蹤應用 ([activefitness.co](http://activefitness.co/)),可能您無法在山坡上或跑步時保持流暢的連接。因此,這些應用需要能夠同步離線收集的數據,而又不會顯著影響電池壽命和可靠性。換句話說,這些應用需要可以在任何情況下高效地工作。
在考慮持久存儲時,并沒有表面上那么簡單。首先,同步有多種方法,即可在應用內進行同步或在操作系統中以后臺進程方式進行同步。此外,數據存儲有各種類型,例如來自傳感器的半結構化數據和可能存儲在 SQLite 中的關系數據。實現沖突解決策略也很重要,因為這樣可以盡可能減少數據損失和降級。最后,要管理多種數據格式,例如二進制、JSON、XML 和自定義。
移動應用通常可存儲多種類型的數據。結構化數據(如 JSON 或 XML)通常用于本機設置、本機文件或緩存。除了在文件中存儲數據以外,您還可以選擇使用存儲引擎(如 SQLite)來存儲和查詢數據。移動應用還可以存儲 Blob、媒體文件和其他類型的大型二進制數據。對于這些數據類型,我將演示一些讓數據傳輸在偶爾連接的設備上更加可靠的技術。我將概述幾種技術。例如,我將向您展示更全面的結構化和未結構化 (blob) 數據同步技術,而不是僅密切關注結構化數據的離線同步。我還將在這些示例中使用跨平臺方法。
## 跨平臺方法
由于將啟用傳感器的設備連接到云變得越來越受歡迎,因此我在我的項目中添加了設備傳感器數據以演示使用云進行同步的不同方法。我將討論三種情境:離線數據同步、手動數據同步和同步大型媒體和二進制數據。隨附的代碼示例完全跨平臺,可以在 Android、iOS 和 Windows 中 100% 重復使用。為實現此目的,我使用了 Xamarin.Forms,這是可在 iOS、Android 和 Windows 上正常工作的跨平臺 XAML/C# 工具,并且正在與 Visual Studio 工具集成(請觀看 Microsoft 第 9 頻道視頻“使用 Visual Studio 進行跨平臺移動開發”([bit.ly/1xyctO2](http://bit.ly/1xyctO2)))。
在代碼示例中,使用了 2 個類來管理跨平臺數據模型:SensorDataItem 和 SensorModel。此方法可供很多運動和健身跟蹤應用(例如 Active Fitness 或需要使用云從本機存儲同步結構化數據的應用)使用。我向 SensorDataItem 類添加了經度、緯度、速度和距離作為傳感器(如 GPS)收集的示例數據,以便闡述我的想法。當然,實際應用中的數據結構可能更加復雜,并且會包含依賴關系,不過我提供的示例只是讓您了解相關概念。
## 使用離線同步來同步結構化數據
離線同步是 Azure 移動服務中的一項強大的新增功能。您可以使用 NuGet 來引用 Visual Studio 項目中的 Azure 移動服務包。更重要的是,它還在包含新版 Azure 移動服務 SDK 的跨平臺應用中受支持。這意味著您可以在偶爾需要連接到云并同步其狀態的 Windows、iOS 和 Android 應用中使用此功能。
我先介紹幾個概念。
**同步表**?這是 Azure 移動服務 SDK 中新建的對象,用來區分支持同步的表和“本機”表。同步表可實現 IMobileServiceSyncTable 接口,并包含額外的“同步”方法,例如 PullAsync、PushAsync 和 Purge。如果您要使用云來同步離線傳感器數據,則需要使用同步表來代替標準表。在我的代碼示例中,我通過使用 GetSyncTable 調用來初始化我的傳感器數據同步表。在 Azure 移動服務門戶中,我創建了名為 SensorDataItem 的常規表,并向客戶端初始化中添加了**圖 1**?中的代碼(您可以在?[bit.ly/11yZyhN](http://bit.ly/11yZyhN)?中下載完整的源代碼)。
**同步上下文**?負責在本機和遠程存儲之間同步數據。Azure 移動服務將隨附基于常用 SQLite 庫的 SQLiteStore。**圖 1**?中的代碼可以完成這幾項工作。檢查同步上下文是否已初始化,如果未初始化,它會從 local.db 文件新建一個 SQLite 存儲實例,基于 SensorDataItem 類定義表并初始化該存儲。為了處理掛起的操作,同步上下文將使用可通過 PendingOperations 屬性訪問的隊列。Azure 移動服務提供的同步上下文還非常“智能”,可以區分本機存儲中發生的更新操作。同步將由系統自動完成,因此您無需手動不必要地調用云來保留數據。該功能比較好,因為它可以降低流量,并提高設備的電池使用壽命。
**圖 1 使用 MobileServiceSQLiteStore 對象進行同步**
~~~
// Initialize the client with your app URL and key
client = new MobileServiceClient(applicationURL, applicationKey);
// Create sync table instance
todoTable = client.GetSyncTable<SensorDataItem>();
// Later in code
public async Task InitStoreAsync()
{
? if (!client.SyncContext.IsInitialized)
? {
??? var store = new MobileServiceSQLiteStore(syncStorePath);
??? store.DefineTable<SensorDataItem>();
??? await client.SyncContext.InitializeAsync(store,
????? new MobileServiceSyncHandler?? ());
? }
}
~~~
**Push 操作**?通過將本機數據推送到服務器,該操作可用于在本機存儲和云存儲之間顯式同步數據。需要指出的是,在當前版本的 Azure 移動服務 SDK 中,您需要顯式調用 push 和 pull 以同步上下文。在整個同步上下文中執行 push 操作以幫助您保留表與表之間的關系。例如,如果我有表與表之間的關系,我的第一個插入將為我提供對象 Id,后續插入將保留引用完整性:
~~~
await client.SyncContext.PushAsync();
~~~
**Pull 操作**?通過將數據從遠程存儲提取到本機存儲,該操作可用于顯式同步數據。您可以使用 LINQ 來指定數據的子集或任何 OData 查詢。和在整個上下文中執行的 push 操作不同的是,pull 操作在表級別執行。如果項目在同步隊列中掛起,則在執行 pull 操作之前,會先推送這些項目以防止數據丟失(這是使用 Azure 移動服務進行數據同步的另一個好處)。在本示例中,我將提取之前存儲在服務器中的包含非零速度(例如,通過我的 GPS 傳感器收集)的數據:
~~~
var query = sensorDataTable.Where(s => s.speed > 0);
await sensorDataTable.PullAsync(query);
~~~
**Purge 操作**?此操作將清除本機和遠程表中指定的數據,同時觸發同步。和 pull 操作類似,您可以使用 LINQ 來指定數據的子集或任何 OData 查詢。在本示例中,我會將含有零距離(其可能來自我的 GPS 傳感器)的數據從我的表中清除:
~~~
var query = sensorDataTable.Where(s => s.distance == 0);
await sensorDataTable.PurgeAsync(query);
~~~
**適當地處理沖突**?當設備進入在線和離線狀態時,這是數據同步策略的一個重要部分。將發生沖突,而且 Azure 移動服務 SDK 會提供處理沖突的方法。為了讓沖突解決方案生效,我在 SensorDataItem 對象上啟用了 Version 屬性列。此外,我創建了 ConflictHandler 類,其可實現 IMobileServiceSyncHandler 接口。當您需要解決沖突時,可以選擇以下 3 個方案:保留客戶端值、保留服務器值或中止 push 操作。
在我的示例中,檢查 ConflictHandler 類。當其初始化后,我在構造函數中為其設置了以下 3 個沖突解決方案策略之一:
~~~
public enum ConflictResolutionPolicy
{
? KeepLocal,
? KeepRemote,
? Abort
}
~~~
根據該方法,每次發生沖突時,將在 ExecuteTableOperationAsync 方法中自動應用沖突解決方案策略。在初始化我的同步上下文時,我將 ConflictHandler 類傳遞到包含默認沖突解決方案策略的同步上下文:
~~~
await client.SyncContext.InitializeAsync(
? store,
? new ConflictHandler(client, ConflictResolutionPolicy.KeepLocal)
);
~~~
要了解有關沖突解決方案的更多信息,請參閱 MSDN 示例《Azure 移動服務 - 使用離線 WP8 處理沖突》([bit.ly/14FmZan](http://bit.ly/14FmZan)) 和 Azure 文檔文章《使用移動服務中的離線數據同步處理沖突》 ([bit.ly/1zA01eo](http://bit.ly/1zA01eo))。
## 手動同步序列化數據
在 Azure 移動服務開始提供離線同步之前,開發人員必須手動實現數據同步。因此,如果您開發了一個偶爾需要同步數據的應用,但未使用 Azure 移動服務離線同步功能,則可以手動進行同步(盡管我強烈建議考慮離線同步功能)。您可以在文件中使用直接對象序列化(如 JSON 序列化程序),或使用數據存儲引擎(如 SQLite)。離線同步機制和手動同步之間的主要不同之處在于手動同步需要您自己執行大部分工作。檢測數據是否已同步的一個方法是在數據模型中使用任何對象的 Id 屬性。例如,請參閱我在之前的示例中使用的 SensorDataItem 類,請注意**圖 2**?中所示的 "Id" 和“版本”字段。
**圖 2 數據同步的數據結構**
~~~
public class SensorDataItem
{
? public string Id { get; set; }
? [Version]
? public string Version { get; set; }
? [JsonProperty]
? public string text { get; set; }
? [JsonProperty]
? public double latitude { get; set; }
? [JsonProperty]
? public double longitude { get; set; }
? [JsonProperty]
? public double distance { get; set; }
? [JsonProperty]
? public double speed { get; set; }
}
~~~
在將一個記錄插入到遠程數據庫時,Azure 移動服務將自動創建 Id,并將其分配給對象,因此該 Id 在插入記錄時為非 null 值,在記錄從未與數據庫同步時為 null 值:
~~~
// Manually synchronizing data
if (item.Id == null)
{
? await this.sensorDataTable.InsertAsync(item);
}
~~~
手動同步刪除和更新是一個非常具有挑戰性且較為復雜的過程,不在本文的討論范圍內。如果您在尋求一個全面的同步解決方案,請考慮 Azure 移動服務 SDK 的離線同步功能。當然,本例比實際情境簡單一些,但是如果您希望實現手動數據同步,本示例可以啟發您從哪里開始著手。當然,由于 Azure 移動服務 SDK 提供一個經過測試和周詳考慮的數據同步解決方案,因此我建議尤其是在需要穩定的、經過測試的方法來保持本機和遠程數據同步的應用中嘗試離線同步方法。
## 向云中傳輸二進制數據、圖片和媒體
除了結構化數據以外,應用通常還需要同步未結構化或二進制數據或文件。例如移動拍照應用或需要將二進制文件(例如照片或視頻)上傳到云的應用。由于我在跨平臺上下文中探索此主題,而不同的平臺具有不同的功能。但是,它們真的大相徑庭嗎?可通過多種方法同步 Blob 數據,例如使用進程內服務或使用特定于平臺的進程外后臺傳輸服務。為了管理下載內容,我還提供了一個基于 ConcurrentQueue 的簡單 TransferQueue 類。每次我需要提交文件以便上傳或下載時,我會向隊列中添加一個新的 Job 對象。這是云中常用的模式,即將未完成的工作添加到隊列,然后讓其他后臺進程讀取該隊列,并完成該工作。
**進程內文件傳輸**?有時您需要直接在應用內傳輸文件。這是處理 Blob 傳輸的最直接的方式,但如我之前所述,它有一些弊端。為了保護 UX,操作系統對應用使用的帶寬和資源設置了上限。但此條件假設用戶正在使用該應用。在應用偶爾斷開連接的情況中,這可能并非最佳方法。直接從應用傳輸文件的有利方面是可以完全控制數據傳輸。通過完全控制,應用可以使用共享的訪問簽名方法來管理上載和下載內容。請訪問 Microsoft Azure 存儲團隊博文“表 SAS(共享的訪問簽名)、隊列 SAS 和對 Blob SAS 的更新的介紹”([bit.ly/1t1Sb94](http://bit.ly/1t1Sb94)),閱讀有關優勢。盡管不是所有平臺都內置了此功能,但是如果您希望使用基于 REST 的方法,可以使用 Azure 存儲服務中的 SAS 鍵。直接在應用中傳輸文件的不利方面有兩點。第一,您必須編寫更多代碼。第二,應用必須處于運行狀態,這樣可能會消耗電池使用壽命,并限制 UX。最佳解決方案是使用豐富的內置數據同步技術。
我在 BlobTransfer.cs 的跨平臺 Xamarin 應用中提供了可執行基本上載和下載操作的源代碼(請參閱隨附的代碼下載)。此代碼應可在 iOS、Android 和 Windows 上運行。為了使用獨立于平臺的文件存儲,我使用了 PCLStorage NuGet 包(PCLStorage 安裝包),其可允許我在 iOS、Android 和 Windows 上執行抽取文件操作。
為了初始化進程內傳輸,我調用了 TransferQueue AddInProcessAsync 方法:
~~~
var ok = await queue.AddInProcessAsync(new Job {
? Id = 1, Url = imageUrl, LocalFile = String.Format("image{0}.jpg", 1)});
~~~
這會計劃典型的進程內下載操作,其在 BlobTransfer 對象中定義,如**圖 3**?中所示。
**圖 3 下載操作(跨平臺代碼)**
~~~
public static async Task<bool> DownloadFileAsync(
? IFolder folder, string url, string fileName)
{
? // Connect with HTTP
? using (var client = new HttpClient())
? // Begin async download
? using (var response = await client.GetAsync(url))
? {
??? // If ok?
??? if (response.StatusCode == System.Net.HttpStatusCode.OK)
??? {
????? // Continue download
????? Stream temp = await response.Content.ReadAsStreamAsync();
????? // Save to local disk
????? IFile file = await folder.CreateFileAsync(fileName,
??????? CreationCollisionOption.ReplaceExisting);
????? using (var fs =
??????? await file.OpenAsync(PCLStorage.FileAccess.ReadAndWrite))
????? {
??????? // Copy to temp folder
??????? await temp.CopyToAsync(fs);?
??????? fs.Close();
??????? return true;
????? }
??? }
??? else
??? {
????? Debug.WriteLine("NOT FOUND " + url);
????? return false;
??? }
? }
}
~~~
當然,如果要上載文件,您可以使用如**圖 4**?中所示的方法在進程內執行此操作。
**圖 4 上載操作(跨平臺代碼)**
~~~
public static async Task UploadFileAsync(
? IFolder folder, string fileName, string fileUrl)
{
??// Connect with HTTP
? using (var client = new HttpClient())?
? {
??? // Start upload
??? var file = await folder.GetFileAsync(fileName);
??? var fileStream = await file.OpenAsync(PCLStorage.FileAccess.Read);
??? var content = new StreamContent(fileStream);
??? // Define content type for blob
??? content.Headers.Add("Content-Type", "application/octet-stream");
??? content.Headers.Add("x-ms-blob-type", "BlockBlob");
??? using (var uploadResponse = await client.PutAsync(
????? new Uri(fileUrl, UriKind.Absolute), content))
??? {
????? Debug.WriteLine("CLOUD UPLOADED " + fileName);
????? return;
??? }
? }
}
~~~
**使用特定于操作系統的傳輸服務進行進程外文件傳輸**?使用內置的文件傳輸服務下載和上載具有很多優勢。大多數平臺提供以后臺服務的方式匿名傳輸大型文件的服務(上載和下載皆可)。您應該盡可能使用此類服務,因為它們在進程外運行,即您的應用不會受在資源消耗方面可能會昂貴的實際數據傳輸的限制。此外,您的應用不必一直保留在內存中以傳輸文件,并且操作系統通常會提供沖突解決方案(重試)機制以重新啟動上載和下載。其他優勢包括要編寫的代碼較少,應用無需處于活躍狀態(操作系統管理其自己的上載和下載隊列),而且應用在內存/資源方面更加高效。但是,面臨的挑戰是此方法需要特定于平臺的實現:iOS、Windows Phone 等有其自己的后臺傳輸實現。
從概念上講,使用特定于操作系統的進程外服務的移動應用中的可靠文件上載類似于應用內實現。但是,實際上載/下載隊列管理外包給操作系統傳輸服務。對于 Windows Phone 應用商店應用和 Windows 應用商店應用,開發人員可以使用 BackgroundDownloader 和 BackgroundUploader 對象。對于 iOS 7 和更高版本,NSUrlSession 提供 CreateDownloadTask 和 CreateUploadTask 方法以初始化下載和上傳。
使用我之前提供的示例,現在我需要調用進程外方法以調用使用特定于操作系統的后臺傳輸服務的調用。事實上,因為該服務由操作系統進行處理,我將計劃 10 個下載內容以演示該應用未阻塞,且由操作系統處理執行(在本示例中,我使用了 iOS 后臺傳輸服務):
~~~
for (int i = 0; i < 10; i++)
{
? queue.AddOutProcess(new Job { Id = i, Url = imageUrl,
??? LocalFile = String.Format("image{0}.jpg", i) });
}
~~~
對于 iOS 后臺傳輸服務示例,請查看 BackgroundTransferService.cs。在 iOS 中,您首先需要使用 CreateBackgroundSessionConfiguration 來初始化后臺會話(請注意,此功能只適用于 iOS 8 或更高版本):
~~~
using (var configuration = NSUrlSessionConfiguration.
? CreateBackgroundSessionConfiguration(sessionId))
{
? session = NSUrlSession.FromConfiguration(configuration);
}
~~~
然后,您可以提交一個較長的上載或下載操作,操作系統將在您的應用中獨立對其進行處理:
~~~
using (var uri = NSUrl.FromString(url))
using (var request = NSUrlRequest.FromUrl(uri))
{
? downloadTask = session.CreateDownloadTask(request);
? downloadTask.Resume();
}
~~~
您還需要考慮可靠上載和下載 Blob 的隊列機制。
## 示例代碼和后續步驟
本文的所有示例代碼可從 GitHub ([bit.ly/11yZyhN](http://bit.ly/11yZyhN)) 中獲取。要使用此源代碼,您可以結合使用 Visual Studio 和 Xamarin 或 Xamarin Studio(可從?[xamarin.com](http://xamarin.com/)?下載)。該項目使用跨平臺 Xamarin.Forms 和帶有離線同步功能的 Azure 移動服務庫。對于后續步驟,您將會很高興地發現添加到社區庫的進程外服務(如 Xamarin Labs)和隊列功能以及類似于當前為 Azure 移動服務離線同步 SDK 中的結構化數據提供的內容的沖突解決方案。
概括來說,Microsoft Azure 移動服務提供了同步離線數據的強大有效的方法。您可以在 Windows、Android 和 iOS 跨平臺情境中使用這些服務。Microsoft 還提供易于使用的本機 SDK,其可在所有平臺上工作。通過集成這些服務并向您的應用添加離線同步關閉,您可以提高應用在斷開連接的情境中的可靠性。