應用程式向網路發出的要求會開啟耗電的行動數據或 Wi-Fi 無線電,因此是造成電池耗電的主要原因。除了傳送及接收封包所需的電力外,這些無線電還會耗用額外電力來開啟及保持運作。如果每 15 秒發出一次網路要求,行動無線電就會持續運作,快速耗盡電池電量。
定期更新一般分為三種類型:
- 使用者啟動:根據某些使用者行為執行更新,例如下拉重新整理手勢。
- 應用程式發起。定期更新。
- 伺服器啟動。根據伺服器通知執行更新。
本主題將逐一探討這些因素,並說明如何進一步最佳化,以減少耗電量。
針對使用者起始的要求進行最佳化
使用者發起的要求通常是因應某些使用者行為而產生。舉例來說,使用者可能在閱讀最新新聞報導的應用程式中,執行「下拉更新」手勢來查看是否有新文章。您可以運用下列技術回應使用者發出的要求,同時盡量減少網路用量。
調節使用者要求數
如果不需要某些使用者啟動的要求,您可以忽略這些要求。舉例來說,如果目前資料仍為最新狀態,使用者在短時間內多次執行「下拉更新」手勢來檢查新資料,您就可以忽略這些要求。如果對每項要求都採取行動,可能會讓無線電保持喚醒狀態,進而浪費大量電力。更有效率的做法是節流使用者發出的要求,在一段時間內只允許發出一個要求,減少無線電的使用頻率。
使用快取
快取應用程式資料時,您會建立應用程式需要參照的資訊本機副本。這樣一來,應用程式就能多次存取相同的資訊本機副本,而不必開啟網路連線來提出新要求。
您應盡可能積極地快取資料,包括靜態資源和隨選下載項目 (例如全尺寸圖片)。您可以使用 HTTP 快取標頭,確保快取策略不會導致應用程式顯示過時資料。如要進一步瞭解如何快取網路回應,請參閱「避免多餘的下載作業」。
在 Android 11 以上版本中,您的應用程式可以與其他應用程式共用大型資料集,用於機器學習和媒體播放等用途。如果應用程式需要存取共用資料集,可以先檢查是否有快取版本,再嘗試下載新副本。如要進一步瞭解共用資料集,請參閱「存取共用資料集」。
使用較大頻寬,減少下載資料的頻率
透過無線電連線時,頻寬越高,電池耗電量通常也越高,也就是說,5G 通常比 LTE 更耗電,而 LTE 又比 3G 更耗電。
也就是說,雖然基礎無線電狀態會因無線電技術而異,但一般來說,對於頻寬較高的無線電,狀態變更尾端時間對電池的相對影響較大。如要進一步瞭解尾端時間,請參閱「無線電狀態機器」。
同時,更高的頻寬代表您可以更積極地預先擷取,在相同時間內下載更多資料。或許較不直覺的是,由於尾端時間的電池成本相對較高,因此在每次傳輸工作階段中,讓無線電保持運作較長的時間,以減少更新頻率,也是更有效率的做法。
舉例來說,如果 LTE 無線電的頻寬和能源成本是 3G 的兩倍,您應該在每個工作階段下載四倍的資料量,或可能多達 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,伺服器可以通知在特定裝置上執行的應用程式有新資料可用。
相較於輪詢 (應用程式必須定期 Ping 伺服器來查詢新資料),這種事件驅動模型可讓應用程式僅在知道有資料可供下載時,才建立新連線。模型會盡量減少不必要的連線,並縮短更新應用程式內資訊時的延遲時間。
FCM 是透過持續性 TCP/IP 連線實作,這樣做可減少持續連線的數量,讓平台最佳化頻寬,並盡量減少對電池續航力的影響。