Microsoft Azure

クロスプラットフォーム モバイル アプリの断続的に接続されるデータ

Kevin Ashley

コード サンプルのダウンロード

多くのモバイル アプリは一定時間オフライン状態にとどまる必要があり、モバイル ユーザーは、アプリが接続状態であっても切断状態であってもスムーズに動作することを期待します。何億人もモバイル デバイス ユーザーがいても、アプリをオンライン状態およびオフライン状態にする必要性に気付いていない可能性があります。モバイル ユーザーにとっては、アプリがどのような状態であろうと動作することだけを想定します。今回は、クロスプラットフォームの Xamarin ツールと Microsoft Azure Mobile Services を使用して、Windows、iOS、および Android で、モバイル アプリをオンライン状態でもオフライン状態でも機能させる方法と、データをクラウドとスムーズに同期する方法について説明します。

私自身はモバイル アプリの開発者として、早くからオフライン データを同期する必要性を感じていました。以前に開発した "Winter Sports" スキー アプリ (winter-sports.co、英語) や "Active Fitness" 活動追跡アプリ (activefitness.co、英語) の場合、ゲレンデ上やエクササイズ中に十分な接続を確保できないことが予想されました。そのため、このようなアプリでは、バッテリー残量や信頼性に大きな影響を与えることなく、オフラインで収集したデータを同期できる必要があります。つまり、どのような状況でも効率よく動作することが求められます。

固定記憶域について検討するだけでも、見かけ以上に多くのことを考えることになります。まず、同期には複数の方法があり、アプリ内で行うことも、OS のバックグラウンド プロセスとして実行することもできます。また、保存するデータの種類もさまざまで、センサーから得られる半構造化されたデータや、SQLite に保存されるリレーショナル データも考えられます。競合解決ポリシーを実装して、データの損失や劣化を最小限に抑えることも重要です。バイナリ、JSON、XML、カスタムなど、多種多様なデータ形式を扱うことも必要です。

モバイル アプリは、一般的に複数種類のデータを保存します。通常、ローカル設定、ローカル ファイル、キャッシュには、JSON や XML などの構造化データを使用します。データはファイルに保存するだけでなく、SQLite のようなストレージ エンジンを使って保存およびクエリすることもあります。モバイル アプリの場合、BLOB、メディア ファイルなど、大量のバイナリ データを保存する可能性があります。今回は、このような種類のデータを、断続的に接続されるデバイスで確実に転送する方法を例を挙げて説明します。複数の手法について概要を取り上げますが、たとえば、構造化データのオフライン同期だけに注目するのではなく、構造化データと非構造化データ (BLOB) の両方の同期手法をより広い視点で示します。ここで紹介するすべての例に、クロスプラットフォームのアプローチを使用します。

クロスプラットフォーム アプローチ

センサー対応のデバイスをクラウドに接続する人気が高まっているため、プロジェクトでデバイス センサーのデータを取り込み、このデータをクラウドと同期するさまざまな方法についてデモを行います。オフライン データの同期、手動でのデータ同期、大量のメディア データやバイナリ データの同期という 3 つのシナリオを取り上げます。付属のコード サンプルは、Android、iOS、および Windows 向けに 100% 再利用可能で、クロスプラットフォームに完全対応します。これを実現するために、iOS、Android、および Windows で適切に機能し、Visual Studio のツールへの統合が進んでいる、クロス プラットフォーム XAML/C# ツールである Xamarin.Forms を使用します。詳細については、Microsoft Channel 9 のビデオ「Visual Studio を使用したクロスプラットフォーム モバイル開発」(bit.ly/1xyctO2、英語) を参照してください。

コード サンプルには、クロスプラットフォーム データのモデルを管理する SensorDataItem と SensorModel という 2 つのクラスがあります。このアプローチは、Active Fitness などの多くのスポーツやフィットネスの活動追跡アプリや、ローカル ストレージの構造化データをクラウドと同期する必要があるアプリに使用できます。ここでは考え方を示すために、GPS などのセンサーで収集したデータの例として、latitude (緯度)、longitude (経度)、speed (速度)、および distance (距離) を SensorDataItem クラスに追加しています。もちろん、実際のアプリのデータ構造はもっと複雑で、依存関係を含む可能性がありますが、今回示す例は概念についての考え方を示すものです。

オフライン同期による構造化データの同期

オフライン同期は、Microsoft Azure Mobile Services の強力な新機能です。Visual Studio のプロジェクトで NuGet を使用すれば、Microsoft Azure Mobile Services パッケージを参照できます。もっと重要なことは、新しいバージョンの Microsoft Azure Mobile Services SDK を使用するクロスプラットフォーム アプリでもオフライン同期がサポートされることです。つまり、不定期にクラウドに接続して状態を同期する必要がある Windows アプリ、iOS アプリ、および Android アプリで、オフライン同期機能を使用できます。

まず、いくつかの概念について説明します。

同期テーブル: Microsoft Azure Mobile Services SDK の新しいオブジェクトで、"ローカル" のテーブルからの同期をサポートするテーブルと区別するために作成されました。同期テーブルは、IMobileServiceSyncTable<T> インターフェイスを実装し、PullAsync、PushAsync、Purge などの "同期" メソッドが追加されています。オフラインのセンサー データをクラウドと同期する場合は、標準のテーブルではなく、同期テーブルを使用する必要があります。コード サンプルでは、GetSyncTable<T> 呼び出しを使用して、センサー データの同期テーブルを初期化しています。Microsoft Azure Mobile Services のポータルで SensorDataItem という標準テーブルを作成し、図 1 のコードをクライアントの初期化に追加します (bit.ly/11yZyhN (英語) で完全なソース コードをダウンロードできます)。

同期コンテキスト: ローカル ストアとリモート ストア間のデータ同期を担当します。Microsoft Azure Mobile Services は、広く使用されている SQLite ライブラリに基づく SQLiteStore を搭載しています。図 1 のコードでは、いくつかの処理を行っています。同期コンテキストを既に初期化したかどうかをチェックしています。初期化していなければ、local.db ファイルから SQLite ストアの新しいインスタンスを作成し、SensorDataItem クラスに基づいてテーブルを定義して、ストアを初期化します。保留中の操作を処理するために、同期コンテキストは PendingOperations プロパティ経由でアクセスできるキューを使用します。Microsoft Azure Mobile Services によって提供される同期コンテキストは "高い機能を備えて" おり、ローカル ストアで行われる更新操作を区別できます。同期はシステムによって自動で行われるため、データを保存するために、クラウドに対して手動で接続したり、不要な呼び出しを行うことはありません。これにより、トラフィックが少なくなり、デバイスのバッテリー残量を節約できます。

図 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   ());
  }
}

プッシュ操作: ローカル データをサーバーに送信することで、ローカル ストアとクラウド ストア間で明示的にデータを同期できるようにします。現バージョンの Microsoft Azure Mobile Services SDK では、プッシュ操作とプル操作を明示的に呼び出してコンテキストを同期する必要があります。プッシュ操作は同期コンテキスト全体で実行され、テーブル間のリレーションシップを保存できるようにします。たとえば、テーブル間にリレーションシップがある場合、最初の挿入でオブジェクトの ID を取得し、その後に続く挿入で参照整合性を確保します。

 

await client.SyncContext.PushAsync();

プル操作: データをリモート ストアからローカル ストアに取り出すことで、明示的にデータを同期できるようにします。LINQ を使用してデータのサブセットまたは OData のクエリを指定できます。コンテキスト全体で行われるプッシュ操作とは異なり、テーブル レベルで実行されます。アイテムが同期キュー内で保留中になっている場合、プル操作が実行される前に保留中のアイテムを最初にプッシュして、データが失われないようにします (データ同期に Microsoft Azure Mobile Services を使用するもう 1 つの別のメリットです)。以下の例では、GPS センサーによって収集されて以前にサーバーに保存された、速度 (speed) が 0 でないデータを取り出します。

var query = sensorDataTable.Where(s => s.speed > 0);
await sensorDataTable.PullAsync(query);

削除操作: 同期によって、ローカル テーブルとリモート テーブルの指定されたデータを消去します。プル操作と同様、LINQ を使用してデータのサブセットまたは OData のクエリを指定できます。以下の例では、先ほどと同様、GPS センサーによって収集された距離 (distance) が 0 のデータをテーブルから削除します。

var query = sensorDataTable.Where(s => s.distance == 0);
await sensorDataTable.PurgeAsync(query);

競合の適切な処理: データ同期戦略では、デバイスがオンラインまたはオフラインになる時点が重要です。競合が発生するのはこのタイミングです。Microsoft Azure Mobile Services SDK は、競合に対処する手段を提供します。競合の解決を機能させる場合は、SensorDataItem オブジェクトの Version プロパティ列を有効にします。また、IMobileServiceSyncHandler インターフェイスを実装する ConflictHandler クラスを作成します。競合を解決する必要がある場合に選択できるオプションは 3 つあり、クライアントの値を優先するか、サーバーの値を優先するか、またはプッシュ操作を中止します。

今回の例の ConflictHandler クラスを見てみます。このクラスを初期化するときに、コンストラクターで以下の 3 つの解決ポリシーのいずれかを設定します。

public enum ConflictResolutionPolicy
{
  KeepLocal,
  KeepRemote,
  Abort
}

方法によって異なりますが、今回は競合が発生するたびに、ExecuteTableOperationAsync メソッドで競合解決ポリシーを自動的に適用します。同期コンテキストを初期化するときに、以下のように既定の競合解決ポリシーを指定して、ConflictHandler クラスを同期コンテキストに渡します。

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

競合解決の詳細については、MSDN のサンプル「Microsoft Azure Mobile Services - オフラインの Windows Phone 8 との競合の処理」(bit.ly/14FmZan、英語) および Microsoft Azure に関するドキュメント記事「モバイル サービスでのオフライン データの同期との競合の処理」(https://azure.microsoft.com/ja-jp/documentation/articles/mobile-services-windows-store-dotnet-handling-conflicts-offline-data/) を参照してください。

シリアル化したデータの手動同期

Microsoft Azure Mobile Services によってオフライン同期の提供が開始されるまでは、開発者は手動のデータ同期を実装する必要がありました。Microsoft Azure Mobile Services のオフライン同期機能を使用しないで不定期にデータを同期する必要があるアプリを開発する場合は、同期を手動で行います (いずれにせよ、オフライン同期機能は見ておくことを強くお勧めします)。手動で同期を行う場合は、オブジェクトをファイルに直接シリアル化する JSON シリアライザーや、SQLite などのデータ格納エンジンを使用します。オフライン同期メカニズムと手動同期の大きな違いは、手動同期の場合大半の作業を開発者自身が行う必要があることです。データが同期されているかどうかを検出する 1 つの方法は、データ モデル内の任意のオブジェクトの 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; }
}

レコードをリモート データベースに挿入するときは、Microsoft Azure Mobile Services が自動的に ID を作成してそのオブジェクトに割り当てます。そのため、挿入が既に完了しているレコードの場合、ID は null 以外の値になり、データベースと同期されていないレコードの場合、ID は null 値になります。

// Manually synchronizing data
if (item.Id == null)
{
  await this.sensorDataTable.InsertAsync(item);
}

削除と更新を手動で同期するのはかなり難しく、複雑なプロセスなのでここでは説明しません。包括的な同期ソリューションが必要ならば、Microsoft Azure Mobile Services SDK のオフライン同期機能を検討します。今回の例は実際のシナリオに比べてシンプルなのは言うまでもありませんが、データの手動同期を実装する場合には、この例で基本的な考え方を知ることができます。もちろん、Microsoft Azure Mobile Services SDK は、データの同期にテスト済みの綿密なソリューションを提供しているため、信頼できるテスト済みのメソッドを使用してローカル データとリモート データを同期する必要があるアプリでは特に、Microsoft Azure Mobile Services SDK のオフライン同期アプローチをお勧めします。

クラウドへのバイナリ データ、写真、およびメディアの転送

アプリでは、構造化データだけでなく、構造化されていないデータやバイナリ データ、またはファイルを同期する必要が生じることがよくあります。そこで、モバイル フォト アプリや、バイナリ ファイル (写真、動画など) をクラウドにアップロードするアプリについて考えます。今回はこのテーマをクロスプラットフォームのコンテキストで調べることになるので、プラットフォームが異なればその機能も異なると考えます。しかし、本当に違いはあるのでしょうか。BLOB データの同期は、プロセス内のサービスを使用する方法や、プロセス外のプラットフォーム固有のバックグラウンド転送サービスを使用する方法など、複数の方法で実現できます。今回はダウンロードを管理するために、ConcurrentQueue に基づくシンプルな TransferQueue クラスを用意しました。アップロードまたはダウンロードでファイルを送信する必要があるたびに、新しい Job オブジェクトをキューに追加します。これはクラウドでの一般的なパターンで、未完了の作業をキューに追加しておいて、他のバックグラウンド プロセスがキューを読み取って作業を完了できるようにします。

プロセス内でのファイル転送: アプリ内から直接ファイルを転送する必要が生じることもあります。これは、BLOB の転送を処理する最も明確な方法ですが、前述のとおりデメリットがあります。ユーザー エクスペリエンスを保護するために、OS によってアプリが使用する帯域幅やリソースに制限が設けられています。ただし、この制限はユーザーが作業のためにアプリを使用中であることが前提です。断続的に接続されるアプリの場合、この方法はベストな考え方とは言えません。アプリから直接ファイルを転送するメリットは、データの転送を完全に制御できることです。転送を完全に制御できれば、アップロードとダウンロードを管理するために Shared Access Signature (SAS) 手法を利用できます。この手法のメリットの詳細については、Microsoft Azure Storage チームのブログ投稿「Introducing Table SAS (Shared Access Signature), Queue SAS and Update to Blob SAS」(テーブル SAS (Shared Access Signature)、キュー SAS、および BLOB SAS の更新の概要、bit.ly/1t1Sb94、英語) を参照してください。この機能が組み込まれていないプラットフォームもありますが、REST ベースのアプローチを使用してもかまわなければ、Microsoft Azure Storage Services の SAS キーを間違いなく利用できます。アプリから直接ファイルを転送する場合の問題点は 2 つあります。1 つは、記述するコード量が多くなります。もう 1 つは、アプリを実行状態にしなければならず、バッテリーを消費してユーザー エクスペリエンスを制限する可能性があります。最適なソリューションは、組み込みの優れたデータ同期手法を利用することです。

今回は、クロスプラットフォーム Xamarin アプリで基本的なアップロード操作とダウンロード操作を行うソース コードを BlobTransfer.cs に用意しました (付属のコード ダウンロードを参照)。このコードは iOS、Android、および Windows で問題なく動作します。プラットフォームに依存しないファイル ストレージを使用するために、iOS、Android、および Windows でのファイル操作を抽象化できる PCLStorage NuGet パッケージ (Install-Package PCLStorage) を使用しました。

プロセス内での転送を開始するには、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;
    }
  }
}

OS 固有の転送サービスを使用したプロセス外でのファイル転送: 組み込みのファイル転送サービスを使用してダウンロードとアップロードを行うことには多くのメリットがあります。ほとんどのプラットフォームは、大きなファイルをバックグラウンド サービスとして自動転送 (アップロードとダウンロードの両方) するサービスを提供しています。このようなサービスはプロセス外で実行されるため、できる限りこのようなサービスを利用することをお勧めします。つまり、リソースをかなり消費する可能性がある実際のデータ転送によってアプリが制限を受けることがありません。また、ファイルを転送するためにアプリをメモリに常駐させる必要もなく、通常、アップロードやダウンロードを再開 (リトライ) する競合解決メカニズムが OS によって用意されます。他にも、記述するコード量が少なくなる、アプリをアクティブにする必要がない (OS が独自にアップロードとダウンロードのキューを管理)、アプリでのメモリ/リソースの使用効率が高まるといったメリットもあります。ただし、この手法はプラットフォームごとに固有の実装が求められるという課題があります。iOS、Windows Phone などのプラットフォームには、それぞれバックグラウンド転送の独自の実装があります。

モバイル アプリで OS 固有のプロセス外のサービスを使用して信頼できる方法でファイルをアップロードする場合の考え方は、一見アプリ内に実装する方法と似ています。ただし、実際のアップロード/ダウンロード キューの管理は OS 転送サービスが行います。Windows Phone ストア アプリや Windows ストア アプリの場合は BackgroundDownloader オブジェクトと BackgroundUploader オブジェクトを使用できます。iOS 7 以上の場合、NSUrlSession によって、ダウンロードとアップロードを開始するための CreateDownloadTask メソッドと CreateUploadTask メソッドが提供されます。

前述の例を使用している場合は、プロセス外のメソッドを呼び出して、OS 固有のバックグラウンド転送サービスを使用する呼び出しを行えるようになります。実際には OS によってサービスが処理されるため、10 個のダウンロードのスケジュールを設定して、実行がアプリによって妨害されずに OS によって処理されることをデモします (今回の例では、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);
}

次に、時間がかかるアップロード操作またはダウンロード操作を送信でき、OS によってアプリとは無関係に処理されます。

using (var uri = NSUrl.FromString(url))
using (var request = NSUrlRequest.FromUrl(uri))
{
  downloadTask = session.CreateDownloadTask(request);
  downloadTask.Resume();
}

また、BLOB を確実にアップロードおよびダウンロードするには、キューのメカニズムについても考える必要があります。

サンプル コードと次のステップ

本稿付属のサンプル コードはすべて GitHub (bit.ly/11yZyhN、英語) からダウンロードできます。このソース コードを使用する場合、Xamarin と Visual Studio、または Xamarin Studio を使用できます (xamarin.com、英語から入手可能)。プロジェクトでは、クロスプラットフォームの Xamarin.Forms とオフライン同期を備えた Microsoft Azure Mobile Services ライブラリを使用しています。次のステップとして、Xamarin Labs などのコミュニティ ライブラリに追加されたプロセス外のサービスや、Microsoft Azure Mobile Services SDK のオフライン同期で構造化データ用に現在提供されているものに似たキュー機能や競合解決について確認してみてください。

Microsoft Azure Mobile Services は、オフライン データを同期する強力かつ効率の高い方法を提供します。このようなサービスは、Windows、Android、および iOS でのクロスプラットフォーム シナリオで使用できます。マイクロソフトでは、すべてのプラットフォームで機能し、簡単に使用できるネイティブ SDK も提供しています。このようなサービスを統合してオフライン同期機能をアプリに追加することで、断続的に接続されるシナリオでアプリの信頼性を向上することができます。


Kevin Ashley はマイクロソフトのアーキテクト エバンジェリストです。彼は『Professional Windows 8 Programming』(Wrox、2012 年) の共著者であり、人気アプリやゲームの開発者でもあります。代表的なアプリは Active Fitness (activefitness.co、英語) です。Kevin はさまざまなイベント、産業展覧会や Web キャストで技術発表をよく行っています。新興企業やパートナーに協力し、ソフトウェア設計、ビジネスとテクノロジ戦略、アーキテクチャ、および開発に関するアドバイスを行っています。Kevin のブログは kevinashley.com (英語) で Twitter は twitter.com/kashleytwit (英語) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Greg Oliver および Bruno Terkaly に心より感謝いたします。