您的应用向网络发出的请求是主要的耗电来源,因为需要开启高功耗的移动网络或 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
库按照高效的时间表执行工作,该时间表会考虑是否满足特定条件(例如网络可用性和电源状态)。例如,假设您有一个名为 DownloadHeadlinesWorker
的 Worker
子类,用于检索最新的新闻标题。您可以安排此 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 云消息传递 (FCM) 是一种轻量级机制,用于将数据从服务器传输到特定应用实例。借助 FCM,您的服务器可以通知您在特定设备上运行的应用有新数据可用。
与轮询(应用必须定期 ping 服务器以查询新数据)相比,这种事件驱动型模型允许应用仅在知道有数据要下载时创建新连接。该模式最大限度地减少了不必要的连接,并降低了在应用内更新信息时的延迟。
FCM 通过持久性 TCP/IP 连接实现。这样可以最大限度地减少持久连接的数量,使平台能够优化带宽并最大限度地减少对电池续航时间的影响。