JobScheduler之重试机制

1,913 阅读4分钟

文中的源代码版本为api23

JobScheduler之重试机制

通过前面的学习,我们知道如果Job需要进行重试,那么会在JobSchedulerService.onJobCompleted方法中生成一个新的JobStatus实例,然后重新执行任务。 接下来我们就来探讨一下

  1. 那些场景下Job会进行重试
  2. 如何进行重试的

首先我们来看一下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();
}

通过源码的阅读,我们可以看到有两种重试场景

  1. 定时任务 JobInfo.isPeriodic返回true,Job会周期性的反复执行
  2. 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.handleMessageMSG_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();
}

所以总结如下

  1. 服务绑定成功后,发现当前的mRunningJob为空,或者服务与JobStatus中指定的组件不一致则发送MSG_SHUTDOWN_EXECUTION消息
  2. 服务进程发生异常(如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.handleMessageMSG_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;
        //...
    }
}

mVerbVERB_EXECUTINGVERB_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

总结

综上所述,我们可以整理出以下重试场景

  1. 定时任务
  2. jobFinished第二个入参(needsReschedule)传true
  3. onStopJob返回true
  4. onStopJob方法超时
  5. 任务在服务绑定成功之后,onStartJob触发之前被取消
  6. 服务绑定成功之后发现mRunningJob字段为空,或者启动的服务与mRunningJob指定的不一致