Android 平台不断努力改善用户体验,从 API 级别 26 开始,对后台服务引入了严格的限制。基本上,除非你的应用程序在 前台 运行,否则系统将在几分钟内停止所有应用的后台服务。
由于对后台服务的这些限制, JobScheduler 作业已经成为执行后台任务的事实上的解决方案。对于熟悉服务的人员, JobScheduler 通常使用简单:除少数情况外,我们现在将探索其中一个。
想像你正在建立一个 Android TV 应用程序。由于渠道对电视应用程序非常重要,因此你的应用程序应能够在频道上执行至少五种不同的后台操作:发布频道,向频道添加节目,将有关频道的日志发送到远程服务器,更新频道的元数据,并删除频道。在 Android 8.0(Oreo)之前,这五个操作中的每一个都可以在后台服务中实现。然而,从 API 26 开始,你必须明智地决定哪些应该是简单的旧就、后台 Service ,哪些应该是 JobService 。
在电视应用程序的情况下,上述五个操作,只有频道发布可以是一个简单的旧的后台服务。对于某些上下文,通道发布涉及三个步骤:首先用户单击按钮开始该过程; 第二,应用程序启动后台操作来创建和提交出版物; 第三,用户获取用户界面来确认订阅。因此,你可以看到,发布频道需要用户交互,因此需要可见的活动。因此, ChannelPublisherService 可以 IntentService 处理后台部分。你不应该使用 JobService 这里的原因 是因为 JobService 会引入执行延迟,而用户交互通常需要你的应用程序的即时响应。
然而,对于其他四个操作,你应该使用 JobServices ; 这是因为所有这些都可能在你的应用程序在后台执行。所以分别,你应该有 ChannelProgramsJobService , ChannelLoggerJobService , ChannelMetadataJobService ,和 ChannelDeletionJobService 。
由于以上所有的四个 JobService 处理 Channel 对象,所以使用它们 channelId 中的 jobId 每一个应该是方便的。但是由于 JobService 是 Android 框架中设计的方式 ,你不能。以下是 jobId 的官方描述 )
- Application-provided id for this job. Subsequent calls to cancel,
- or jobs created with the same jobId, will update the pre-existing
- job with the same id. This ID must be unique across all clients
- of the same uid (not just the same package). You will want to
- make sure this is a stable id across app updates, so probably not
- based on a resource ID.
-
告诉你的是什么,即使你使用 4 个不同的 Java 对象(即-JobServices),你仍然不能使用 channelId 作为它们 jobId 。你不会得到类级别命名空间的信用。
这确实是一个真正的问题。你需要一个稳定和可扩展的方式来将 channelId 其与其一组相关联 jobId 。你想要的最后一件事是由于 jobId 碰撞而使不同的频道覆盖对方的操作。是 jobId 是 String 类型替代整数,该解决方案将是很容易: 对于 ChannelProgramsJobService , jobId= "ChannelPrograms" + channelId , 对 ChannelLoggerJobService , jobId= "ChannelLogs" + channelId 等,但因为 jobId 是一个整数,而不是一个字符串,你要设计一个聪明的系统,可重复使用的产生 job 的 jobId 。为此,你可以使用以下 JobIdManager 。
JobIdManager 是根据你的应用需求调整的课程。对于这个现在的电视应用程序,基本思想是使用一个 channelId 处理 Channels 的所有工作。要加快澄清:我们先来看看这个样本 JobIdManager 类的代码 ,然后再讨论一下。
- public class JobIdManager {
-
- public static final int JOB_TYPE_CHANNEL_PROGRAMS = 1;
- public static final int JOB_TYPE_CHANNEL_METADATA = 2;
- public static final int JOB_TYPE_CHANNEL_DELETION = 3;
- public static final int JOB_TYPE_CHANNEL_LOGGER = 4;
-
- public static final int JOB_TYPE_USER_PREFS = 11;
- public static final int JOB_TYPE_USER_BEHAVIOR = 21;
-
- @IntDef(value = {
- JOB_TYPE_CHANNEL_PROGRAMS,
- JOB_TYPE_CHANNEL_METADATA,
- JOB_TYPE_CHANNEL_DELETION,
- JOB_TYPE_CHANNEL_LOGGER,
- JOB_TYPE_USER_PREFS,
- JOB_TYPE_USER_BEHAVIOR
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface JobType {
- }
-
- //16-1 for short. Adjust per your needs
- private static final int JOB_TYPE_SHIFTS = 15;
-
- public static int getJobId(@JobType int jobType, int objectId) {
- if ( 0 < objectId && objectId < (1<< JOB_TYPE_SHIFTS) ) {
- return (jobType << JOB_TYPE_SHIFTS) + objectId;
- } else {
- String err = String.format("objectId %s must be between %s and %s",
- objectId,0,(1<<JOB_TYPE_SHIFTS));
- throw new IllegalArgumentException(err);
- }
- }
- }
-
正如你所看到的, JobIdManager 只需组合一个前缀与一个 channelId 来获得 jobId 。然而,这种优雅的简单只是冰山一角。让我们考虑下面的假设和注意事项。
第一个洞察力:你必须能够强制 channelId 进入一个 Short,所以当你结合 channelId 一个前缀你最终会得到一个有效的 Java 整数。当然,严格来说,它不一定是短的。只要你的前缀和 channelId 组合成一个不溢出的整数,它将会工作。但边缘对声音工程至关重要。所以,除非你真的没有选择,否则就用短暂的强制去。你可以在实践中为远程服务器上具有较大 ID 的对象执行此操作的一种方法是在本地数据库或内容提供者中定义一个密钥,并使用该密钥生成你 jobId 的密钥。
第二个见解:你的整个应用程序应该只有一个 JobIdManager 类。该类应该 jobId 为你的所有应用程序的工作生成:这些工作是否与 Channels,Users 或 Cats 和 Dogs 有关。示例 JobIdManager 类指出了这一点:并不是所有 JOB_TYPE 的都与 Channel 操作有关。一个 job 类型与用户 prefs 有关,一个与用户行为有关。 JobIdManager 通过为每个 job 类型分配一个不同的前缀,它们的帐户都是这样。
第三个见解:对于每个 -JobService 应用程序,你必须具有唯一和最终的 JOB TYPE 前缀 。再次,这必须是一个详尽的一对一的关系。
以下代码片段是 ChannelProgramsJobService 演示如何在你的项目中使用 JobIdManager。无论何时需要安排新工作,都会生成 jobId 使用 JobIdManager.getJobId(...)
- import android.app.job.JobInfo;
- import android.app.job.JobParameters;
- import android.app.job.JobService;
- import android.content.ComponentName;
- import android.content.Context;
- import android.os.PersistableBundle;
-
- public class ChannelProgramsJobService extends JobService {
-
- private static final String CHANNEL_ID = "channelId";
- . . .
-
- public static void schedulePeriodicJob(Context context,
- final int channelId,
- String channelName,
- long intervalMillis,
- long flexMillis)
- {
- JobInfo.Builder builder = scheduleJob(context, channelId);
- builder.setPeriodic(intervalMillis, flexMillis);
-
- JobScheduler scheduler =
- (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
- if (JobScheduler.RESULT_SUCCESS != scheduler.schedule(builder.build())) {
- //todo what? log to server as analytics maybe?
- Log.d(TAG, "could not schedule program updates for channel " + channelName);
- }
- }
-
- private static JobInfo.Builder scheduleJob(Context context,final int channelId){
- ComponentName componentName =
- new ComponentName(context, ChannelProgramsJobService.class);
- final int jobId = JobIdManager
- .getJobId(JobIdManager.JOB_TYPE_CHANNEL_PROGRAMS, channelId);
- PersistableBundle bundle = new PersistableBundle();
- bundle.putInt(CHANNEL_ID, channelId);
- JobInfo.Builder builder = new JobInfo.Builder(jobId, componentName);
- builder.setPersisted(true);
- builder.setExtras(bundle);
- builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
- return builder;
- }
-
- ...
- }
-
脚注:感谢 Christopher Tate 和 Trevor Johns 的宝贵意见