最佳化背景程序

背景程序可能會耗用大量記憶體和電量。舉例來說,隱式廣播可能會啟動多個已註冊監聽的背景程序,即使這些程序可能執行少量工作,也可能會對裝置效能和使用者體驗產生重大影響。

為減輕這個問題的影響,Android 7.0 (API 級別 24) 會套用下列限制:

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

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

使用者啟動限制

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

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

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

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

  • Wake Lock 過多:在螢幕關閉時,持續 1 小時的 1 個部分 Wake Lock
  • 背景服務過多:應用程式指定 API 26 以下級別,且提供的背景服務過多

精確的限制項目由裝置製造商決定。舉例來說,在執行 Android 9 (API 級別 28) 以上的 Android 開放原始碼計畫版本中,對在背景執行且處於「受限制」狀態的應用程式設有以下限制:

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

此外,如果應用程式指定 Android 13 (API 級別 33) 以上版本且處於「受限制」狀態,系統不會提供 BOOT_COMPLETEDLOCKED_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