安卓任务调度局:JobScheduler 的快递派送故事

83 阅读7分钟

一、公寓楼的任务调度中心

想象安卓系统是一座名为 "安卓大厦" 的智能公寓,里面住着各种应用 "居民"。这些居民经常需要完成一些特殊任务,比如:

  • 当 Wi-Fi 连接时同步文件

  • 充电时清理缓存

  • 空闲时备份数据

但这些任务不能随便执行,需要满足特定条件才行。这时,大厦里的 "任务调度局"——JobScheduler 就登场了,它就像专业的快递中心,确保任务在最合适的时间派送。

1.1 调度局的开张

每天大厦总管(SystemServer)醒来后,会专门叫醒任务调度局的管理员:

java

// 大厦总管启动任务调度局
private void startOtherServices() {
    mSystemServiceManager.startService(JobSchedulerService.class);
}

调度局管理员上岗后,会立即组建一支 "条件检查队",包括:

  • 网络检查员(ConnectivityController):负责检查网络是否可用

  • 时间监督员(TimeController):监控任务时间是否到期

  • 电量检测员(BatteryController):确认设备是否在充电

  • 空闲侦察员(IdleController):判断设备是否空闲

  • 应用闲置观察员(AppIdleController):观察应用是否长时间未使用

java

// 组建条件检查队
public JobSchedulerService(Context context) {
    mControllers.add(ConnectivityController.get(this));
    mControllers.add(TimeController.get(this));
    mControllers.add(IdleController.get(this));
    mControllers.add(BatteryController.get(this));
    mControllers.add(AppIdleController.get(this));
}

1.2 任务调度局的 "任务账本"

管理员还会准备一本 "任务账本"(JobStore),记录所有待执行的任务。这本账本存在大厦的 "文件柜" 里(/data/system/job/jobs.xml),即使大厦停电(重启)也不会丢失记录。

java

// 创建任务账本
private JobStore(Context context, File dataDir) {
    mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
    mJobSet = new ArraySet<JobStatus>();
    readJobMapFromDisk(mJobSet); // 从文件读取历史任务
}

二、居民的任务委托:从创建到提交

2.1 填写任务委托单

公寓居民(应用)想提交一个任务时,需要填写一份详细的 "任务委托单"(JobInfo)。比如,一个云同步应用想在 Wi-Fi 连接且充电时同步数据:

java

// 创建任务委托单
ComponentName jobService = new ComponentName(this, MyJobService.class);
JobInfo jobInfo = new JobInfo.Builder(123, jobService)
        .setMinimumLatency(5000)         // 至少延迟5秒
        .setOverrideDeadline(60000)      // 最多等待60秒
        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // 需要Wi-Fi
        .setRequiresCharging(true)        // 需要充电
        .setPersisted(true)               // 设备重启后继续执行
        .build();

这张委托单就像快递单,详细记录了任务 ID、执行组件、所需条件和时间要求。

2.2 提交委托单给调度局

居民把委托单交给大厦的 "前台接待员"(JobScheduler),接待员通过大厦内部对讲机(Binder 机制)传给任务调度局:

java

// 前台接待员提交任务
JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
scheduler.schedule(jobInfo);

2.3 调度局的任务登记

管理员收到委托单后,先创建一个 "任务记录"(JobStatus),然后检查是否有相同任务:

java

// 管理员登记任务
public int schedule(JobInfo job, int uId) {
    JobStatus jobStatus = new JobStatus(job, uId);
    cancelJob(uId, job.getId()); // 取消旧任务(如果有)
    startTrackingJob(jobStatus); // 开始追踪新任务
    mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); // 通知检查任务条件
    return RESULT_SUCCESS;
}

三、条件检查:快递派送前的准备

3.1 各部门的条件检查

任务登记后,调度局的 "条件检查队" 开始工作,每个检查员都会检查自己负责的条件:

  1. 网络检查员:查看是否连接了 Wi-Fi(非流量计费网络)

  2. 电量检测员:确认设备是否在充电

  3. 时间监督员:计算任务是否到了该执行的时间

  4. 空闲侦察员:判断设备是否处于空闲状态(屏幕关闭、无操作)

java

// 条件检查示例(网络检查员)
public class ConnectivityController extends StateController {
    private void onConnectivityChanged() {
        for (JobStatus job : mTrackedJobs) {
            if (job.hasConnectivityConstraint() && isNetworkReady()) {
                notifyStateChanged(job); // 通知任务条件满足
            }
        }
    }
}

3.2 任务就绪队列

当所有条件都满足时,任务会被放入 "就绪队列"(mPendingJobs),等待派送:

java

// 加入就绪队列
private void maybeQueueReadyJobsForExecutionLockedH() {
    List<JobStatus> runnableJobs = new ArrayList<>();
    for (JobStatus job : mJobs.getJobs()) {
        if (isReadyToBeExecutedLocked(job)) {
            runnableJobs.add(job); // 条件满足,加入就绪队列
        }
    }
    mPendingJobs.addAll(runnableJobs);
}

四、任务执行:快递员的派送过程

4.1 派遣快递员(JobService)

调度局有一批 "快递员"(JobService),负责执行具体任务。当任务就绪后,管理员会派遣空闲的快递员:

java

// 派遣快递员
private void maybeRunPendingJobsH() {
    for (JobStatus job : mPendingJobs) {
        JobServiceContext availableContext = findAvailableContext();
        if (availableContext != null) {
            availableContext.executeRunnableJob(job); // 快递员执行任务
        }
    }
}

4.2 快递员上门服务

快递员接到任务后,会前往居民家中(应用进程)执行任务:

java

// 快递员上门(绑定服务)
boolean executeRunnableJob(JobStatus job) {
    final Intent intent = new Intent().setComponent(job.getServiceComponent());
    boolean binding = mContext.bindServiceAsUser(
        intent, this, Context.BIND_AUTO_CREATE, new UserHandle(job.getUserId()));
    if (binding) {
        // 服务绑定成功,准备执行任务
    }
}

4.3 执行任务回调

当快递员到达居民家后,会调用居民指定的 "任务处理方法"(onStartJob):

java

// 居民家中的任务处理
public class MyJobService extends JobService {
    @Override
    public boolean onStartJob(JobParameters params) {
        // 在这里执行具体任务(如同步数据)
        new Thread(() -> {
            syncDataToCloud();
            jobFinished(params, false); // 任务完成
        }).start();
        return true; // 任务正在执行
    }
}

五、任务取消:快递的中途召回

5.1 居民取消任务

如果居民想取消任务,可以通知前台接待员:

java

// 取消任务
scheduler.cancel(123); // 取消ID为123的任务
scheduler.cancelAll(); // 取消当前应用的所有任务

5.2 调度局的召回流程

管理员收到取消请求后,会通知所有相关检查员停止追踪该任务:

java

// 管理员召回任务
public void cancelJob(int uid, int jobId) {
    JobStatus toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
    if (toCancel != null) {
        cancelJobImpl(toCancel); // 执行取消流程
    }
}

private void cancelJobImpl(JobStatus cancelled) {
    stopTrackingJob(cancelled); // 停止条件检查
    stopJobOnServiceContextLocked(cancelled); // 停止执行中的任务
}

5.3 快递员的任务终止

如果快递员正在派送该任务,会收到终止通知:

java

// 快递员收到终止通知
void cancelExecutingJob() {
    mCallbackHandler.obtainMessage(MSG_CANCEL).sendToTarget();
}

private void handleCancelH() {
    if (mVerb == VERB_EXECUTING) {
        sendStopMessageH(); // 发送停止任务消息
    }
}

六、特殊场景:停电后的任务恢复

当大厦停电(设备重启)后,任务调度局会从 "任务账本" 中恢复所有未完成的任务:

java

// 重启后恢复任务
public void onBootPhase(int phase) {
    if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
        mReadyToRock = true;
        ArraySet<JobStatus> jobs = mJobs.getJobs();
        for (JobStatus job : jobs) {
            for (StateController controller : mControllers) {
                controller.maybeStartTrackingJob(job); // 重新开始追踪任务
            }
        }
        mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); // 检查任务条件
    }
}

七、调度局的智慧:节能与效率优化

7.1 批量任务处理

调度局不会立即执行单个任务,而是会等待多个任务就绪后批量处理,就像快递员攒够一批快递再统一派送,减少频繁奔波:

java

// 批量处理条件
if (idleCount >= MIN_IDLE_COUNT || 
    connectivityCount >= MIN_CONNECTIVITY_COUNT ||
    runnableJobs.size() >= MIN_READY_JOBS_COUNT) {
    mPendingJobs.addAll(runnableJobs);
}

7.2 退避重试策略

如果任务执行失败,调度局会采用 "退避策略",就像快递员遇到收件人不在家时,会间隔越来越长的时间再尝试:

java

// 设置退避策略
.setBackoffCriteria(3000, JobInfo.BACKOFF_POLICY_LINEAR)
// 第一次失败后等3秒,第二次等6秒,依此类推

7.3 紧急任务优先

对于有截止时间的任务(deadline),调度局会优先处理,就像快递单上标注 "加急" 的包裹:

java

// 检查截止时间
private boolean isReadyToBeExecutedLocked(JobStatus job) {
    if (job.hasDeadlineConstraint() && 
        job.getLatestRunTimeElapsed() < SystemClock.elapsedRealtime()) {
        return true; // 截止时间已到,优先执行
    }
    // 其他条件检查...
}

八、总结:JobScheduler 的工作流程图解

  1. 任务创建:应用创建 JobInfo 委托单,指定任务条件和执行组件
  2. 提交调度:通过 JobScheduler 提交任务,进入调度局登记
  3. 条件检查:各检查员(StateController)监控网络、电量等条件
  4. 任务就绪:条件满足后加入就绪队列,等待执行
  5. 任务执行:派遣 JobService 执行任务,回调 onStartJob 方法
  6. 任务取消:应用取消任务时,调度局终止执行并清理记录
  7. 重启恢复:设备重启后从文件恢复任务,继续追踪

九、实战小贴士:避免踩坑

  1. cancelAll 的陷阱

    • cancelAll()会取消同一 UID 下的所有任务
    • 如果多个应用共享 UID,一个应用调用 cancelAll 会取消其他应用的任务
  2. 耗时任务处理

    • onStartJob 运行在主线程,耗时操作必须放在子线程
    • 使用 jobFinished () 通知调度局任务完成,避免资源浪费
  3. 条件组合使用

    • 合理组合网络、电量、时间条件,减少设备唤醒次数

    • 例如:同时设置setRequiresCharging(true)setRequiredNetworkType(),确保只在充电且连 Wi-Fi 时执行

通过这个故事,我们了解到 JobScheduler 就像一个智能的快递调度中心,不仅能按条件精确派送任务,还能优化效率、节省资源,是安卓系统中处理后台任务的重要机制。