咱们永远都须要流畅的用户体验,但很遗憾咱们手上的硬件资源却老是和这个需求唱反调。这也是 Android 平台不断努力的切入点——从 API 26开始,Android 对后台服务引入了严格的限制。基本上,除非您的应用在前台运行,不然系统将在几分钟内中止应用的全部后台服务。
android
因为对后台服务的这些限制,JobScheduler 已经成为执行后台任务的实际解决方案。对于熟悉服务的开发者来讲,JobScheduler 使用起来一般很简单,固然也存在少许例外。咱们此次就来探讨其中一个例外。数据库
假如您正在搭建一个 Android TV 应用。频道对电视应用很是重要,所以您的应用须要可以执行至少五种与频道有关的后台操做:发布频道,向频道添加节目,将有关频道的日志发送到远程服务器,更新频道的元数据,以及删除频道。在 Android 8.0(Oreo)以前,这五个操做中的每个均可以在后台服务中实现。然而,从 API 26 开始,您必须明智地决定,哪些应该沿用原有的普通后台 Service,哪些应该使用 JobService。bash
若是只考虑电视 App 的使用场景,上述五个操做里,其实只有 “频道发布” 能够作成一个原有的普通后台服务。在某些场合下,频道发布涉及三个步骤:首先用户单击按钮开始该过程; 而后,应用启动后台操做来建立和提交出版物; 最后,用户经过用户界面以确认订阅。至此您能够看到,发布频道须要用户交互,所以须要可见的 Activity。因此,ChannelPublisherService 能够是一个 IntentService,负责处理后台逻辑。您不该该在这里使用 JobService,由于 JobService 会引入延迟,而用户交互一般须要您的应用进行即时响应。服务器
对于其余四个操做,您应该使用 JobService; 由于它们均可以在您的应用位于后台时执行。因此您应该分别建立 ChannelProgramsJobService,ChannelLoggerJobService,ChannelMetadataJobService,和 ChannelDeletionJobService。app
因为以上全部的四个 JobService 都在处理 Channel 对象,您彷佛能够方便地使用 channelId 做为 jobId。可是因为 JobService 在 Android Framework 中设计的方式,您不能这样作。如下是 jobId 的官方描述:
ide
应用为这个做业提供的 ID。 随后调用取消,或建立相同 jobId 的做业,
将会更新已经存在的同一个 ID 的做业。该 ID 在同一个 uid 的全部客户端(不仅是同一个应用包)中必须是惟一的。
您须要确保该 ID 在应用更新时始终保持稳定,所以它可能不该该基于资源 ID。 复制代码
根据以上的描述,即便您使用 4 个不一样的 Java 对象(即 -JobService),也仍然不能使用 channelId来做为它们的 jobId。类级别的命名空间不能帮助到您。flex
这确实是个问题。您须要一个稳定、可扩展的方式来将 channelId 和它的 jobId 关联起来。而最糟的结果莫过于,因为 jobId 冲突,致使不一样的频道互相覆盖操做。若是jobId 是 String 类型,而不是 Integer 类型的话,解决起来就很容易:ChannelProgramsJobService 的 jobId = "ChannelPrograms" + channelId, ChannelLoggerJobService 的 jobId = "ChannelLogs" + channelId,等等。但由于 jobId属于 Integer 类型,而不属于 String 类型,因此您就要设计一个智能的系统,用来为您的做业生成可重复使用 jobId。ui
重点来了 —— 如今咱们来聊聊 JobIdManager,看怎样用它来解决这个问题。spa
JobIdManager 是一个类别,您能够根据本身的应用需求进行调整。对于目前谈到的这个电视应用,基本构想是:使用一个 channelId 处理与 Channel 相关的全部做业 。下面咱们先来看看这个样本 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 Integer。固然,严格来讲,它不必定是 Short。只要您的前缀和 channelId 组合成一个不溢出的 Integer,它就能有效运做。但边际处理在坚实的软件工程中相当重要。因此,除非您真的走投无路,不然就强制为 Short 类型吧。在实践中,为远程服务器上具备较大 ID 的对象执行此操做的一种方法是,在本地数据库或 content provider 中定义一个密钥,并使用该密钥生成您的jobId。
您的整个应用只应该有一个 JobIdManager 类。该类能够为应用的全部做业生成 jobId:不管这些工做是否与频道、用户或者其余任何事情有关。事实上咱们的示例 JobIdManager 类指出了这一点:并非全部 JOB_TYPE 都与 Channel 操做有关。一个做业类型与用户偏好有关,一个与用户行为有关。JobIdManager 经过为每一个做业类型分配一个不一样的前缀来覆盖以上种类型。
您的应用中的每一个 -JobService,都必须拥有惟一和最终的 JOB_TYPE_ 前缀。再强调一次,必须是完全的一对一关系。
如下代码片断摘自 ChannelProgramsJobService,它为咱们演示了如何在您的项目中使用 JobIdManager。不管什么时候须要安排新做业,都会使用 JobIdManager.getJobId(…) 生成 jobId。
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;
}
...
}复制代码
相信看到这里,您对如何针对不一样的场景来设计后台机制有了比较清晰的认识。但无论怎样,从 Oreo 开始对后台任务作出的种种限制都会对提高用户体验有着现实的意义,这也要求开发者们对本身的应用须要完成以及什么时候须要完成一些事情有着更精准的规划。若是您有什么问题,或者经验之谈,欢迎在下面和咱们分享哦~
* 注:感谢 Christopher Tate 和 Trevor Johnsz 在本文撰写中提供的宝贵反馈意见