ダウンロードを最適化して効率的なネットワーク アクセスを実現する

無線通信を使用したデータ転送は、アプリのバッテリー消費に大きな影響を及ぼす要因の 1 つです。ネットワーク アクティビティに関連する電池の消耗を最小限に抑えるには、基盤となる無線通信ハードウェアに対して接続モデルがどのような影響を及ぼすかを理解することが重要です。

このレッスンでは、無線通信のステートマシンを紹介し、アプリの接続モデルと無線通信のステートマシンの相互作用について説明します。さらに、データ接続を最小限に抑える方法、プリフェッチを使用する方法、一括転送によってデータ転送に伴う電池の消耗を最小限に抑える方法を紹介します。

無線通信のステートマシン

完全にアクティブな無線通信は大量の電力を消費するため、通信を行っていないときには電力を節約するために別のエネルギー状態に遷移します。その一方で、無線出力を上げることによって生じる遅延を必要に応じて最小限に抑えようとします。

一般的な 3G ネットワークの無線通信のステートマシンは、以下の 3 つのエネルギー状態で構成されます。

  1. 最大電力: 接続がアクティブなときに使用され、デバイスは最大転送率でデータを転送できます。
  2. 低電力: 電池の最大電力の約 50% を使用する中間の状態。
  3. スタンバイ: ネットワーク接続がアクティブでない、または不要なときの最小のエネルギー状態。

低電力とスタンバイの状態では電池の消耗がかなり少なくなりますが、ネットワーク リクエストに対する遅延が非常に大きくなります。低電力から最大電力の状態に戻るのには約 1.5 秒かかるのに対し、スタンバイから最大電力の状態に遷移するのには 2 秒以上かかることもあります。

遅延を最小限に抑えるには、ステートマシンで低電力のエネルギー状態への遷移を先送りにします。図 1 では、AT&T が測定した一般的な 3G 無線通信のタイミングを使用しています。

図 1: 一般的な 3G 無線通信ステートマシン

各デバイスの無線通信ステートマシン(特に、付随する遷移時遅延(「テールタイム」)と起動時遅延)は、使用されている無線通信テクノロジー(2G、3G、LTE など)によって異なり、デバイスが利用している携帯通信会社ネットワークによって定義と設定が行われています。

このレッスンでは、AT&T 提供のデータに基づき、一般的な 3G 無線通信の代表的なステートマシンについて説明します。説明対象は 3G ですが、一般原則やベスト プラクティスは、すべての無線通信実装に当てはまります。

このアプローチは一般的なウェブ ブラウジングに特に効果的で、ウェブ ブラウジング中に発生する望ましくない遅延を防ぐことができます。また、テールタイムが比較的短いため、ブラウジング セッションが終了次第、無線通信は低電力の状態に遷移できます。

残念ながらこのアプローチでは、アプリがフォアグラウンド(遅延が目立つ)とバックグラウンド(電池寿命を優先する必要がある)の両方で実行される Android のような最新のスマートフォン向け OS では、アプリの効率が低下する可能性があります。

アプリが無線通信のステートマシンに及ぼす影響

新しいネットワーク接続を確立するたびに、無線通信は最大電力の状態に遷移します。上記の一般的な 3G 無線通信のステートマシンでは、転送中(およびテールタイムの追加の 5 秒)は最大電力の状態が維持され、その後の 12 秒間は低電力の状態になります。このように、一般的な 3G デバイスでは、すべてのデータ転送セッションにおいて約 20 秒間、無線通信によって電力が消費されます。

実際面では、たとえば、バンドルされていないデータを 18 秒ごとに 1 秒間転送するアプリでは、無線通信が絶えずアクティブな状態に維持され、スタンバイの状態になろうとするとすぐに最大電力の状態に戻ります。その結果、1 分のうち 18 秒間を最大電力の状態で、残りの 42 秒間を低電力の状態で電力を消費します。

これに対し、バンドルされたデータを 1 分ごとに 3 秒間転送する同じアプリでは、無線通信が最大電力の状態に維持されるのは 1 分のうち 8 秒間のみ、低電力の状態に維持されるのは 12 秒間のみになります。

2 つ目の例では、無線通信が毎分 40 秒間スタンバイ状態になるため、バッテリーの消耗を大幅に抑えることができます。

図 2: 無線通信における一括転送と非一括転送の電力消費量の比較

データのプリフェッチ

データをプリフェッチすると、独立したデータ転送セッションの回数を効果的に減らすことができます。また、特定の期間に必要になる可能性があるすべてのデータを、1 つの接続を最大限に利用して、一気にダウンロードできます。

転送を前倒しで行うことで、データのダウンロードの際に必要になる無線通信の有効化の回数を減らすことができます。その結果、電池を節約できるだけでなく、遅延の改善、必要な帯域幅の削減、ダウンロード時間の短縮も実現できます。

また、プリフェッチにより、操作を実行したりデータを表示したりする前にダウンロードが完了するのを待機することで生じるアプリ内遅延を最小限に抑え、ユーザー エクスペリエンスを向上させることができます。

ただし、プリフェッチを積極的に使用しすぎると、使用しないデータをダウンロードすることで、電池の消耗が激しくなり、帯域幅の使用量(およびダウンロード容量)が増加するリスクが生じます。また、プリフェッチが完了するのをアプリが待機している間、プリフェッチによってアプリの起動が遅れないようにすることも重要です。具体的に言うと、データを着実に処理し、あるいは連続した転送を優先して開始して、アプリの起動に必要なデータを最初にダウンロードして処理するようにします。

どれだけ積極的にプリフェッチするかは、ダウンロードするデータのサイズと、ダウンロードしたデータが使用される可能性によって異なります。上記のステートマシンに基づくおおよその目安として、現在のユーザー セッションで使用される可能性が 50% のデータの場合、通常は、使用しないデータのダウンロードにかかる潜在的なコストがそのデータを最初にダウンロードしないことによる潜在的な節約コストと一致するまでの約 6 秒の間(約 1~2 MB)、プリフェッチを実行できます。

一般に、2~5 分ごとに 1~5 メガバイトのオーダーで新たにダウンロードを開始するだけで済むようにデータをプリフェッチすることをおすすめします。

大規模なダウンロード(動画ファイルなど)の場合、この原則に沿って定期的(2~5 分ごと)に分割してダウンロードを行うことで、数分以内に再生される可能性のある動画データだけを効果的にプリフェッチするようにしてください。

次のセクションの転送と接続をまとめて処理するで説明するように、ダウンロードの回数が多い場合は、まとめて行うようにしてください。また、接続タイプに基づいてダウンロード パターンを変更するで説明するように、ここで示した概算値は接続のタイプや速度に応じて変化します。

実際の例をいくつか見てみましょう。

音楽プレーヤー

アルバム全体をプリフェッチすることもできますが、ユーザーが最初の曲しか聞かなかった場合、かなりの量の帯域幅と電力を無駄にしたことになります。

おすすめの方法は、再生中の曲ともう 1 曲をバッファする方法です。音楽のストリーミングでは、無線通信を常にアクティブな状態に保つ連続的なストリームを維持するのではなく、HTTP Live Streaming を使用して音声ストリームをバースト状に送信して、上記のプリフェッチのアプローチをシミュレートすることを検討してください。

ニュース リーダー

多くのニュースアプリでは、カテゴリが選択された後にのみヘッドラインをダウンロードし、ユーザーが記事を読む場合にのみ記事全文をダウンロードし、ユーザーがビューにスクロールしたときにのみサムネイルをダウンロードすることによって帯域幅を削減しようとします。

このアプローチでは、ユーザーがヘッドラインをスクロールし、カテゴリを変更して、それから記事を読むと、そのセッションのほとんどの間、無線通信が強制的にアクティブな状態に維持されます。それだけでなく、エネルギー状態の切り替えが絶えず行われるため、カテゴリを変更するときや記事を読むときに大幅な遅延が生じます。

おすすめの方法は、アプリの起動時に適量のデータをプリフェッチする方法です。まず、ニュースのヘッドラインとサムネイルの最初のセットをダウンロードし(これにより、起動時の遅延を軽減できます)、その後で、残りのヘッドラインとサムネイル、および少なくともメインのヘッドライン リストから読むことができる各記事のテキストをダウンロードします。

もう 1 つのおすすめの方法は、すべてのヘッドライン、サムネイル、記事のテキストに加え、できれば記事の画像もすべてプリフェッチしてしまう方法です。これは通常、あらかじめ決められたスケジュールで、バックグラウンドで行います。ただし、この方法には、使用されることのないコンテンツをダウンロードすることで大量の帯域幅とバッテリー寿命を消費するリスクがあるため、慎重に実装する必要があります。

解決策の 1 つは、Wi-Fi 接続のときに限り、そして可能であればデバイスの充電中に限り、フル ダウンロードを行うようにスケジュールを設定する方法です。詳しくは、接続タイプに基づいてダウンロード パターンを変更するをご覧ください。

転送と接続をまとめて処理する

一般的な 3G 無線通信では、接続を開始するたびに、実行されるデータ転送のサイズに関係なく、電力が約 20 秒間消費される可能性があります。

アプリが実行中でユーザーに表示されていることを通知するためだけに 20 秒ごとにサーバーに ping を送信するアプリでは、無線通信がいつまでもアクティブなままになり、その結果、実際にはデータ転送がほとんど行われないにもかかわらず、電力が大量に消費されます。

この点を考慮して、データ転送をまとめて行い、保留中の転送キューを作成することが重要です。適切に行えば、ほぼ同じ期間内に行われることになっているデータ転送のフェーズシフトを効果的に行うことができ、すべてのデータ転送を同時に行えるようになります。これにより、無線通信が電力を消費する期間をできる限り短くすることができます。

この方法の基本的な考え方は、各転送セッションにおいてできる限り多くのデータを転送し、必要なセッション数を制限することにあります。

つまり、一定の時間内に転送を行う必要がある場合でもすべて実行されるよう、遅延が許容される転送はキューに登録し、スケジュールされているアップデートとプリフェッチを優先することで、転送をまとめて行う必要があります。同様に、スケジュールに基づくアップデートと定期的なプリフェッチの際に、保留中の転送キューの実行を開始する必要があります。

実際の例を見るために、データをプリフェッチするの例に戻りましょう。

上記のプリフェッチ ルーティンを使用するニュースアプリを見てみましょう。ニュース リーダーは、ユーザーが記事を読むパターンを把握したり、人気記事のランキングを作成したりするために、分析情報を収集します。ニュースの鮮度を保つために、1 時間ごとに最新情報をチェックします。また、帯域幅を節約するために、記事ごとにすべての写真をダウンロードするのではなく、サムネイルのみをプリフェッチして、そのサムネイルが選択されたときにすべての写真をダウンロードします。

この例では、アプリ内で収集されたすべての分析情報が、収集と同時に送信されるのではなく、まとめられてダウンロード用のキューに登録されます。まとめられた情報は、フルサイズの写真のダウンロード時または 1 時間ごとの更新の実行時に転送されます。

時間的制約のある転送やオンデマンド転送(フルサイズの画像のダウンロードなど)は、定期的なアップデートより優先する必要があります。また、計画されているアップデートはオンデマンド転送と同時に実行し、次回のアップデートが所定の時間後に行われるようスケジュールする必要があります。この方法では、一定の時間内に行う必要がある写真のダウンロードに便乗することで、定期的なアップデート実行の負担を軽減できます。

接続回数を減らす

一般に、新しいネットワーク接続を開始するよりも既存のネットワーク接続を再利用する方が効率的です。また、接続を再利用すると、輻輳やそれに関連するネットワーク データの問題に対してネットワークがよりインテリジェントに対応できます。

データをダウンロードするために複数の同時接続を作成したり、複数の連続した GET リクエストをチェーン化したりする代わりに、可能であれば、複数のリクエストを 1 つの GET リクエストにまとめるようにしてください。

たとえば、複数のニュース カテゴリに対して複数のクエリを作成するよりも、すべてのニュース記事を対象とする単一のリクエストを作成して、単一のリクエスト / レスポンスで返されるようにした方が効率的です。また、サーバーやクライアントのタイムアウトに伴う終了 / 終了肯定応答パケットを送信するためには、無線通信がアクティブになっている必要があるため、タイムアウトを待つのではなく、接続が使用されなくなったときに接続を終了することをおすすめします。

ただし、接続の終了が早すぎると、接続を再利用できなくなることがあります。その場合、新しい接続を確立するために追加のオーバーヘッドが必要になります。妥協案として、接続をすぐに終了するのではなく、今までどおり、本来のタイムアウト時間になる前に接続を終了することをおすすめします。

ネットワーク プロファイラで問題を特定する

アプリがネットワーク リクエストを作成するタイミングを追跡するには、ネットワーク プロファイラを使用します。アプリがデータを転送する方法とタイミングを監視して、基盤となるコードを適切に最適化できます。

図 3 は、少量のデータを約 15 秒間隔で転送するパターンを示しています。各リクエストをプリフェッチするか、アップロードをまとめることによって、大幅に効率を向上できることが示されています。

図 3: ネットワーク使用状況のトラッキング

データ転送の頻度と、各接続で転送されるデータの量をモニタリングすることで、アプリのどの部分を修正すればバッテリー効率を向上できるか判断できるようになります。一般に、短期的な急増をそれぞれ検証して、遅らせることが可能なケースや、後の転送を先に実行した方が良いケースを見つけ出します。

転送が急増する原因を正確に特定するには、Traffic Stats API を使用します。この API の TrafficStats.setThreadStatsTag() メソッドを使用すると、スレッド内で発生するデータ転送にタグを付けることができます。また、続けて TrafficStats.tagSocket()TrafficStats.untagSocket() を使用することで、個々のソケットに対して手動でタグの付け外しを行うことができます。たとえば、次のようになります。

Kotlin

    TrafficStats.setThreadStatsTag(0xF00D)
    TrafficStats.tagSocket(outputSocket)
    // Transfer data using socket
    TrafficStats.untagSocket(outputSocket)
    

Java

    TrafficStats.setThreadStatsTag(0xF00D);
    TrafficStats.tagSocket(outputSocket);
    // Transfer data using socket
    TrafficStats.untagSocket(outputSocket);
    

HttpURLConnection ライブラリは、現在の TrafficStats.getThreadStatsTag() 値に基づいて、自動的にソケットにタグを付けます。また、このライブラリは、キープアライブ プールを通じてリサイクルする際も、ソケットに対してタグの付け外しを行います。

Kotlin

    private class IdentifyTransferSpikeTask : AsyncTask<String, Nothing, String>() {

        override fun onPreExecute() = TrafficStats.setThreadStatsTag(0xF00D)

        override fun doInBackground(vararg urls: String): String {
            try {
                // Make network request using HttpURLConnection.connect()
            }
        }

        override fun onPostExecute(result: String) = TrafficStats.clearThreadStatsTag()
    }
    

Java

    private class IdentifyTransferSpikeTask extends AsyncTask<String, Void, String> {
        @Override
        protected void onPreExecute() {
          TrafficStats.setThreadStatsTag(0xF00D);
        }

        @Override
        protected String doInBackground(String... urls) {
            try {
                // Make network request using HttpURLConnection.connect()
            }
        }

        @Override
        protected void onPostExecute(String result) {
            TrafficStats.clearThreadStatsTag();
       }
    }
    

ソケットのタグ付けは Android 4.0 でサポートされていますが、リアルタイム統計情報が表示されるのは Android 4.0.3 以降を搭載しているデバイス上に限られます。