从源码角度看 JobScheduler

简介

做过 Android 应用开发的相信对 JobScheduler 都比较熟悉了,顾名思义,任务调度者的作用在于只需要获取到 JobScheduler 的服务、在 JobService 的子类下实现 onStartJob 与 onStopJob 方法,随后调用 JobScheduler.schedule 方法便可以将任务的调度任务交给系统。借助于此,开发者只需要关注任务在开发执行与结束执行时的业务逻辑,而不需要处理根据系统状态来调度任务启动停止时机的重担

Android O 之后,系统对 AMS 中的后台服务与隐性广播分别做出了严格的限制。如果一个进程正在处于后台的状态,那么他将不能通过调用 startService 来拉起一个后台的服务。如果一个应用发出了一个隐性的广播,那么使用静态 manifest 注册广播的接收者将不再能收到隐性 Intent 发来的通知。对于这两种限制,谷歌推荐的解决方案之一就是使用 JobScheduler,通过将业务逻辑从后台服务 onStartCommand 挪到 JobService的 onStartJob 方法可以越过系统对于后台服务的启动限制;通过设置 Job 的触发条件,比如说电池充满,网络已连接,可以实现等同于监听电池广播与 WIFI 广播变化的功能

JobScheudler 是如何能越过 AMS 限制的呢,这篇文章我将由客户端与服务端类图入手来讲解其中重要的类及其作用,通过这两张图大家可以对 JobScheduler 有个初步的认识。随后,我会结合着 JobScheduler.schedule 方法调用的时序图来分析 JobScheduler 从客户端到服务端的具体源码。文章最后,我会总结 JobScheduler 大体实现思路以及为什么谷歌推荐使用它来替代已经被限制的后台服务与隐性广播

本篇文章分析的源码是基于 Android 8.0的,但是JobScheduler 从5.0之后其实大致的流程都差不多,到了8.0也只是添加了一些细节

相关类图

客户端

查看大图

  • JobService: 使用 Android JobScheduler 必须在 xml 中要声明的一个 Service,它的启动需要使用到 android.permission.BIND\_JOB\_SERVICE 这个权限。JobService 的启动是由 JobSchedulerService 在 system_server 进程中以 bindService 的形式拉起来的。在 JobService 调用 onBind 之后会返回 IJobService.Stub 对象到 system_server,随后 JobSchedulerService 拿到 IJobService.Proxy 的对象后,随时可以调用 startJob 方法来触发客户端 Service 中的 onStartJob 这个方法
  • JobInterface: IJobService.Stub 的实现类,在这里是 system_server 的服务端,调用 startJob 之后会调用到这个类的方法,随后经过层层调用,最终调用了用户实现的 onStartJob 方法
  • JobServiceEngine: 顾名思义,这个类其实就相当于一个客户端的 JobService 的引擎,主要用于处理system_server服务端请求与客户端业务逻辑的调用
  • JobInfo: 一个 Job 信息的抽象,包含了任务实例中所有的信息,客户端常常需要对它先做好声明,随后才能发起派发请求到服务端
  • JobParameters: 一个 Job 参数的抽象,它的属性中包含了 IJobCallback.Proxy 对象,用于回复 system_server JobSchedulerService 执行 onStartJob 已经结束的信息
  • JobSchedulerImpl: JobSchedler 抽象类的真正实现者,其主要的操作是通过 binder call 向 system_server 发起请求

服务端

查看大图

  • JobSchedlerService: 用来在 system_server 进程中管理 JobScheduler 服务的主体,相当于一个大总管管理与协调各个重要的模块
  • JobSchedulerStub: JobScheduler 的服务端,用户处理客户端传来的 binder call 请求
  • JobStore: 服务端存储正在进行中的 Job 信息,提供了插入、删除、遍历的功能
  • JobSet: Job 的集合抽象,提供了 CRUD 的方法供 JobStore 进行使用
  • JobStatus: 一个 Job 在服务端运行状态的抽象,直接引用着从客户端传来的 JobInfo 对象
  • JobStatusFuntor: 用户负责处理任务的接口抽象
  • JobServiceContext: 真正会执行 bindService 操作的类,实现了 ServiceConnection 接口,从这个角度来看,system_server 相当于是一个客户端,而 APP 自己实现的 MyJobService onBind 返回的 JobInterface 对象才是一个服务端,当 JobServiceContext 的 onServiceConnected 方法触发后,只有调用 mService.startJob 后,APP 的 onStartJob 方法才会被触发
  • JobCallback: 用于实现 JobSchedulerService 与 App 双工通信的一个服务类,当 APP 执行完成 onStartJob 方法后,会调用 JobCallback 代理类的方法,通知 system_server APP 的操作已经执行完毕

调用 JobScheduler.schedule 后会发生什么

整体时序图

查看大图

APP->SystemServer

public void scheduleJob(View v) {
    // 使用建造者模式来初始化一个 JobInfo 实例
    JobInfo.Builder builder = new JobInfo.Builder(mJobId++, mServiceComponent);

    ...

    // 得到 JobScheduler 服务
    JobScheduler tm = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
    // 调用 JobScheduler.schedule 来开始调度被初始化的任务
    tm.schedule(builder.build());
}

JobInfo 是使用建造者模式进行初始化的,jobId 与需要被 JobScheduler 唤起的 Service 的 ComponentName 是必须填入的参数

http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/java/android/app/JobSchedulerImpl.java

37    /* package */ JobSchedulerImpl(IJobScheduler binder) {
38        mBinder = binder;
39    }
...
41    @Override
42    public int schedule(JobInfo job) {
43        try {
44            return mBinder.schedule(job);
45        } catch (RemoteException e) {
46            return JobScheduler.RESULT_FAILURE;
47        }
48    }

getSystemService 得到的是在 system_server 进程启动时初始化的 JobSchedulerImpl 对象,它的 mBinder 成员对象是一个 IJobScheduler.Proxy 对象,客户端借助着它可以 binder call 到 JobSchedulerService 中,调用 schedule 方法

SystemServer 处理任务

1788    final class JobSchedulerStub extends IJobScheduler.Stub {
...
1843        @Override
1844        public int schedule(JobInfo job) throws RemoteException {
...
1865            try {
1866                return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, -1, null);
1867            } finally {
1868                Binder.restoreCallingIdentity(ident);
1869            }
1870        }
...
        }

JobScheduler 的 enqueue, scheudle 和 scheduleAsPackage方法最终都会调用到 JobSchedulerService 的 scheduleAsPackage 方法

http://androidxref.com/8.0.0_r4/xref/frameworks/base/services/core/java/com/android/server/job/JobSchedulerService.java

654    public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
655            int userId, String tag) {
...
709            if (isReadyToBeExecutedLocked(jobStatus)) {
...
712                mJobPackageTracker.notePending(jobStatus);
713                addOrderedItem(mPendingJobs, jobStatus, mEnqueueTimeComparator);
714                maybeRunPendingJobsLocked();
715            }
716        }
717        return JobScheduler.RESULT_SUCCESS;
718    }

1474    private boolean isReadyToBeExecutedLocked(JobStatus job) {
1475        final boolean jobReady = job.isReady();
...
1516        if (jobPending || jobActive) {
1517            return false;
1518        }
1519
1520        final boolean componentPresent;
1521        try {
1522            componentPresent = (AppGlobals.getPackageManager().getServiceInfo(
1523                    job.getServiceComponent(), PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
1524                    userId) != null);
1525        } catch (RemoteException e) {
1526            throw e.rethrowAsRuntimeException();
1527        }
...
1535        return componentPresent;
1536    }

任务的状态与任务是否已经就绪的逻辑都封装在 JobInfo 中,通过它们可以判断出任务的就绪与有效状态。同时,查看客户端 JobService 的声明状态来确保 JobService 是可以被拉起的

1543    private void maybeRunPendingJobsLocked() {
1547        assignJobsToContextsLocked();
1548        reportActiveLocked();
1549    }
...
1581    private void assignJobsToContextsLocked() {
...
1686        mJobPackageTracker.noteConcurrency(numActive, numForeground);
1687        for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) {
1688            boolean preservePreferredUid = false;
1689            if (act[i]) {
...
1698                } else {
...
1707                    if (!mActiveServices.get(i).executeRunnableJob(pendingJob)) {
1708                        Slog.d(TAG, "Error executing " + pendingJob);
1709                    }
...
1714            }
...
1718        }
1719    }

调用 maybeRunPendingJobsLocked 之后会调用到 executeRunnableJob 方法,随后会触发拉起客户端 JobService 的操作

top Created with Sketch.