アプリがネットワークに対して行うリクエストは、消費電力を消費するセル無線通信や Wi-Fi 無線をオンにするため、バッテリーの消耗の主な原因となります。これらの無線通信は、パケットの送受信に必要な電力だけでなく、電源を入れてスリープ状態から維持し続けるだけで余分な電力を消費します。15 秒ごとにネットワーク リクエストを行うだけで、モバイル無線が継続的にオンになり、バッテリーの消耗が早くなります。
定期的な更新には、主に次の 3 つのタイプがあります。
- ユーザー開始型。プルして更新する操作など、ユーザーの行動に基づいて更新を実行する。
- アプリ起動。定期的な更新の実行。
- サーバー開始。サーバーからの通知に応じて更新を実行する。
このトピックでは、それぞれの要素を検討し、最適化してバッテリーの消耗を抑えるその他の方法について説明します。
ユーザーが開始したリクエストを最適化する
ユーザー開始リクエストは通常、ユーザーの行動に応じて発生します。たとえば、最新のニュース記事を読むために使用されるアプリで、ユーザーが「プルして更新する」ジェスチャーを実行して新しい記事をチェックできるようにします。次の方法を使用して、ネットワークの使用を最適化しながら、ユーザーが開始したリクエストに応答できます。
ユーザー リクエストのスロットリング
現在のデータが最新の状態である間に新しいデータをチェックするため、短時間に複数のプルして更新の操作を行うなど、ユーザーが開始したリクエストが必要ない場合は、そのリクエストを無視することもできます。各リクエストに対応すると、無線通信がウェイクアップしたままになり、大量の電力が浪費される可能性があります。より効率的なアプローチは、ユーザーが開始するリクエストをスロットリングして、一定期間内にリクエストが 1 つだけできるようにし、ラジオの使用頻度を減らすことです。
キャッシュを使用する
アプリのデータをキャッシュに保存することで、アプリが参照する必要がある情報のローカルコピーを作成できます。これにより、アプリは、新しいリクエストを行うためにネットワーク接続を開かなくても、情報の同じローカルコピーに複数回アクセスできます。
静的リソースやフルサイズ画像などのオンデマンド ダウンロードなど、データは可能な限り積極的にキャッシュに保存する必要があります。HTTP キャッシュ ヘッダーを使用すると、キャッシュ戦略に基づいてアプリに古いデータが表示されないようにできます。ネットワーク レスポンスのキャッシュ保存の詳細については、冗長ダウンロードを避けるをご覧ください。
Android 11 以降では、他のアプリが機械学習やメディア再生などのユースケースで使用するものと同じ大規模なデータセットをアプリで使用できます。アプリが共有データセットにアクセスする必要がある場合、新しいコピーをダウンロードする前に、まずキャッシュに保存されたバージョンを確認できます。共有データセットの詳細については、共有データセットにアクセスするをご覧ください。
より広い帯域幅を使用して、より多くのデータをより少ない頻度でダウンロードする
無線通信を介して接続する場合、一般に、帯域幅が大きいほどバッテリーコストも高くなります。つまり、5G は通常、LTE よりも消費エネルギーが多く、3G よりもコストが高くなります。
つまり、基礎となる無線の状態は無線技術によって異なりますが、一般に、状態変化のテールタイムがバッテリーに及ぼす影響は、高帯域幅の無線ほど大きくなります。テールタイムの詳細については、無線ステートマシンをご覧ください。
同時に、帯域幅が高いほど積極的にプリフェッチを行い、同時により多くのデータをダウンロードできるようになります。テールタイムのバッテリーのコストは比較的高いため、更新の頻度を減らすために、各転送セッション中に無線を長期間アクティブにしておく方が効率的かもしれませんが、直感的ではありません。
たとえば、LTE 無線の帯域幅が 3G の 2 倍、エネルギー コストが 2 倍である場合、各セッション中に 4 倍のデータ(場合によっては 10 MB)をダウンロードする必要があります。この大量のデータをダウンロードする場合は、使用可能なローカル ストレージに対するプリフェッチの影響を考慮し、プリフェッチ キャッシュを定期的にフラッシュすることが重要です。
ConnectivityManager
を使用してデフォルト ネットワークのリスナーを登録し、TelephonyManager
を使用して PhoneStateListener
を登録して現在のデバイス接続タイプを決定できます。接続タイプが判明したら、それに応じてプリフェッチ ルーティンを変更できます。
Kotlin
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager private var hasWifi = false private var hasCellular = false private var cellModifier: Float = 1f private val networkCallback = object : ConnectivityManager.NetworkCallback() { // Network capabilities have changed for the network override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { super.onCapabilitiesChanged(network, networkCapabilities) hasCellular = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) hasWifi = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } } private val phoneStateListener = object : PhoneStateListener() { override fun onPreciseDataConnectionStateChanged( dataConnectionState: PreciseDataConnectionState ) { cellModifier = when (dataConnectionState.networkType) { TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1/2f else -> 1f } } private class NetworkState { private var defaultNetwork: Network? = null private var defaultCapabilities: NetworkCapabilities? = null fun setDefaultNetwork(network: Network?, caps: NetworkCapabilities?) = synchronized(this) { defaultNetwork = network defaultCapabilities = caps } val isDefaultNetworkWifi get() = synchronized(this) { defaultCapabilities?.hasTransport(TRANSPORT_WIFI) ?: false } val isDefaultNetworkCellular get() = synchronized(this) { defaultCapabilities?.hasTransport(TRANSPORT_CELLULAR) ?: false } val isDefaultNetworkUnmetered get() = synchronized(this) { defaultCapabilities?.hasCapability(NET_CAPABILITY_NOT_METERED) ?: false } var cellNetworkType: Int = TelephonyManager.NETWORK_TYPE_UNKNOWN get() = synchronized(this) { field } set(t) = synchronized(this) { field = t } private val cellModifier: Float get() = synchronized(this) { when (cellNetworkType) { TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1 / 2f else -> 1f } } val prefetchCacheSize: Int get() = when { isDefaultNetworkWifi -> MAX_PREFETCH_CACHE isDefaultNetworkCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt() else -> DEFAULT_PREFETCH_CACHE } } private val networkState = NetworkState() private val networkCallback = object : ConnectivityManager.NetworkCallback() { // Network capabilities have changed for the network override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { networkState.setDefaultNetwork(network, networkCapabilities) } override fun onLost(network: Network?) { networkState.setDefaultNetwork(null, null) } } private val telephonyCallback = object : TelephonyCallback(), TelephonyCallback.PreciseDataConnectionStateListener { override fun onPreciseDataConnectionStateChanged(dataConnectionState: PreciseDataConnectionState) { networkState.cellNetworkType = dataConnectionState.networkType } } connectivityManager.registerDefaultNetworkCallback(networkCallback) telephonyManager.registerTelephonyCallback(telephonyCallback) private val prefetchCacheSize: Int get() { return when { hasWifi -> MAX_PREFETCH_CACHE hasCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt() else -> DEFAULT_PREFETCH_CACHE } } }
Java
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); private boolean hasWifi = false; private boolean hasCellular = false; private float cellModifier = 1f; private ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() { @Override public void onCapabilitiesChanged( @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities ) { super.onCapabilitiesChanged(network, networkCapabilities); hasCellular = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); hasWifi = networkCapabilities .hasTransport(NetworkCapabilities.TRANSPORT_WIFI); } }; private PhoneStateListener phoneStateListener = new PhoneStateListener() { @Override public void onPreciseDataConnectionStateChanged( @NonNull PreciseDataConnectionState dataConnectionState ) { switch (dataConnectionState.getNetworkType()) { case (TelephonyManager.NETWORK_TYPE_LTE | TelephonyManager.NETWORK_TYPE_HSPAP): cellModifier = 4; Break; case (TelephonyManager.NETWORK_TYPE_EDGE | TelephonyManager.NETWORK_TYPE_GPRS): cellModifier = 1/2.0f; Break; default: cellModifier = 1; Break; } } }; cm.registerDefaultNetworkCallback(networkCallback); tm.listen( phoneStateListener, PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE ); public int getPrefetchCacheSize() { if (hasWifi) { return MAX_PREFETCH_SIZE; } if (hasCellular) { return (int) (DEFAULT_PREFETCH_SIZE * cellModifier); } return DEFAULT_PREFETCH_SIZE; }
アプリ開始リクエストを最適化する
アプリから開始されたリクエストは、通常、バックエンド サービスにログや分析を送信するアプリなど、スケジュールに従って発生します。アプリから開始されたリクエストを処理する場合は、それらのリクエストの優先度、リクエストをまとめてバッチ処理できるかどうか、デバイスが充電中または定額制ネットワークに接続されるまで遅延できるかどうかを検討します。これらのリクエストは、慎重にスケジューリングし、WorkManager などのライブラリを使用することで最適化できます。
バッチ ネットワーク リクエスト
モバイル デバイスでは、無線をオンにして接続を確立し、無線通信をスリープ状態から維持するプロセスに大量の電力が消費されます。このため、個々のリクエストをランダムに処理すると、電力を大量に消費し、バッテリー駆動時間が短くなることがあります。一連のネットワーク リクエストをキューに入れてまとめて処理する方法は、より効率的な方法です。これにより、システムは無線通信をオンにする消費電力を 1 回だけ支払っても、アプリからリクエストされたすべてのデータを取得できます。
WorkManager を使用する
WorkManager
ライブラリを使用すると、ネットワークの可用性や電源状態など、特定の条件が満たされるかどうかを考慮した効率的なスケジュールで処理を実行できます。たとえば、最新のニュースの見出しを取得する DownloadHeadlinesWorker
という Worker
サブクラスがあるとします。デバイスが定額制ネットワークに接続されていて、デバイスのバッテリー残量が少なくなっていない場合、このワーカーは 1 時間ごとに実行されるようにスケジュールできます。また、データの取得中に問題が発生した場合は、以下に示すようにカスタムの再試行戦略が適用されます。
Kotlin
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder<DownloadHeadlinesWorker>(1, TimeUnit.HOURS) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES) .build() WorkManager.getInstance(context).enqueue(request)
Java
Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) .build(); WorkRequest request = new PeriodicWorkRequest.Builder(DownloadHeadlinesWorker.class, 1, TimeUnit.HOURS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES) .build(); WorkManager.getInstance(this).enqueue(request);
Android プラットフォームには、WorkManager の他にも、ポーリングなどのネットワーク タスクを完了するための効率的なスケジュールの作成に役立つツールが用意されています。これらのツールの使用方法について詳しくは、バックグラウンド処理ガイドをご覧ください。
サーバー開始リクエストを最適化する
サーバー開始リクエストは通常、サーバーからの通知に応じて発生します。たとえば、最新のニュース記事を読むために使用されるアプリは、ユーザーのパーソナライズ設定に一致する新しい記事のバッチに関する通知を受け取り、それをダウンロードします。
Firebase Cloud Messaging を使用してサーバーの更新情報を送信する
Firebase Cloud Messaging(FCM)は、サーバーから特定のアプリ インスタンスにデータを送信するために使用される軽量のメカニズムです。FCM を使用すると、サーバーは特定のデバイスで実行されているアプリに、利用可能な新しいデータがあることを通知できます。
新しいデータをクエリするためにアプリが定期的にサーバーに ping する必要があるポーリングとは異なり、このイベント ドリブン モデルでは、ダウンロードするデータがあることを認識した場合にのみ、アプリが新しい接続を作成できます。このモデルは、不要な接続を最小限に抑え、アプリ内の情報を更新する際のレイテンシを短縮します。
FCM は永続的な TCP/IP 接続を使用して実装されます。これにより、永続的な接続の数を最小限に抑え、プラットフォームで帯域幅を最適化し、それに伴うバッテリー駆動時間への影響を最小限に抑えることができます。