后台优化

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

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

如果您的应用使用上述任何 intent,您应该尽快移除对其的依赖,以便正确地以运行 Android 7.0 或更高版本的设备为目标。Android 框架提供了多种解决方案来缓解对这些隐式广播的需求。例如,JobScheduler 和新的 WorkManager 提供了可靠的机制,可在满足特定条件(例如连接到不按流量计费的网络)时安排网络操作。您现在还可以使用 JobScheduler 来响应内容提供商的变化。JobInfo 对象封装了 JobScheduler 用于安排任务的参数。当满足任务条件时,系统会在您应用的 JobService 上执行此任务。

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

用户发起的限制

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

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

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

如果系统检测到某个应用正在消耗过多资源,它会通知用户,并为用户提供限制该应用操作的选项。可能触发此通知的行为包括:

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

施加的具体限制由设备制造商决定。例如,在运行 Android 9(API 级别 28)或更高版本的 AOSP 版本上,处于“受限”状态的后台运行应用有以下限制:

  • 无法启动前台服务
  • 现有前台服务将从前台移除
  • 闹钟不会触发
  • 任务不会执行

此外,如果应用以 Android 13(API 级别 33)或更高版本为目标并处于“受限”状态,系统不会分发 BOOT_COMPLETED 广播或 LOCKED_BOOT_COMPLETED 广播,直到应用因其他原因启动为止。

具体限制列在电量管理限制中。

接收网络活动广播的限制

如果应用在清单中注册接收 CONNECTIVITY_ACTION 广播,则以 Android 7.0(API 级别 24)为目标的应用将不会收到该广播,并且依赖此广播的进程将不会启动。这可能会给那些希望在设备连接到不按流量计费的网络时监听网络变化或执行批量网络活动的应用带来问题。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 发生更改时,您的应用会收到回调,并且一个 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