# 跨平台移动应用中偶尔连接的数据
**[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,其可在所有平台上工作。通过集成这些服务并向您的应用添加离线同步关闭,您可以提高应用在断开连接的情境中的可靠性。