應用程式對網路發出的要求會開啟耗用大量電力的行動網路或 Wi-Fi 無線電,因此是造成電池耗電的主要原因。除了收發封包所需的電力以外,這些無線電會消耗額外電力,只開啟和保持喚醒狀態。例如每隔 15 秒發出一次網路要求,就能讓行動無線電持續開啟,並快速消耗電池電量。
一般更新分為三種類型:
- 使用者啟動。根據部分使用者行為 (例如下拉即可重新整理手勢) 執行更新。
- 應用程式啟動。定期更新。
- 伺服器啟動。執行更新以回應伺服器通知。
本主題會逐一介紹這些單元,並討論更多方法可以降低電池耗電量。
針對使用者發出的要求進行最佳化
使用者發起的要求通常發生於回應某些使用者行為。舉例來說,用來閱讀最新文章的應用程式可能會允許使用者執行下拉即可重新整理手勢以檢查是否有新文章。您可以運用下列技巧回應使用者發出的要求,同時最佳化網路用量。
限制使用者要求
您可能會想要忽略某些由使用者啟動的要求,例如:如果某些要求並不需要,例如在短時間內使用多個下拉即可重新整理手勢,在現有資料仍為最新資料時檢查新資料。藉由回應每個要求,讓無線電保持喚醒狀態,可能會浪費大量電力。有一種更有效率的方法是調節使用者發出的要求,以便在一段時間內只能提出一項要求,進而減少使用無線電的頻率。
使用快取
藉由快取應用程式資料,您會建立本機應用程式需要參照的資訊副本。如此一來,您的應用程式就可以多次存取相同的本機副本,而不必開啟網路連線來提出新的要求。
您應盡可能積極快取資料,包括靜態資源和隨選下載 (例如完整大小的圖片)。您可以使用 HTTP 快取標頭,確保快取策略不會導致應用程式顯示過時的資料。如要進一步瞭解如何快取網路回應,請參閱「避免多餘的下載內容」。
在 Android 11 以上版本中,應用程式可以使用其他應用程式使用的大型資料集,例如機器學習和媒體播放。如果應用程式需要存取共用資料集,可在嘗試下載新副本前,先檢查快取版本。如要進一步瞭解共用資料集,請參閱「存取共用資料集」一文。
使用較高的頻寬降低下載頻率
使用無線無線電連線時,如果使用無線無線電,頻寬較高的費用通常會較高,這表示 5G 耗電量通常會比 LTE 要多,因此也比 3G 來得昂貴。
這表示基礎無線電狀態會因無線電技術而異,但一般來說,在較高的頻寬無線電技術中,說明狀態變更尾時間的相對電池影響會較高。如要進一步瞭解尾時間,請參閱「無線電狀態機器」。
但由於頻寬較高,表示您可以更積極預先擷取,同時下載更多資料。也許比較不符合直覺,因為尾部電池成本相對較高,為了減少更新頻率,在每次傳輸工作階段中保持無線電的長時間使用效率也較高。
舉例來說,如果 LTE 無線電有雙倍頻寬,使 3G 的能源成本增加一倍,則應在每個工作階段期間下載四倍的資料,或可能為 10 MB 的 10 MB 資料。下載如此大量資料時,請務必考量預先擷取功能對可用本機儲存空間的影響,並定期清除預先擷取快取。
您可以使用 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 雲端通訊傳送伺服器更新
Firebase 雲端通訊 (FCM) 是一種輕量級機制,可將伺服器的資料傳送至特定應用程式執行個體。使用 FCM 時,伺服器可通知在特定裝置上執行的應用程式有新資料可用。
與輪詢相比,應用程式必須定期連線偵測伺服器才能查詢新資料,而此事件導向模式可讓應用程式在已知有可下載的資料時,才建立新的連線。這個模型能盡量減少不必要的連線,並減少更新應用程式內資訊時的延遲時間。
FCM 是透過永久 TCP/IP 連線實作。這樣做可以盡量減少持續連線的數量,並讓平台最佳化頻寬,並將對電池續航力的影響降到最低。