Special Windows 10 issue 2015

Volume 30 Number 11

アプリ ライフサイクル - バックグラウンド タスクと延長実行によるアプリ稼働状態の維持

Shawn Henry | Windows 2015

かつて、アプリのライフサイクルは簡単でした。ユーザーがアプリを起動すると、アプリはシステム上で制約も受けずに実行されます。リソースを利用し、ポップアップ ウィンドウを表示して、通常は他のアプリへの配慮もなく、自由に動作していました。最近は、状況が厳しくなっています。世の中はモバイル優先になり、アプリは、アプリ ライフサイクルの規定によって制約を受けます。アプリ ライフサイクルは、アプリの実行を許可するタイミングを指定します。そして、大半の時間は実行が許可されません。つまり、ユーザーが現在操作中のアプリ以外は、実行が許可されません。

ほとんどの場合、この考え方は適切です。アプリが電力を消費していないことや、パフォーマンスを低下させていないことがユーザーにわかるからです。アプリにとっては、アクティブに使用されていないときは休止状態に入るという、いつもの推奨手順を OS が強制しているにすぎません。ユニバーサル Windows プラットフォーム (UWP) のアプリ モデルでは、この考え方が特に重要になります。それは UWP のアプリ モデルは、スマートフォンやモノのインターネット (IoT) デバイスのようなローエンドのデバイスから、最高性能のデスクトップや Xbox まで、多岐に渡るスケーラビリティが求められるためです。

複雑なアプリにとっては、一見、この最新アプリ モデルでは制約が大きいように思えますが、アプリがフォアグラウンドで実行されていない場合でも実行を延長する (タスクを完了して通知を配信する) 方法がいくつかあります。それが今回のテーマです。

アプリ ライフサイクル

これまでの Win32 や .NET デスクトップ開発では、アプリは通常、「実行中」か「停止」のいずれかの状態になり、大半の時間は「実行中」の状態です。これは明白な事実のように感じますが、Skype などのインスタント メッセージ アプリや Spotify などのミュージック アプリを考えてみてください。このようなアプリのユーザーは、アプリを起動し、いくつか操作 (メッセージの送信や音楽の検索) を行えば、その後はアプリから離れて別の作業を行います。その間、Skype はバックグラウンドに移動し、メッセージの着信を待機し、Spotify は音楽の再生を続けます。これは、最新のアプリ (UWP を基にする Windows アプリなど) とは対照的で、大半の時間は「実行中」以外の状態になります。

Windows アプリは、常に、「実行中」、「中断」、または「停止」の 3 つの状態のいずれかになります (図 1 参照)。[スタート] メニューのタイルをタップするなど、ユーザーが Windows アプリを起動すると、アプリはアクティブになり、「実行中」状態になります。ユーザーがアプリを操作している間は「実行中」状態のままです。ユーザーがアプリから離れるか、アプリを最小化すると、中断イベントが発生します。この時点で、アプリは状態をシリアル化します。アプリの作業中のページや一部入力済みのフォーム データなど、アプリが再開または再アクティブ化されるときに必要になる可能性があるすべての状態をシリアル化します。アプリが「中断」状態になると、Windows はアプリのプロセスとスレッドを停止し、アプリは実行できなくなります。ユーザーがアプリの操作に戻ると、アプリのスレッドは停止状態を解除されて、アプリは再び「実行中」状態を再開します。

Windows アプリ ライフサイクル
図 1 Windows アプリ ライフサイクル

アプリが「中断」状態であっても、そのアプリが使用しているリソース (通常はメモリ) が、他のアプリを実行するといった別の処理に必要になると、そのアプリは「停止」状態に移行します。

アプリ実行の観点から見ると、「中断」状態と「停止」状態の違いは、アプリがメモリ内に常駐することを許可されるかどうかです。アプリが単に「中断」状態になっている場合、アプリの実行は停止されていても、そのアプリの状態情報はすべてメモリ内に常駐します。アプリが「停止」状態になると、そのアプリの状態情報はすべてメモリから削除されます。アプリが「中断」状態から「停止」状態に移行すると、イベントは発生しなくなるため、「停止」状態から再度アクティブになるときに備えて、その時点でメモリ内にあるすべての状態情報をシリアル化することが重要です。

図 2 は、アプリがライフサイクルを遷移する過程でのリソースの使用状況を示します。アプリがアクティブになると、メモリの利用が開始され、一般には比較的安定した状態になります。アプリが「中断」状態に入ると、通常はメモリ使用量が低下します。バッファーや使用中のリソースが解放されて、CPU 使用率がゼロになります (OS による制御)。アプリが「中断」状態から「停止」状態に移行すると、再度 OS の制御により、メモリ使用量も CPU 使用率もゼロになります。

アプリ ライフサイクルとリソース使用状況
図 2 アプリ ライフサイクルとリソース使用状況

大容量の RAM と大きなページ ファイルを備えたデスクトップ システムでは、アプリがメモリから削除されることはあまりありませんが (削除されないわけではありません)、モバイル端末など、リソースに制約があるデバイスでは、「停止」状態に移行するのが一般的です。このため、多様なデバイスで UWP アプリをテストすることが重要になります。Visual Studio 付属の Windows エミュレーターは、こうしたテストに威力を発揮し、開発者はメモリ容量が 512MB しかないデバイスを想定してテストすることができます。

延長実行

ここまで説明したアプリ ライフサイクルは効率的です。アプリが使用されていなければ、リソースが消費されません。しかし、アプリが必要とする処理が行われないという状況も発生します。たとえば、ソーシャル アプリやコミニケーション アプリの典型的なユース ケースとしては、通常、サインイして、一連のクラウド データ (連絡先、フィード、会話履歴など) を同期します。先ほどのアプリ基本ライフサイクルであれば、連絡先をダウンロードするまで、ユーザーがアプリを開き、終始フォアグラウンド状態を維持しなければなりません。ほかにも、ユーザーの位置を追跡するナビゲーション アプリやフィットネス アプリが考えられます。ユーザーが別のアプリに切り替えたり、デバイスをポケットにしまうと、それ以降は機能しなくなります。このような場合は、アプリをもう少し長く実行できるメカニズムが必要です。

UWP では、このような状況に利用できる「延長実行」という概念が導入されます。延長実行を使用できるのは、次の 2 つの状況です。

  1. アプリが「実行中」状態で、フォアグラウンドでの標準実行期間内の任意の時点。
  2. アプリの中断イベント ハンドラー内で中断イベントを受け取った後 (OS がアプリを「中断」状態に移行しようとするとき)。

どちらの状況でもコードは同じですが、アプリの動作は若干異なります。最初の状況では、通常ならアプリが「中断」状態に入る状況 (ユーザーがアプリケーションから離れる場合など) でも、アプリは「実行中」状態のままになります。延長実行が有効であれば、アプリは中断イベントを受け取りません。延長期間が終了すると、アプリは再び「中断」状態に入るようになります。

もう 1 つの状況では、アプリが「中断」状態に入る状況になっても、延長期間中は中断への移行が保留されている状態になります。延長期間が終了すると、アプリは特に通知を行わずに「中断」状態に入ります。

図 3 に、アプリの中断状態を延長するために延長実行を使用する方法を示します。まず、ExtendedExecutionSession を新規作成し、理由と説明を指定します。この 2 つのプロパティを使用し、アプリに適切なリソース一式 (許容メモリ量、CPU 使用率、実行時間) を割り当て、バックグラウンドで実行中のアプリに関する情報をユーザーに公開します。次に、失効イベント ハンドラーをフックします。失効ハンドラーは、Windows が延長をサポートできなくなったときに呼び出されます。たとえば、フォアグラウンド アプリや VoIP 呼び出しの着信など、優先度の高い別のタスクがリソースを必要とする場合に延長期間が失効します。最後に、延長を要求して、要求に成功すると保存操作を開始します。延長が拒否されると、延長を受け取らなかった場合の通常の中断イベントと同様に中断処理を実行します。

図 3 OnSuspending ハンドラーでの延長実行

private async void OnSuspending(object sender, SuspendingEventArgs e)
{
  var deferral = e.SuspendingOperation.GetDeferral();
  using (var session = new ExtendedExecutionSession())
  {
    session.Reason = ExtendedExecutionReason.SavingData;
    session.Description = "Upload Data";
    session.Revoked += session_Revoked;
    var result = await session.RequestExtensionAsync();
    if (result == ExtendedExecutionResult.Denied)
    {
      UploadBasicData();
    }
    // Upload Data
    var completionTime = await UploadDataAsync(session);
  }
  deferral.Complete();
}

図 4 に、この延長実行がアプリ ライフサイクルに与える影響を示します。図 2 と比較してみてください。アプリが中断イベントを受け取ると、リソースの解放とシリアル化を開始します。もっと時間が必要だとアプリが判断し、延長を行うと、延長期間が終了するまで中断を保留している状態になります。

延長実行期間のリソース使用状況
図 4 延長実行期間のリソース使用状況

延長実行は必ずしも中断ハンドラー内で要求される (先ほどの 2 番目の状況) わけではありません。アプリが「実行中」状態の任意の時点で延長が要求される場合もあります。前述のナビゲーション アプリのように、バックグラウンドで実行を継続しなければならないことが事前にわかっているアプリでは、この方法が役に立ちます。図 5 に示すように、コードは前の例とほぼ同じです。

図 5 通常実行中の延長実行

private async void StartTbTNavigationSession()
{
  using (var session = new ExtendedExecutionSession())
  {
    session.Reason = ExtendedExecutionReason.LocationTracking;
    session.Description = "Turn By Turn Navigation";
    session.Revoked += session_Revoked;
    var result = await session.RequestExtensionAsync();
    if (result == ExtendedExecutionResult.Denied
    {
      ShowUserWarning("Background location tracking not available");
    }
    // Do Navigation
    var completionTime = await DoNavigationSessionAsync(session);
  }
}

図 6 に、このシナリオでのアプリ ライフサイクルを示します。繰り返しになりますが、コードはほぼ同じです。違いは、延長実行が終了するときに、アプリが「中断」状態から「停止」状態へすぐに移行することが多い点です。一般に、延長期間が失効するのは、リソースが足りなくなるときのみなので、この状況を緩和するにはリソースを解放する (アプリをメモリから削除する) しかないため、このような状態遷移になります。

延長実行中のリソース使用状況
図 6 延長実行中のリソース使用状況

バックグラウンド タスク

アプリをバックグラウンドで実行するもう 1 つの方法は、バックグラウンド タスクにする方法です。バックグラウンド タスクは、アプリ内で独立したコンポーネントで、IBackgroundTask インターフェイスを実装します。バックグラウンド タスクは負荷が高い UI フレームワークなしで実行され、一般に独立したプロセスで実行されます (ただし、主要アプリ実行可能ファイルを使ってインプロセスで実行することもできます)。

バックグラウンド タスクは、タスクに関連付けられたトリガーが発生すると実行されます。トリガーとは、アプリが実行中でなくても発生し、アプリをアクティブにできるシステム イベントです。たとえば、特定の時間間隔 (30 分おきなど) で生成される TimeTrigger があります。このイベントでは、トリガーが発生する 30 分おきに、アプリのバックグラウンド タスクがアクティブになります。Windows では、TimeTrigger、PushNotificationTrigger、LocationTrigger、ContactStoreNotificationTrigger、BluetoothLEAdvertisementWatcherTrigger、UserPresent, InternetAvailable、PowerStateChange など、多くの種類のトリガーがサポートされます。

バックグラウンド タスクは 3 つの手順で使用します。まず、コンポーネントを作成し、次にアプリのマニフェストで宣言して、最後は実行時に登録します。

バックグラウンド タスクは、通常、UI プロジェクトとは別の Windows Runtime (WinRT) コンポーネント プロジェクトとして、同じソリューション内に実装します。その結果、バックグラウンド タスクを別のプロセス内でアクティブにできるため、コンポーネントに必要なメモリ オーバーヘッドが低減されます。IBackgroundTask の簡単な実装を図 7 に示します。IBackgroundTask は、Run メソッドが 1 つだけ定義されるシンプルなインターフェイスです。このメソッドは、バックグラウンド タスクのトリガーが発生すると、呼び出されます。メソッド唯一のパラメーターが、IBackgroundTaskInstance オブジェクト です。このオブジェクトは、アクティブ化に関するコンテキスト (関連付けられるプッシュ通知のペイロードやトースト通知で使用するアクションなど) とライフサイクル イベントを処理するイベント ハンドラー (キャンセルなど) を保持します。Run メソッドが完了すると、バックグラウンド タスクが終了します。このため、前述の中断ハンドラーとまったく同様に、コードを非同期にする場合には deferral オブジェクト (と IBackgroundTaskInstance) を使用することが重要になります。

図 7 BackgroundTask の実装

public sealed class TimerTask : IBackgroundTask
{
  public void Run(IBackgroundTaskInstance taskInstance)
  {
    var deferral = taskInstance.GetDeferral();
    taskInstance.Canceled += TaskInstance_Canceled;
    await ShowToastAsync("Hello from Background Task");
    deferral.Complete();
  }
  private void TaskInstance_Canceled(IBackgroundTaskInstance sender,
    BackgroundTaskCancellationReason reason)
  {
    // Handle cancellation
    deferral.Complete();
  }
}

アプリのマニフェストで、このバックグラウンド タスクを登録することも必要です。この登録によって、タスクのトリガーの種類、エントリ ポイント、および実行可能なホストを Windows に指示します。

<Extensions>
  <Extension Category="windows.backgroundTasks" EntryPoint="BackgroundTasks.TimerTask">
    <BackgroundTasks>
      <Task Type="timer" />
    </BackgroundTasks>
  </Extension>

この登録は、XML に詳しくなくても、Visual Studioのマニフェスト デザイナーを使用して実行できます。

最後に、タスクを実行時登録します (図 8 参照)。ここでは、BackgroundTaskBuilder オブジェクトを使用して、インターネットが利用可能な場合に TimeTrigger によって 30 分おきに起動されるタスクを登録しています。TimeTrigger は、タイルを更新したり、少量のデータを定期的に同期するといった一般的な操作に適したトリガーの種類です。

図 8 バックグラウンド タスクの登録

private void RegisterBackgroundTasks()
{
  BackgroundTaskBuilder builder = new BackgroundTaskBuilder();
  builder.Name = "Background Test Class";
  builder.TaskEntryPoint = "BackgroundTaskLibrary.TestClass";
  IBackgroundTrigger trigger = new TimeTrigger(30, true);
  builder.SetTrigger(trigger);
  IBackgroundCondition condition =
    new SystemCondition(SystemConditionType.InternetAvailable);
  builder.AddCondition(condition);
  IBackgroundTaskRegistration task = builder.Register();
  task.Progress += new BackgroundTaskProgressEventHandler(task_Progress);
  task.Completed += new BackgroundTaskCompletedEventHandler(task_Completed);
}

バックグラウンド タスクの最大の利点が、問題点になることもあります。バックグラウンド タスクは、ユーザーがフォアグラウンドで重要な作業を行っているときや、デバイスがスタンバイ状態のときに、バックグラウンドで実行されるため、使用できるメモリ量や CPU 時間が厳しく制限されます。たとえば、図 8 で登録したバックグラウンド タスクは 30 秒間しか実行されず、512MB のメモリを搭載するデバイスでは 16MB までしかメモリを利用できません (メモリの上限は、デバイスに搭載されるメモリ容量によって変わります)。バックグラウンド タスクを開発して実装する場合、こうした制限事項を考慮することが重要です。アプリを公開する前に、バックグラウンド タスクをさまざまなデバイス (特にローエンド デバイス) でテストするようにします。次の点に注意が必要です。

  • バッテリー節約機能が利用可能でアクティブになっている場合 (通常は、バッテリーが所定の充電しきい値を下回っている場合)、バッテリーが再び充電されてバッテリー節約機能のしきい値を超えるまで、バックグラウンド タスクは実行されません。
  • 以前のバージョンの Windows では、バックグラウンドでのアプリの実行を許可する前にロックするため、アプリを "ピン留め" する必要がありました。場合によっては、デバイス全体で登録できるバックグラウンド タスクの最大数が決まっていることもありました。Windows 10 ではこの問題は解消されていますが、アプリでは常に BackgroundExecutionManger.RequestAcessAsync を呼び出して、バックグラウンドでの実行を宣言する必要があります。

同期よりも優れた方法

延長実行またはバックグラウンド タスクのどちらかを使用して完了できるバックグラウンド操作はたくさんあります。一般的には、可能であれば、より信頼性が高く、効率的なバックグラウンド タスクを使用することをお勧めします。

たとえば、アプリの初回起動時にデータを同期するというシナリオは、Windows 10 の新機能のアプリケーション トリガーを使用するバックグラウンド タスクによって、効率が向上します。

アプリケーション トリガー (DeviceUseTrigger など) は、アプリのフォアグラウンド部分から直接トリガーされる特別なクラスのトリガーに属しています。これは、トリガー オブジェクトの RequestAsync を明示的に呼び出すことで実行します。アプリがもはやフォアグラウンドに存在しない場合はバックグラウンドで続行すべき操作で、かつその操作が必ずしもフォアグラウンドに密接に結び付けられていない場合に、その操作をフォアグラウンドで開始したい状況には、アプリケーション トリガーが特に便利です。次のコード例は、ほとんどのシナリオで延長実行の代わりに使用できる ApplicationTrigger タスクです。

var appTrigger = new ApplicationTrigger();
var backgroundTaskBuilder = new BackgroundTaskBuilder();
backgroundTaskBuilder.Name = "App Task";
backgroundTaskBuilder.TaskEntryPoint = typeof(AppTask).FullName;
backgroundTaskBuilder.SetTrigger(appTrigger);
backgroundTaskBuilder.Register();
var result = await appTrigger.RequestAsync();

まとめ

今回は、Windows 10 のアプリ ライフサイクルとバックグラウンド実行の概要を示し、アプリのバックグラウンド実行に利用できる、延長実行とバックグラウンド タスクという 2 つの新しいメカニズムを紹介しました。バックグラウンド タスクはメモリ容量に制限があるローエンド デバイス (スマートフォンなど) に適しており、延長実行はハイエンド デバイス (デスクトップ PC など) に適しています。


Shawn Henryは、ユニバーサル Windows プラットフォーム チームの上級プログラム マネージャーです。Twitter は @shawnhenry (英語) からフォローきます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Anis Mohammed Khaja Mohideen および Abdul Hadi Sheikh に心より感謝いたします。
Abdul Hadi Sheikh はユニバーサル Windows プラットフォーム チームのソフトウェア開発者です。

Anis Mohammed Khaja Mohideen はユニバーサル Windows プラットフォーム チームの上級ソフトウェア開発者です。