定期的な更新による影響を最小限に抑える

アプリがネットワークに対して行うリクエストは、バッテリーを消費するモバイル通信や Wi-Fi をオンにするため、バッテリーを消耗する主な原因となります。これらの無線通信は、パケットの送受信に必要な電力を超えて、電源をオンにしてスリープ状態を解除するだけで余分な電力を消費します。15 秒ごとにネットワーク リクエストを送信するような単純な処理でも、モバイル無線が継続的にオンになり、バッテリー電力が急速に消費される可能性があります。

定期的な更新には、一般的に次の 3 種類があります。

  • ユーザー主導。スワイプで更新する操作など、ユーザーの操作に基づいて更新を行う。
  • アプリが開始。定期的に更新を行う。
  • サーバー開始。サーバーからの通知に応じて更新を実行する。

このトピックでは、これらの各要素について説明し、バッテリーの消耗を抑えるために最適化する方法について説明します。

ユーザーが開始したリクエストを最適化する

ユーザーが開始するリクエストは、通常、ユーザーの操作に応じて発生します。たとえば、最新のニュース記事を読むために使用するアプリでは、ユーザーが下にスワイプして更新する操作を行うことで、新しい記事を確認できるようにします。ネットワークの使用を最適化しながら、ユーザーが開始したリクエストに応答するには、次の手法を使用できます。

ユーザー リクエストをスロットリングする

ユーザーが開始したリクエストの中には、無視しても問題ないものもあります。たとえば、現在のデータがまだ最新であるにもかかわらず、新しいデータを確認するために短時間で複数回スワイプして更新する操作などです。各リクエストを処理すると、無線が起動したままになり、大量の電力が浪費される可能性があります。より効率的なアプローチは、ユーザーが開始したリクエストをスロットリングして、一定期間に 1 つのリクエストのみを送信できるようにし、無線通信の使用頻度を減らすことです。

キャッシュを使用する

アプリのデータをキャッシュに保存すると、アプリが参照する必要がある情報のローカルコピーが作成されます。これにより、アプリは、ネットワーク接続を開いて新しいリクエストを送信することなく、同じローカルコピーの情報に複数回アクセスできます。

静的リソースや、フルサイズの画像などのオンデマンド ダウンロードを含め、できる限り積極的にデータをキャッシュに保存する必要があります。HTTP キャッシュ ヘッダーを使用すると、キャッシュ戦略によってアプリに古いデータが表示されるのを防ぐことができます。ネットワーク レスポンスのキャッシュ保存の詳細については、冗長なダウンロードを回避するをご覧ください。

Android 11 以降では、アプリは、他のアプリが機械学習やメディア再生などのユースケースで使用するのと同じ大規模なデータセットを使用できます。アプリが共有データセットにアクセスする必要がある場合、新しいコピーをダウンロードする前に、まずキャッシュに保存されたバージョンを確認できます。共有データセットの詳細については、共有データセットにアクセスするをご覧ください。

より広い帯域幅を使用して、より多くのデータをより少ない頻度でダウンロードする

無線接続の場合、一般に帯域幅が広いほどバッテリー消費量が多くなります。つまり、5G は LTE よりも消費電力が多く、LTE は 3G よりも消費電力が多くなります。

つまり、基盤となる無線状態は無線技術によって異なりますが、一般的に、状態変化のテールタイムがバッテリーに与える相対的な影響は、帯域幅の広い無線ほど大きくなります。テールタイムの詳細については、無線ステートマシンをご覧ください。

同時に、帯域幅が広いほど、より積極的にプリフェッチして、同じ時間により多くのデータをダウンロードできます。直感的にはわかりにくいかもしれませんが、テールタイムのバッテリー消費は比較的大きいため、各転送セッション中に無線を長時間アクティブにして、更新の頻度を減らすことも効率的です。

たとえば、LTE 無線の帯域幅とエネルギー コストが 3G の 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 接続を使用して実装されます。これにより、永続的な接続の数が最小限に抑えられ、プラットフォームは帯域幅を最適化し、バッテリー駆動時間への影響を最小限に抑えることができます。