アプリからネットワークへのリクエストは、電力を消費するモバイル無線または Wi-Fi 無線がオンになるため、バッテリー消耗の主要な原因となります。これらの無線通信は、パケットの送受信に必要な電力以外にも、電源をオンにしてウェイクアップを続けるだけで電力を消費します。15 秒ごとにネットワーク リクエストを行うだけで、モバイル無線を常時オンにして電池の消耗を早めます。
定期更新には主に 3 つのタイプがあります。
- ユーザー開始。下にスワイプして更新する操作など、ユーザーの行動に基づいて更新を実行する。
- アプリ開始。更新を定期的に実行する。
- サーバー開始。サーバーからの通知に応答して更新を実行する。
このトピックでは、それぞれについて説明し、バッテリーの消耗を抑えるために最適化できるその他の方法について説明します。
ユーザーが開始したリクエストを最適化する
ユーザーが開始するリクエストは通常、なんらかのユーザーの行動に応じて発生します。たとえば、最新のニュース記事を読むために使用するアプリで、ユーザーが「下にスワイプして更新」のジェスチャーを実行して新しい記事をチェックできる場合があります。次の手法を使用すると、ネットワークの使用を最適化しながら、ユーザーが開始したリクエストに応答できます。
ユーザー リクエストのスロットリング
ユーザーが開始したリクエストのうち、現在のデータが最新でない間に新しいデータを確認するために短時間のうちに pull して更新する操作を複数実行するなど、不要なものは無視することをおすすめします。リクエストごとに動作させると、無線通信のウェイクアップ状態を維持することで、大量の電力が浪費される可能性があります。より効率的なアプローチは、ユーザーが開始したリクエストをスロットリングして、一定期間に 1 つのリクエストだけを実行し、ラジオの使用頻度を減らすことです。
キャッシュを使用する
アプリのデータをキャッシュに保存すると、アプリが参照する必要がある情報のローカルコピーが作成されます。これにより、アプリはネットワーク接続を開いて新しいリクエストを行うことなく、同じローカルコピーに複数回アクセスできます。
静的リソースや、フルサイズの画像などのオンデマンド ダウンロードなど、データを可能な限り積極的にキャッシュに保存する必要があります。HTTP キャッシュ ヘッダーを使用すると、キャッシュ戦略によってアプリに古いデータが表示されないようにできます。ネットワーク レスポンスのキャッシュ保存の詳細については、冗長なダウンロードを避けるをご覧ください。
Android 11 以降では、他のアプリが ML やメディア再生などのユースケースに使用するのと同じ大規模なデータセットをアプリで使用できます。アプリが共有データセットにアクセスする必要がある場合、新しいコピーをダウンロードする前に、まずキャッシュに保存されたバージョンの有無を確認できます。共有データセットの詳細については、共有データセットへのアクセスをご覧ください。
より広い帯域幅を使用して、より多くのデータをより少ない頻度でダウンロードする
無線通信で接続する場合、一般に帯域幅が広いとバッテリー コストが高くなります。つまり、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 接続を使用して実装されます。これにより、永続的な接続の数を最小限に抑え、プラットフォームが帯域幅を最適化し、バッテリー駆動時間に対する影響を最小限に抑えることができます。