后台优化

后台进程可能占用大量内存和电池电量。例如,隐式广播可能会启动许多已注册监听它的后台进程,即使这些进程可能不会执行很多工作。这会对设备性能和用户体验产生重大影响。

为了缓解此问题,Android 7.0(API 级别 24)实施了以下限制

如果您的应用程序使用任何这些意图,您应该尽快删除对它们的依赖关系,以便能够正确地针对运行 Android 7.0 或更高版本的设备。Android 框架提供了多种解决方案来减少对这些隐式广播的需求。例如,JobScheduler 和新的 WorkManager 提供了强大的机制来安排网络操作,以满足指定条件,例如连接到非计量网络。您现在还可以使用 JobScheduler 对内容提供者更改做出反应。 JobInfo 对象封装了 JobScheduler 用于安排您的作业的参数。当满足作业条件时,系统将在您的应用程序的 JobService 上执行此作业。

在本页面中,我们将学习如何使用替代方法,例如 JobScheduler,来使您的应用适应这些新的限制。

用户触发的限制

在系统设置中的 电池使用情况页面上,用户可以选择以下选项

  • 不受限制:允许所有后台工作,这可能会消耗更多电池。
  • 优化(默认):根据用户与应用的交互方式,优化应用执行后台工作的能力。
  • 受限制:完全阻止应用在后台运行。应用可能无法按预期工作。

如果应用表现出Android 性能指标中描述的一些不良行为,系统可能会提示用户限制该应用访问系统资源。

如果系统注意到应用正在消耗过多的资源,它会通知用户,并让用户可以选择限制应用的操作。可能触发通知的行为包括

  • 过度唤醒锁:当屏幕关闭时,保持一个部分唤醒锁一个小时
  • 过多的后台服务:如果应用的目标 API 级别低于 26,并且具有过多的后台服务

实施的具体限制由设备制造商决定。例如,在运行 Android 9(API 级别 28)或更高版本的 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);
}

当满足您的作业条件时,您的应用会收到一个回调以运行 onStartJob() 方法,该方法位于指定的 JobService.class 中。若要查看 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 对象传递给 JobInfo。一个 ContentObserver 监视封装的内容 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。
String[] getTriggeredContentAuthorities()
返回已触发作业的内容权限的字符串数组。如果返回的数组不为空,请使用 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 调试桥 (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