最大限度减少定期更新的影响

您的应用程序向网络发出的请求是电池电量消耗的主要原因,因为它们会打开功耗高的蜂窝网络或 Wi-Fi 无线电。除了发送和接收数据包所需的电量之外,这些无线电还会消耗额外的电量来打开并保持唤醒状态。像每 15 秒执行一次网络请求这样简单的事情,会导致移动无线电持续开启,并迅速消耗电池电量。

定期更新一般有三种类型。

  • **用户发起。** 根据某些用户行为执行更新,例如下拉刷新手势。
  • **应用程序发起。** 定期执行更新。
  • **服务器发起。** 响应来自服务器的通知执行更新。

本主题将介绍这三种类型,并讨论其他可以优化以减少电池电量消耗的方法。

优化用户发起的请求

用户发起的请求通常是响应某些用户行为而发生的。例如,用于阅读最新新闻文章的应用程序可能允许用户执行下拉刷新手势以检查是否有新文章。您可以使用以下技术来响应用户发起的请求,同时优化网络使用。

限制用户请求

如果不需要用户发起的请求,则可能需要忽略这些请求,例如在短时间内多次执行下拉刷新手势以检查是否有新数据,而当前数据仍然有效。处理每个请求可能会浪费大量的电量,因为无线电会保持唤醒状态。更有效的方法是限制用户发起的请求,以便在一段时间内只允许执行一个请求,从而减少无线电的使用频率。

使用缓存

通过缓存应用程序的数据,您将创建应用程序需要引用信息的本地副本。然后,您的应用程序可以多次访问同一本地副本的信息,而无需打开网络连接以发出新请求。

您应该尽可能积极地缓存数据,包括静态资源和按需下载的内容,例如全尺寸图像。您可以使用 HTTP 缓存标头来确保您的缓存策略不会导致应用程序显示过时数据。有关缓存网络响应的更多信息,请参阅 避免重复下载

在 Android 11 及更高版本中,您的应用可以使用与其他应用相同的庞大数据集,用于机器学习和媒体播放等用例。当您的应用需要访问共享数据集时,它可以先检查缓存版本,然后再尝试下载新副本。要了解更多有关共享数据集的信息,请参阅 访问共享数据集

使用更大的带宽更少地下载更多数据

通过无线电连接时,更高的带宽通常会以更高的电池消耗为代价,这意味着 5G 通常比 LTE 消耗更多能量,而 LTE 又比 3G 更贵。

这意味着,虽然底层无线电状态会根据无线电技术而有所不同,但总的来说,对于更高带宽的无线电,状态更改尾部时间的相对电池影响更大。有关尾部时间的更多信息,请参阅 无线电状态机

同时,更高的带宽意味着您可以更积极地预取,在相同时间内下载更多数据。也许不太直观的是,由于尾部时间的电池成本相对较高,因此在每次传输会话期间让无线电保持更长时间的活动也更有效,从而降低更新频率。

例如,如果 LTE 无线电的带宽和能耗是 3G 的两倍,那么您应该在每次会话期间下载四倍的数据——或者可能高达 10MB。在下载这么多数据时,重要的是要考虑预取对可用本地存储的影响,并定期刷新预取缓存。

您可以使用 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 之类的库来优化。

批处理网络请求

在移动设备上,打开无线电、建立连接并保持无线电唤醒的过程会消耗大量电量。因此,在随机时间处理单个请求会消耗大量电量并缩短电池寿命。更有效的方法是将一组网络请求排队并一起处理。这使系统能够仅一次支付打开无线电的电源成本,并且仍然获得应用请求的所有数据。

使用 WorkManager

您可以使用 WorkManager 库按照考虑是否满足特定条件(例如网络可用性和电源状态)的有效计划执行工作。例如,假设您有一个名为 DownloadHeadlinesWorkerWorker 子类,它检索最新的新闻标题。可以安排此工作程序每小时运行一次,前提是设备连接到非计量网络并且设备的电池电量不低,如果检索数据时出现任何问题,则使用自定义重试策略,如下所示

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);

除了 WorkManager 之外,Android 平台还提供其他一些工具来帮助您创建完成网络任务(例如轮询)的有效计划。要了解有关使用这些工具的更多信息,请参阅 后台处理指南

优化服务器发起的请求

服务器发起的请求通常是响应来自服务器的通知而发生的。例如,用于阅读最新新闻文章的应用可能会收到有关符合用户个性化偏好的新文章批次的通知,然后它会下载这些文章。

使用 Firebase Cloud Messaging 发送服务器更新

Firebase Cloud Messaging (FCM) 是一种轻量级机制,用于将数据从服务器传输到特定应用实例。使用 FCM,您的服务器可以通知在特定设备上运行的应用,有新的数据可供它使用。

与轮询相比,您的应用必须定期 ping 服务器以查询新数据,这种事件驱动模型允许您的应用仅在知道有数据要下载时才创建新连接。该模型最大限度地减少了不必要的连接,并在更新应用中的信息时减少了延迟。

FCM 使用持久 TCP/IP 连接实现。这最大限度地减少了持久连接的数量,并允许平台优化带宽并最大限度地减少对电池寿命的影响。