Январь 2015

Том 30 выпуск 5


Microsoft Azure - Периодически подключаемые данные в кросс-платформенных мобильных приложениях

Kevin Ashley | Январь 2015

Продукты и технологии:

Microsoft Azure Mobile Services, Xamarin

В статье рассматриваются:

  • использование кросс-платформенной функциональности;
  • автономная синхронизация структурированных данных;
  • ручная синхронизация сериализованных данных;
  • синхронизация неструктурированных данных с облаком.

Исходный код можно скачать по ссылке

Большинству мобильных приложений приходится оставаться в автономном состоянии в течение некоторых периодов времени, и пользователи ожидают, что их приложения будут корректно работать как в подключенном, так и в отключенном от сети состоянии. Сотни миллионов пользователей мобильных устройств могут не осознавать, что требуется приложениям в подключенном и отключенном состояния; они просто ожидают, что приложения будут работать в любых обстоятельствах. В этой статье я покажу способы, позволяющие мобильному приложению работать в обоих состояниях и корректно синхронизировать данные с облаком в Windows, iOS и Android, используя кросс-платформенный инструментарий Xamarin и Microsoft Azure Mobile Services.

Я сам как разработчик мобильных приложений сталкивался с необходимостью синхронизировать автономные данные. Для моих приложений Winter Sports (winter-sports.co) и Active Fitness (activefitness.co) как-то само собой очевидно, что, катаясь на лыжах на склонах гор или в процессе пробежки, вряд ли удастся добиться приличного соединения. Так что эти приложения должны уметь синхронизировать собранные в автономном режиме данные, не оказывая значительного влияния на длительность работы аккумулятора и надежность. Иначе говоря, приложения должны работать эффективно в любых условиях.

Не все так просто, как кажется на первый взгляд, когда дело доходит до постоянного хранилища. Для начала, подходов к синхронизации несколько, ее можно выполнять из приложения или как фоновый процесс ОС. Кроме того, существуют разные типы хранилищ данных, в частности полуструктурированные данные от датчиков и потенциально реляционные данные, хранящиеся в SQLite. Также важно реализовать политику разрешения конфликтов, чтобы свести к минимуму вероятность потери и деградации данных. Наконец, у данных могут быть самые разнообразные форматы, такие как двоичный, JSON, XML и пользовательский.

Мобильные приложения обычно хранят несколько типов данных. Структурированные данные вроде JSON или XML, как правило, используются для локальных параметров, локальных файлов или кеширования. В дополнение к хранению данных в файлах вы можете выбирать механизмы хранения, например SQLite, для сохранения и запроса данных. Мобильные приложения также могут сохранять двоичные неструктурированные данные (blobs), медиа-файлы и другие типы больших двоичных данных. Для этих типов данных я продемонстрирую методы, которые делают передачу данных более надежной на периодически подключаемых устройствах. Я дам обзор нескольких технологий. Так, вместо того чтобы сосредоточиться строго на автономной синхронизации структурированных данных, я покажу вам более широкую картину — методы синхронизации как структурированных, так и неструктурированных данных. Кроме того, я буду применять кросс-платформенный подход во всех примерах.

Кросс-платформенный подход

Поскольку подключение устройств с датчиками к облаку становится все популярнее, я включил данные от аппаратного датчика в свой проект, чтобы продемонстрировать различные методы их синхронизации с облаком. Я рассмотрю три сценария: автономную синхронизацию данных (offline data sync), ручную синхронизацию данных (manual data synchronization) и синхронизацию больших медийных и двоичных данных. Сопутствующий пример кода является полностью кросс-платформенным, со 100% возможностью повторного использования в Android, iOS и Windows. Для этого я использовал Xamarin.Forms, кросс-платформенный инструментарий XAML/C#, который отлично работает в iOS, Android и Windows и интегрируется с инструментарием Visual Studio (см. видеоролик «Cross-Platform Mobile Development Using Visual Studio» на Microsoft Channel 9 по ссылке bit.ly/1xyctO2).

В этом примере кода два класса, которые управляют кросс-платформенными моделями данных: SensorDataItem и SensorModel. Этот подход можно применять во многих спортивных приложениях, таких как Active Fitness, или в приложениях, которым нужно синхронизировать структурированные данные из локального хранилища с облаком. Я добавил широту, долготу, скорость и расстояние в класс SensorDataItem как пример данных, собираемых датчиками, например GPS, для иллюстрации своей идеи. Конечно, структура данных в реальном приложении может быть сложнее (и включать зависимости), но мой пример даст вам представление о концепции.

Автономная синхронизация структурированных данных

Автономная синхронизация — мощное новое средство в Azure Mobile Services. Вы можете ссылаться на пакет Azure Mobile Services в своем проекте Visual Studio, используя NuGet. Еще важнее, что это поддерживается и в кросс-платформенных приложениях с помощью новой версии Azure Mobile Services SDK. То есть это средство можно задействовать в ваших приложениях Windows, iOS и Android, которым нужно периодически соединяться с облаком и синхронизировать свои состояния.

Начну с нескольких концепций.

Таблица синхронизации Это новый объект в Azure Mobile Services SDK, созданный для того, чтобы различать таблицы, которые поддерживают синхронизацию из «локальных» таблиц. Таблицы синхронизации реализуют интерфейс IMobileServiceSyncTable<T> и включают дополнительные методы «синхронизации», такие как PullAsync, PushAsync и Purge. Если вы хотите синхронизировать свои автономные данные от датчика с облаком, то должны использовать таблицы синхронизации вместо стандартных таблиц. В своем примере кода я инициализирую таблицу синхронизации данных от датчика вызовом GetSyncTable<T>. На портале Azure Mobile Services я создал обычную таблицу SensorDataItem и добавил код с рис. 1 к инициализации клиента (вы можете скачать полный исходный код по ссылке bit.ly/11yZyhN).

Рис. 1. Использование объекта MobileServiceSQLiteStore для синхронизации

// Инициализируем клиент с помощью URL приложения и ключа
client = new MobileServiceClient(applicationURL,
  applicationKey);
// Создаем экземпляр таблицы синхронизации
todoTable = client.GetSyncTable<SensorDataItem>();
// Далее в коде
public async Task InitStoreAsync()
{
  if (!client.SyncContext.IsInitialized)
  {
    var store = new MobileServiceSQLiteStore(syncStorePath);
    store.DefineTable<SensorDataItem>();
    await client.SyncContext.InitializeAsync(store,
      new MobileServiceSyncHandler());
  }
}

Контекст синхронизации Он отвечает за синхронизацию данных между локальным и удаленным хранилищами. Azure Mobile Services поставляется с SQLiteStore, основанной на популярной библиотеке SQLite. Код на рис. 1 делает несколько вещей. Он проверяет, инициализирован ли контекст синхронизации, и, если нет, создает новый экземпляр хранилища SQLite из файла local.db, определяет таблицу на основе класса SensorDataItem и инициализирует хранилище. Для обработки незавершенных операций контекст синхронизации использует очередь, доступную через свойство PendingOperations. Контекст синхронизации, предоставляемый Azure Mobile Services, является также достаточно «умным», чтобы различать операции обновления, выполняемые в локальном хранилище. Синхронизация выполняется системой автоматически, поэтому вам незачем вручную обращаться к облаку для сохранения данных. Это хорошо, потому что уменьшает трафик и продлевает работу аккумуляторов.

Операция push Позволяет явным образом синхронизировать данные между локальным и облачным хранилищами, передавая локальные данные на сервер. Важно отметить, что в текущей версии Azure Mobile Services SDK вы должны явно вызывать операции push и pull для синхронизации контекста. Операция push выполняется применительно ко всему контексту синхронизации, чтобы помочь вам сохранить связи между таблицами. Например, если у меня есть отношения между таблицами, моя первая операция вставки даст мне идентификатор объекта, а последующие операции вставки будут сохранять ссылочную целостность (referential integrity):

await client.SyncContext.PushAsync();

Операция pull Позволяет явным образом синхронизировать данные, извлекая данные из удаленного хранилища в локальное. С помощью LINQ можно указывать подмножество данных или выдавать любой OData-запрос. В отличие от операции push, которая выполняется применительно к контексту в целом, операция pull выполняется на уровне таблицы. Если в очереди синхронизации ожидают какие-то элементы, то сначала передаются они — до выполнения операций pull; это необходимо для того, чтобы предотвратить потерю данных (еще одно преимущество использования Azure Mobile Services для синхронизации данных). В этом примере я извлекаю данные, в чьих полях скорости указаны ненулевые значения (собранные, например, датчиком 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 Mobile Services SDK предусмотрены способы их обработки. Чтобы разрешение конфликтов работало, я включит свойство Version в объекте SensorDataItem. Кроме того, я создал класс ConflictHandler, реализующий интерфейс IMobileServiceSyncHandler. Когда вам нужно разрешить конфликт, вам предлагается три варианта: сохранить значение на клиенте, сохранить значение на сервере или отменить операцию push.

Изучите в моем примере класс ConflictHandler. При его инициализации я настраиваю в конструкторе одну из трех политик разрешения конфликтов:

public enum ConflictResolutionPolicy
{
  KeepLocal,
  KeepRemote,
  Abort
}

В зависимости от конкретного метода при каждом конфликте я автоматически применяю в методе ExecuteTableOperationAsync свою политику разрешения конфликтов. Инициализируя контекст синхронизации, я передаю свой класс ConflictHandler в контекст синхронизации с политикой разрешения конфликтов, используемой мной по умолчанию:

await client.SyncContext.InitializeAsync(store,
  new ConflictHandler(client,
  ConflictResolutionPolicy.KeepLocal)
);

Чтобы узнать больше о разрешении конфликтов, см. пример в MSDN «Azure Mobile Services — Handling Conflicts with Offline WP8» (bit.ly/14FmZan) и статью в документации Azure «Handling Conflicts with Offline Data Sync in Mobile Services» (bit.ly/1zA01eo).

Ручная синхронизация сериализованных данных

До того как в Azure Mobile Services появилась поддержка автономной синхронизации, разработчикам приходилось реализовывать синхронизацию данных вручную. Поэтому, если вы создаете приложение, которому периодически нужно синхронизировать данные, а функционал автономной синхронизации Azure Mobile Services не используется, то вы можете выполнять это вручную (хотя я настоятельно рекомендую задействовать поддержку автономной синхронизации). Вы можете использовать либо прямую сериализацию объектов в файлы (например, сериализатор JSON), либо системы хранения данных вроде SQLite. Основное различие между механизмом автономной синхронизации и ручной синхронизацией в том, что во втором случае вам потребуется самостоятельно выполнять большую часть работы. Один из методов распознавания того, были ли синхронизированы данные, — применение свойства Id любого объекта в вашей модели данных. Посмотрите на класс SensorDataItem, примененный в примере ранее, и обратите внимание на его поля Id и Version (рис. 2).

Рис. 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 Mobile Services автоматически создает Id и присваивает его объекту, поэтому Id будет содержать значение, отличное от null, когда запись вставлена, и null, когда запись еще не синхронизировалась с базой данных:

// Ручная синхронизация данных
if (item.Id == null)
{
  await this.sensorDataTable.InsertAsync(item);
}

Ручная синхронизация операций удаления и обновления гораздо труднее и включает процесс, описание которого выходит за рамки этой статьи. Если вы ищете исчерпывающее решение по синхронизации, присмотритесь к средствам автономной синхронизации Azure Mobile Services SDK. Конечно, этот пример прост по сравнению с реальными сценариями, но, если вы хотите реализовать ручную синхронизацию данных, это даст вам представление, откуда следует начинать. Разумеется, поскольку Azure Mobile Services SDK протестированное, хорошо продуманное решение для синхронизации данных, я рекомендую попробовать подход с автономной синхронизацией, особенно в приложениях, которым нужен надежный, проверенный метод поддержания синхронизации локальных и удаленных данных.

Передача двоичных данных, фотоснимков и медийных данных в облако

Помимо структурированных данных, приложения часто требуется синхронизировать неструктурированные или двоичные данные либо файлы. Возьмем мобильное приложение для работы с фотоснимками или приложение, которому нужно загружать двоичный файл в облако, например фотоснимок или видеоролик. Я исследую этот предмет в кросс-платформенном контексте, а у разных платформ разные возможности. Но так ли уж сильно они различаются? Синхронизировать двоичные данные можно несколькими способами, например с помощью внутрипроцессного (внутреннего) сервиса (in-process service) или использования внепроцессного (внешнего) (out-of process), специфичного для платформы фонового сервиса передачи данных. Чтобы управлять скачиваниями, я также предоставляю простой класс TransferQueue, основанный на ConcurrentQueue. Всякий раз, когда мне нужно передать файл для загрузки или скачивания, я добавляю в очередь новый объект Job. Это распространенный шаблон в облаке, где вы помещаете незаконченную работу в очередь, а затем какой-то другой фоновый процесс считывает очередь и выполняет эту работу.

Внутрипроцессная передача файлов Иногда нужно передавать файлы напрямую из приложения. Это самый очевидный способ обработки двоичных передач, но, как упоминалось ранее, у него есть свои недостатки. Для защиты пользовательской среды (UX) операционная система ограничивает полосу пропускания и ресурсы, выделяемые одному приложению. Однако это предполагает, что приложение используется для работы. В случае периодически отключаемых приложений такая идея не из лучших. Положительная сторона передачи файлов напрямую из приложения в том, что оно полностью контролирует передачу данных. Благодаря полному контролю приложение может задействовать преимущества методов с общей сигнатурой доступа (shared access signature, SAS) для управления загрузками и скачиваниями. Об этих преимуществах можно почитать в публикации в блоге Microsoft Azure Storage Team «Introducing Table SAS (Shared Access Signature), Queue SAS and Update to Blob SAS» (bit.ly/1t1Sb94). Не все платформы обеспечивают эту функциональность как встроенную, но, если вы хотите использовать подход на основе REST, вы определенно можете работать с SAS-ключами из Azure Storage Services. Недостатков у прямой передачи файлов из приложения два. Во-первых, вам придется писать больше кода. Во-вторых, приложение должно выполняться, что потенциально сокращает длительность работы от аккумуляторов и ограничивает UX. Лучшие решения опираются на встроенные средства синхронизации данных.

Я предоставил исходный код для базовой операции загрузки/скачивания в кросс-платформенном приложении Xamarin — BlobTransfer.cs (см. сопутствующий этой статье пакет исходного кода). Этот код должен работать в iOS, Android и Windows. Чтобы задействовать файловое хранилище, независимое от платформы, я использовал NuGet-пакет PCLStorage (установите командой Install-Package PCLStorage), который позволил мне абстрагировать файловые операции в iOS, Android и Windows.

Чтобы инициировать внутрипроцессную передачу, я вызываю свой метод AddInProcessAsync в TransferQueue:

var ok = await queue.AddInProcessAsync(new Job {
  Id = 1, Url = imageUrl, LocalFile = String.Format(
  "image{0}.png", 1)});

Это приводит к планированию типичной внутрипроцессной операции скачивания, определенной в объекте BlobTransfer (рис. 3).

Рис. 3. Операция скачивания (кросс-платформенный код)

public static async Task<bool> DownloadFileAsync(
  IFolder folder, string url, string fileName)
{
  // Создаем HTTP-соединение
  using (var client = new HttpClient())
  // Начинаем асинхронное скачивание
  using (var response = await client.GetAsync(url))
  {
    // Все ли в порядке?
    if (response.StatusCode == System.Net.HttpStatusCode.OK)
    {
      // Продолжаем скачивание
      Stream temp = await response.Content.ReadAsStreamAsync();
      // Сохраняем на локальный диск
      IFile file = await folder.CreateFileAsync(fileName,
        CreationCollisionOption.ReplaceExisting);

      using (var fs = await file.OpenAsync(
        PCLStorage.FileAccess.ReadAndWrite))
      {
        // Копируем во временную папку
        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)
{
  // Создаем HTTP-соединение
  using (var client = new HttpClient())
  {
    // Начинаем загрузку
    var file = await folder.GetFileAsync(fileName);
    var fileStream = await file.OpenAsync(
      PCLStorage.FileAccess.Read);
    var content = new StreamContent(fileStream);
    // Определяем тип контента для 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 Store и Windows Store разработчики могут использовать объекты 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}.png", 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();
}

Кроме того, вам нужно продумать механизм поддержки очереди для надежной загрузки и скачивания больших двоичных объектов (blobs).

Примеры кода и следующие шаги

Исходный код всех примеров для этой статьи доступен на GitHub по ссылке bit.ly/11yZyhN. Чтобы использовать этот исходный код, вы можете задействовать Visual Studio в сочетании с Xamarin или Xamarin Studio, которая доступна на xamarin.com. В проекте применяются кросс-платформенные Xamarin.Forms и библиотека Azure Mobile Services с автономной синхронизацией. В качестве следующих шагов было бы интересно подумать о внепроцессном сервисе, добавленном в библиотеки сообщества, например Xamarin Labs, а также о средствах поддержки очередей и разрешения конфликтов, аналогичных тем, которые сейчас предоставляются для структурированных данных в Azure Mobile Services Offline Sync SDK.

Суммируя, отметим, что Microsoft Azure Mobile Services предоставляет мощный и эффективный способ синхронизации автономных данных. Вы можете применять эти сервисы в кросс-платформенном сценарии в Windows, Android и iOS. Microsoft также предоставляет простые в использовании «родные» SDK, которые работают на каждой платформе. Вы можете улучшить надежность приложения в отключенном состоянии за счет интеграции этих сервисов и добавления автономной синхронизации.


Kevin Ashleyархитектор-идеолог в Microsoft. Соавтор книги «Professional Windows 8 Programming» (Wrox, 2012) и разработчик популярных игр и приложений, самое известное из которых — Active Fitness (activefitness.co). Часто выступает с презентациями технологий на различных мероприятиях, отраслевых выставках и веб-трансляциях. Работает со стартапами и партнерами, консультируя по дизайну ПО, стратегиям в области бизнеса и технологий, по архитектуре и разработке. Читайте его блог kevinashley.com и заметки в twitter.com/kashleytwit.

Выражаю благодарность за рецензирование статьи экспертам Microsoft Грегу Оливеру (Greg Oliver) и Бруно Теркали (Bruno Terkaly).