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

アプリがネットワークに対してリクエストを行うと、電力を大量に消費するモバイルデータ通信や Wi-Fi 通信がオンになるため、バッテリー消費の大きな原因となります。パケットを送受信するのに必要な電力以外にも、このような無線通信は、オンにしてウェイク状態を維持するだけで電力を消費します。15 秒ごとにネットワーク リクエストを行うだけで、モバイルデータ通信が継続的にオン状態になり、すぐにバッテリー切れになります。

定期的なアップデートには、次の 3 種類があります。

  • ユーザーが開始するプルして更新などのユーザー操作に基づいて更新を実行する。
  • アプリが開始した定期的に更新を実行する。
  • サーバー開始型。サーバーからの通知に応じて更新を実行する。

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

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

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

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

ユーザーが開始したリクエストの中には、必要がないリクエストもあります。たとえば、現在のデータがまだ最新の状態であるにもかかわらず、短時間に複数のプルトゥー リフレッシュ ジェスチャーで新しいデータを確認するリクエストなどです。リクエストごとに処理すると、無線通信がウェイク状態を維持するため、大量の電力を消費する可能性があります。より効率的なアプローチは、ユーザーが開始したリクエストをスロットリングして、一定期間に 1 つのリクエストのみを実行できるようにし、無線の使用頻度を減らすことです。

キャッシュを使用する

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

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

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

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

無線通信経由で接続する場合は、一般に、帯域幅が広いほど電池コストが高くなります。つまり、通常、5G は LTE よりも多くのエネルギーを消費し、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 では、ダウンロードするデータがあることをアプリが認識している場合に限り、新しい接続を確立できます。このモデルでは、不要な接続を最小限に抑えるとともに、アプリ内の情報を更新するまでの時間を短縮します。

FCM は永続的な TCP/IP 接続を使用して実装されます。これにより、永続的な接続の数を最小限に抑え、プラットフォームが帯域幅を最適化して電池寿命への影響を最小限に抑えることができます。