后台优化

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

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

如果您的应用使用了其中的任何 intent,您应该尽快取消对它们的依赖,以便正确定位到搭载 Android 7.0 或更高版本的设备。Android 框架提供了多种解决方案来缓解对这些隐式广播的需求。例如,JobScheduler 和新的 WorkManager 提供了强大的机制,可在满足指定条件时(例如连接到不按流量计费网络时)调度网络操作。现在,您还可以使用 JobScheduler 来响应对 content provider 的更改。JobInfo 对象可封装 JobScheduler 用来调度作业的参数。当满足作业条件时,系统会在应用的 JobService 上执行此作业。

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

用户发起的限制

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

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

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

如果系统发现应用消耗的资源过多,就会通知用户,并为用户提供限制应用操作的选项。可触发此类通知的行为包括:

  • 唤醒锁定过多:屏幕关闭时持续 1 小时的 1 次部分唤醒锁定
  • 后台服务过多:应用的目标 API 级别低于 26 且后台服务过多

施加的确切限制由设备制造商决定。例如,在搭载 Android 9(API 级别 28)或更高版本的 AOSP build 中,在后台运行且处于“受限”状态的应用存在以下限制:

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

此外,如果应用以 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 示例应用

JobStessduler 的一个新替代工具是 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