后台优化

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

为了缓解此问题,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);
}

当满足您的作业条件时,您的应用会收到一个回调以运行指定 JobService.class 中的 onStartJob() 方法。要查看更多 JobScheduler 实现示例,请参阅 JobScheduler 示例应用

JobScheduler 的一个新的替代方案是 WorkManager,这是一个允许您安排需要保证完成的后台任务的 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(s) 发生更改时,您的应用会收到回调,并且 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 调试桥 (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