背景最佳化

Stay organized with collections Save and categorize content based on your preferences.

背景處理可能會耗用大量記憶體和電量。舉例來說,隱式廣播可能會啟動多個已註冊監聽的背景處理 (即使這些處理可能無法順利執行)。這可能會對裝置效能和使用者體驗造成重大影響。

為解決這個問題,Android 7.0 (API 級別 24) 會套用下列限制:

如果您的應用程式使用上述任一意圖,建議您盡快移除這些依附元件,以便能正確指定執行 Android 7.0 以上版本的裝置。Android 架構提供數種解決方案,可降低這些對於隱式廣播的需求。舉例來說,JobScheduler 和新的 WorkManager 提供了強大的機制,可在符合指定條件 (例如與非計量付費網路之間的連線) 時,排定網路作業的排程時間。您現在也可以使用 JobScheduler,回應內容供應者做出的變更。JobInfo 物件會封裝 JobScheduler 用於排定工作的參數。只要符合工作條件,系統就會在應用程式的 JobService 上執行這項工作。

在本文件中,我們將瞭解如何使用替代方法 (例如 JobScheduler) 調整應用程式,使其符合這些新的限制。

使用者啟動限制

在系統設定中的電池用量頁面中,使用者可以選擇下列選項:

  • 無限制:允許所有背景作業(可能會耗用更多電池電力)。
  • 最佳化(預設):根據使用者與應用程式互動的方式,最佳化應用程式以執行背景作業。
  • 受限制:完全禁止應用程式在背景中執行。應用程式可能無法正常運作

如果應用程式表現出一些 Android Vitals 中描述的不良行為,系統可能會提示使用者限制該應用程式存取系統資源。

如果系統發現應用程式耗用過多資源,就會通知使用者,並讓使用者選擇限制應用程式的動作。可能觸發通知的行為包括:

  • Wake Lock 次數過多:1 個部分 Wake Lock 會在螢幕關閉時保留 1 小時
  • 背景服務過多:如果應用程式的目標 API 級別低於 26,且背景服務過多

實際採用的限制是由裝置製造商決定。舉例來說,在搭載 Android 9 (API 級別 28) 以上版本的 Android AOSP 版本中,在背景執行且處於「受限制」狀態的應用程式具有下列限制:

  • 無法啟動前景服務
  • 現有前景服務會從前景移除
  • 不會觸發鬧鐘
  • 未執行工作

此外,如果應用程式指定 Android 13 (API 級別 33) 或以上版本,且處於「受限制」狀態,在應用程式基於其他原因啟動之前,系統不會提交 BOOT_COMPLETED 廣播或 LOCKED_BOOT_COMPLETED 廣播。

如要瞭解具體限制,請參閱「電源管理限制」。

接收網路活動播送的限制

指定 Android 7.0 (API 級別 24) 的應用程式,如果註冊以在其資訊清單中接收 CONNECTIVITY_ACTION 播送,並不會收到播送,而且依附此播送的程序也不會開始。如果裝置連接至非計量付費的網路,對於想要監聽網路變更或執行大量網路活動的應用程式而言,這可能就會造成問題。Android 架構中已有數種限制可解決此限制的相關問題,不過還是請根據應用程式的需求選擇適當的解決方案。

注意事項:已向 Context.registerReceiver() 註冊的 BroadcastReceiver 在應用程式執行期間會持續接收此類播送。

針對非計量付費連線排程網路工作

使用 JobInfo.Builder 類別建構 JobInfo 物件時,套用 setRequiredNetworkType() 方法並傳遞 JobInfo.NETWORK_TYPE_UNMETERED 為工作參數。下列程式碼範例會在裝置連上非計量付費網路且充電時排程執行一項服務:

Kotlin

const val MY_BACKGROUND_JOB = 0
...
fun scheduleJob(context: Context) {
    val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
    val job = JobInfo.Builder(
            MY_BACKGROUND_JOB,
            ComponentName(context, MyJobService::class.java)
    )
            .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
            .setRequiresCharging(true)
            .build()
    jobScheduler.schedule(job)
}

Java

public static final int MY_BACKGROUND_JOB = 0;
...
public static void scheduleJob(Context context) {
  JobScheduler js =
      (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
  JobInfo job = new JobInfo.Builder(
    MY_BACKGROUND_JOB,
    new ComponentName(context, MyJobService.class))
      .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
      .setRequiresCharging(true)
      .build();
  js.schedule(job);
}

符合工作條件時,應用程式會收到回呼,在指定的 JobService.class 中執行 onStartJob() 方法。如要查看更多 JobScheduler 實作的範例,請參閱「JobScheduler 範例應用程式」。

WorkManager 是 JobScheduler 另一種新工具,這種 API 可讓您針對需要保證完成的背景工作進行排程,不論是否有應用程式處理程序。WorkManager 會根據裝置 API 等級等因素,選擇適當的方式執行工作 (直接透過應用程式程序中的執行緒,以及使用 JobScheduler、FirebaseJobDispatcher 或 AlarmManager)。此外,WorkManager 不需要 Play 服務,並提供多項進階功能,例如將工作鏈結在一起或檢查工作狀態。詳情請參閱 WorkManager

在應用程式執行時監控網路連線

執行中的應用程式仍可透過已註冊的 BroadcastReceiver 監聽 CONNECTIVITY_CHANGE。然而,ConnectivityManager API 只有在符合指定網路條件時,才會提供更強大的方法以要求回呼。

NetworkRequest 物件定義了有關 NetworkCapabilities 的網路回呼參數。您可以使用 NetworkRequest.Builder 類別建立 NetworkRequest 物件。然後 registerNetworkCallback() 會將 NetworkRequest 物件傳遞至系統。符合網路條件時,應用程式會收到回呼,以執行其 ConnectivityManager.NetworkCallback 類別中定義的 onAvailable() 方法。

應用程式會持續收到回呼,直到應用程式結束或呼叫 unregisterNetworkCallback() 為止。

接收圖片和影片播送的限制

在 Android 7.0 (API 級別 24) 中,應用程式無法傳送或接收 ACTION_NEW_PICTUREACTION_NEW_VIDEO 播送。如果有多個應用程式必須喚醒,然後才能處理新的圖片或影片,此限制有助於減少對應用程式效能和使用者體驗的影響。Android 7.0 (API 級別 24) 會擴充 JobInfoJobParameters,以提供替代解決方案。

在內容 URI 變更時觸發工作

如要觸發內容 URI 變更的工作,Android 7.0 (API 級別 24) 會使用下列方法擴充 JobInfo API:

JobInfo.TriggerContentUri()
封裝於內容 URI 變更時觸發工作所需的參數。
JobInfo.Builder.addTriggerContentUri()
TriggerContentUri 物件傳遞至 JobInfoContentObserver 會監控封裝的內容 URI。如果有多個 TriggerContentUri 物件與工作有關聯,即使其僅回報其中一個內容 URI 的變更,系統也會提供回呼。
新增 TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS 標記,以便在指定 URI 有任何子系變更時觸發工作。此標記與傳傳遞至 registerContentObserver()notifyForDescendants 參數對應。

注意:TriggerContentUri() 不能與 setPeriodic()setPersisted() 合併使用。如要繼續監控內容變更,請在應用程式的 JobService 完成處理最新的回呼前,排定新的 JobInfo

下列範例程式碼會安排在系統回報內容 URI MEDIA_URI 變更時觸發工作:

Kotlin

const val MY_BACKGROUND_JOB = 0
...
fun scheduleJob(context: Context) {
    val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
    val job = JobInfo.Builder(
            MY_BACKGROUND_JOB,
            ComponentName(context, MediaContentJob::class.java)
    )
            .addTriggerContentUri(
                    JobInfo.TriggerContentUri(
                            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                            JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS
                    )
            )
            .build()
    jobScheduler.schedule(job)
}

Java

public static final int MY_BACKGROUND_JOB = 0;
...
public static void scheduleJob(Context context) {
  JobScheduler js =
          (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
  JobInfo.Builder builder = new JobInfo.Builder(
          MY_BACKGROUND_JOB,
          new ComponentName(context, MediaContentJob.class));
  builder.addTriggerContentUri(
          new JobInfo.TriggerContentUri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
          JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
  js.schedule(builder.build());
}

當系統回報指定內容 URI 中的變更時,應用程式會收到回呼,並將 JobParameters 物件傳遞至 MediaContentJob.class 中的 onStartJob() 方法。

判別觸發工作的內容授權單位

Android 7.0 (API 級別 24) 也擴充了 JobParameters,以允許應用程式接收有關內容授權單位和 URI 觸發工作的實用資訊:

Uri[] getTriggeredContentUris()
傳回已觸發工作的 URI 陣列。如果沒有任何 URI 觸發工作 (例如工作因截止期限或其他原因而觸發),或者已變更的 URI 數量大於 50,這個值就會是 null
String[] getTriggeredContentAuthorities()
傳回觸發工作的內容授權單位字串陣列。 如果傳回的陣列不是 null,請使用 getTriggeredContentUris() 擷取 URI 已變更的詳細資料。

下列程式碼範例會覆寫 JobService.onStartJob() 方法,並記錄觸發工作的內容授權單位及 URI:

Kotlin

override fun onStartJob(params: JobParameters): Boolean {
    StringBuilder().apply {
        append("Media content has changed:\n")
        params.triggeredContentAuthorities?.also { authorities ->
            append("Authorities: ${authorities.joinToString(", ")}\n")
            append(params.triggeredContentUris?.joinToString("\n"))
        } ?: append("(No content)")
        Log.i(TAG, toString())
    }
    return true
}

Java

@Override
public boolean onStartJob(JobParameters params) {
  StringBuilder sb = new StringBuilder();
  sb.append("Media content has changed:\n");
  if (params.getTriggeredContentAuthorities() != null) {
      sb.append("Authorities: ");
      boolean first = true;
      for (String auth :
          params.getTriggeredContentAuthorities()) {
          if (first) {
              first = false;
          } else {
             sb.append(", ");
          }
           sb.append(auth);
      }
      if (params.getTriggeredContentUris() != null) {
          for (Uri uri : params.getTriggeredContentUris()) {
              sb.append("\n");
              sb.append(uri);
          }
      }
  } else {
      sb.append("(No content)");
  }
  Log.i(TAG, sb.toString());
  return true;
}

進一步最佳化應用程式

最佳化應用程式以在低記憶體裝置或記憶體不足的情況下執行,有助於提升效能及使用者體驗。移除背景服務與資訊清單註冊的隱式廣播接收器依附元件,有助於提高應用程式在這類裝置中的執行效能。雖然 Android 7.0 (API 級別 24) 確實會減少部分相關問題,但仍建議您直接執行應用程式,而不要完全採用這些背景處理程序。

下列 Android Debug Bridge (ADB) 指令可協助您在停用背景程序的情況下測試應用程式行為:

  • 如要模擬隱式廣播和背景服務無法使用的情況,請輸入下列指令:
  • $ adb shell cmd appops set <package_name> RUN_IN_BACKGROUND ignore
    
  • 如要重新啟用隱式廣播和背景服務,請輸入下列指令:
  • $ adb shell cmd appops set <package_name> RUN_IN_BACKGROUND allow
    
  • 您可以模擬使用者將應用程式設為「受限制」狀態,以減少背景耗電量。這項設定會使應用程式無法在背景執行。為此,請在終端機視窗中執行下列指令:
  • $ adb shell cmd appops set <PACKAGE_NAME> RUN_ANY_IN_BACKGROUND deny