您的应用向网络发出的请求是电池电量消耗的主要原因,因为这些请求会打开耗电的蜂窝或 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
库以有效的调度执行工作,该调度会考虑是否满足特定条件,例如网络可用性和电源状态。例如,假设您有一个名为DownloadHeadlinesWorker
的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 连接实现。这最大限度地减少了持久连接的数量,并允许平台优化带宽并最大限度地减少对电池寿命的相关影响。