尽量减少定期更新的影响

您的应用向网络发出的请求是主要的耗电来源,因为需要开启耗电的移动网络或 WLAN 无线装置。除了发送和接收数据包需要消耗电量之外,这些无线装置还会消耗额外电量,仅仅用来开启并保持唤醒。仅仅是每 15 秒一次的网络请求就能让移动无线装置持续保持开启,并迅速耗尽电池电量。

常规更新一般分为三种类型:

  • 用户发起的。根据某些用户行为(例如下拉刷新手势)执行更新。
  • 应用发起的。定期执行更新。
  • 服务器发起的。根据服务器的通知执行更新。

本主题将逐一介绍这些因素,并讨论如何进一步优化它们以减少电池电量消耗。

优化用户发起的请求

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

限制用户请求

如果用户发起的某些请求没有必要,您可能需要忽略这些请求,例如在当前数据仍然新鲜的情况下,短时间内多次执行下拉刷新手势来检查新数据。如果对每个请求都采取行动,无线装置会一直保持唤醒状态,从而浪费大量电量。更高效的方法是限制用户发起的请求,以便在一段时间内只能发出一个请求,从而减少无线装置的使用频率。

使用缓存

通过缓存应用的数据,您可以针对应用需要引用的信息创建本地副本。这样,您的应用就可以多次访问同一本地信息副本,而无需打开网络连接来发出新请求。

您应尽可能积极地缓存数据,包括静态资源和按需下载内容(例如完整尺寸图片)。您可以使用 HTTP 缓存标头来确保缓存策略不会导致应用显示过时的数据。如需详细了解如何缓存网络响应,请参阅避免冗余下载

在 Android 11 及更高版本中,您的应用可以与其他应用共用大型数据集,为机器学习和媒体播放等用例提供支持。当您的应用需要访问共享数据集时,可以先检查是否有缓存版本,然后再尝试下载新副本。如需详细了解共享数据集,请参阅访问共享数据集

使用更高的带宽以更低的频率下载更多数据

通过无线装置连接时,带宽越高,电池成本通常越高,也就是说,5G 通常比 LTE 的耗电量多,而 LTE 比 3G 更昂贵。

这意味着,虽然底层无线装置状态因无线装置技术而异,但一般而言,无线装置的带宽越高,状态更改拖尾时间对电池续航时间造成的相对影响就越大。如需详细了解尾部时间,请参阅无线电状态机

同时,带宽越高,则意味着您可以更主动地预提取数据,从而在相同的时间下载更多数据。虽然可能不太直观,但因为拖尾时间电池成本相对较高,因此更有效的做法是在每个传输会话过程中,使无线装置长时间保持活动状态以降低更新频率。

例如,如果 LTE 无线装置的带宽和电池成本均是 3G 的两倍,那么在每个会话过程中,您应下载 4 倍的数据量,甚至可能高达 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 子类,用于检索最新的新闻标题。只要设备连接到非按流量计费的网络且电池电量充足,就可以将此 worker 安排为每小时运行一次。如果检索数据时遇到任何问题,可以采用自定义重试策略,如下所示:

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 连接实现。这样可最大限度地减少持久性连接的数量,使平台能够优化带宽并最大限度地减少对电池续航时间造成的关联影响。