文中的源代码版本为api23
JobScheduler之重试机制
通过前面的学习,我们知道如果Job需要进行重试,那么会在JobSchedulerService.onJobCompleted方法中生成一个新的JobStatus实例,然后重新执行任务。
接下来我们就来探讨一下
- 那些场景下
Job会进行重试 - 如何进行重试的
首先我们来看一下onJobCompleted方法
public void onJobCompleted(JobStatus jobStatus, boolean needsReschedule) {
//log...
if (!stopTrackingJob(jobStatus)) {
//log...
return;
}
if (needsReschedule) {
JobStatus rescheduled = getRescheduleJobForFailure(jobStatus);
startTrackingJob(rescheduled);
} else if (jobStatus.getJob().isPeriodic()) {
JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus);
startTrackingJob(rescheduledPeriodic);
}
mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
}
通过源码的阅读,我们可以看到有两种重试场景
- 定时任务
JobInfo.isPeriodic返回true,Job会周期性的反复执行 needsReschedule参数为true 我们姑且称这种为失败重试
定时任务
定时任务是通过构造JobInfo时调用Builder.setPeriodic方法时设置的
/**
* intervalMillis为时间间隔
*/
public Builder setPeriodic(long intervalMillis) {
mIsPeriodic = true;
mIntervalMillis = intervalMillis;
mHasEarlyConstraint = mHasLateConstraint = true;
return this;
}
onJobCompleted方法中,检测到Job是定时任务的话,会通过getRescheduleJobForPeriodic重新构造一个JobStatus实例
private JobStatus getRescheduleJobForPeriodic(JobStatus periodicToReschedule) {
final long elapsedNow = SystemClock.elapsedRealtime();
// Compute how much of the period is remaining.
long runEarly = 0L;
// If this periodic was rescheduled it won't have a deadline.
if (periodicToReschedule.hasDeadlineConstraint()) {
//如果当前时间还没有到上个Job的deadLine,那么runEarly就是deadLine与当前时间的差
//否则runEarly为0
runEarly = Math.max(periodicToReschedule.getLatestRunTimeElapsed() - elapsedNow, 0L);
}
//新的最早执行时间是当前时间+runEarly
long newEarliestRunTimeElapsed = elapsedNow + runEarly;
long period = periodicToReschedule.getJob().getIntervalMillis();
//新的deadLine为newEarliestRunTimeElapsed+intervalMillis
long newLatestRuntimeElapsed = newEarliestRunTimeElapsed + period;
//...
return new JobStatus(periodicToReschedule, newEarliestRunTimeElapsed,
newLatestRuntimeElapsed, 0 /* backoffAttempt */);
}
新创建的JobStatus对象相对于老对象主要更新了一下最早执行时间和最晚执行时间(deadLine)。同时为了确保每个时间间隔(intervalMillis)至多只执行一次,最早执行时间还会加上一个runEarly.
失败重试
失败重试依赖于onJobCompleted的调用点所传的reschedule参数。
onJobCompleted的调用点只有一个JobServiceHandler.closeAndCleanupJobH,而后者的调用点则有多个,其中reschedule参数为true的调用点4个。
第一个调用点
第一个调用点发生在JobServiceHandler.handleMessage的MSG_SHUTDOWN_EXECUTIONcase中。
而触发MSG_SHUTDOWN_EXECUTION消息的地方有两个
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
JobStatus runningJob;
synchronized (mLock) {
//...
runningJob = mRunningJob;
}
if (runningJob == null || !name.equals(runningJob.getServiceComponent())) {
//第一个点
mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget();
return;
}
//...
}
/** If the client service crashes we reschedule this job and clean up. */
@Override
public void onServiceDisconnected(ComponentName name) {
//第二个点
mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget();
}
所以总结如下
- 服务绑定成功后,发现当前的
mRunningJob为空,或者服务与JobStatus中指定的组件不一致则发送MSG_SHUTDOWN_EXECUTION消息 - 服务进程发生异常(如carsh等)导致连接断开,则发送
MSG_SHUTDOWN_EXECUTION消息
第二个调用点
private void handleServiceBoundH() {
//...
if (mCancelled.get()) {
//log...
closeAndCleanupJobH(true /* reschedule */);
return;
}
//...
}
handleServiceBoundH方法的触发时机在服务绑定成功之后,onStartJob触发之前,这段时间内如果Job被取消,则会重试。
第三个调用点
private void handleFinishedH(boolean reschedule) {
switch (mVerb) {
case VERB_EXECUTING:
case VERB_STOPPING:
closeAndCleanupJobH(reschedule);
break;
default:
//log...
}
}
handleFinishedH的调用点有两个,其中一个调用点入参写死为false,所以我们只需要看另一个就好了。另一个调用点在JobServiceHandler.handleMessage的MSG_CALLBACKcase中。
public void handleMessage(Message message) {
switch (message.what) {
//...
case MSG_CALLBACK:
//...
if (mVerb == VERB_STARTING) {
//...
} else if (mVerb == VERB_EXECUTING ||
mVerb == VERB_STOPPING) {
final boolean reschedule = message.arg2 == 1;
handleFinishedH(reschedule);
} else {
///...
}
break;
//...
}
}
mVerb为VERB_EXECUTING、VERB_STOPPING的场景分别对应Job异步执行完毕的回调(jobFinished)以及执行完onStopJob后的回调。
而reshedule则对应着jobFinished方法的第二个入参以及onStopJob的返回值。
第四个调用点
第四个调用点在超时处理的VERB_STOPPINGcase中
private void handleOpTimeoutH() {
switch (mVerb) {
//...
case VERB_STOPPING:
//log...
closeAndCleanupJobH(true /* needsReschedule */);
break;
//...
}
}
也就是说如果onStopJob方法超时,也会重试。
失败重试任务
失败重试的任务是通过getRescheduleJobForFailure方法生成的
private JobStatus getRescheduleJobForFailure(JobStatus failureToReschedule) {
final long elapsedNowMillis = SystemClock.elapsedRealtime();
final JobInfo job = failureToReschedule.getJob();
final long initialBackoffMillis = job.getInitialBackoffMillis();
final int backoffAttempts = failureToReschedule.getNumFailures() + 1;
long delayMillis;
switch (job.getBackoffPolicy()) {
case JobInfo.BACKOFF_POLICY_LINEAR:
delayMillis = initialBackoffMillis * backoffAttempts;
break;
default:
if (DEBUG) {
Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential.");
}
case JobInfo.BACKOFF_POLICY_EXPONENTIAL:
delayMillis =
(long) Math.scalb(initialBackoffMillis, backoffAttempts - 1);
break;
}
delayMillis =
Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
return new JobStatus(failureToReschedule, elapsedNowMillis + delayMillis,
JobStatus.NO_LATEST_RUNTIME, backoffAttempts);
}
该方法会根据back-off策略,重新设置Job的延迟时间,同时将back-off次数加1,还有一个需要注意的是失败重试的任务没有deadLine。
总结
综上所述,我们可以整理出以下重试场景
- 定时任务
jobFinished第二个入参(needsReschedule)传trueonStopJob返回trueonStopJob方法超时- 任务在服务绑定成功之后,
onStartJob触发之前被取消 - 服务绑定成功之后发现
mRunningJob字段为空,或者启动的服务与mRunningJob指定的不一致