將定期更新的影響降至最低

應用程式對網路發出的要求是造成耗電的主要原因,因為這些要求已開啟耗電的行動通訊或 Wi-Fi 無線電。除了收發封包所需的電力外,這些無線電還有開啟並保持喚醒所需的額外電力。就算是每隔 15 秒發出網路要求,行動無線電也能持續開啟,並快速消耗電池電量。

定期更新分為三種一般類型:

  • 使用者啟動。根據部分使用者的行為執行更新,例如下拉重新整理手勢。
  • 應用程式啟動。定期執行更新。
  • 伺服器啟動。進行更新以回應來自伺服器的通知。

本主題會逐一說明這些因素,並討論降低電池耗電量的其他最佳化方法。

針對使用者提出的要求進行最佳化

使用者提出的要求通常是為了回應某些使用者行為。舉例來說,用於閱讀最新新聞報導的應用程式,可以讓使用者執行下拉重新整理手勢,查看新報導。您可以使用下列技巧來回應使用者啟動的要求,同時最佳化網路用量。

限制使用者要求

如果部分使用者提出的要求並不需要,建議您忽略使用者提出的要求。舉例來說,您可以在短時間內多次提取重新整理手勢,藉此在目前資料仍時更新新資料。每次處理要求可能會浪費大量電力,因為無線電仍然處於喚醒狀態。更有效率的做法是調節使用者發出的要求,限制在一段時間內只能發出一項要求,進而降低無線電的使用頻率。

使用快取

透過快取應用程式的資料,就等於建立應用程式需要參考的資訊的本機副本。這樣一來,應用程式就能多次存取同一資訊的本機副本,不必開啟網路連線來發出新要求。

建議您盡量頻繁地快取資料,包括靜態資源和隨選下載 (如原尺寸圖片)。您可以使用 HTTP 快取標頭,確保快取策略不會導致應用程式顯示過時的資料。如要進一步瞭解如何快取網路回應,請參閱「避免冗餘下載」。

在 Android 11 以上版本中,應用程式可使用相同的大型資料集,與其他應用程式用於機器學習和媒體播放等用途。當應用程式需要存取共用資料集時,可以先檢查快取版本,然後再嘗試下載新的副本。如要進一步瞭解共用資料集,請參閱「存取共用資料集」。

使用較大的頻寬,減少下載數據用量的頻率

透過無線無線電時,高頻寬通常價格會較高,亦即 5G 的能源消耗通常比 LTE 更多,而 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 程式庫,以高效的排程執行工作,這個排程會考量是否符合特定條件,例如網路可用性和電源狀態。舉例來說,假設您有一個名為 DownloadHeadlinesWorkerWorker 子類別,用來擷取最新新聞標題。只要裝置連上非計量付費網路,且裝置電量不低,就可以安排此工作站每小時執行一次,並在出現任何擷取資料時使用自訂重試策略,如下所示:

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 連線實作。這樣做可將持續連線的數量降到最低,並讓平台最佳化頻寬,並將對電池續航力的相關影響降到最低。