Android NDK开发详解连接性之尽量减少定期更新的影响
您的应用向网络发出的请求是耗电的主要原因,因为这些请求会打开耗电的移动网络或 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 子类,用于检索最新的新闻标题。此 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 连接实现。这样可以最大限度地减少持久性连接的数量,使平台能够优化带宽并最大限度地降低对电池续航时间的相关影响。
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2023-11-10。